Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3346ce8
Show waiting dependency details in dashboard
adamint May 14, 2026
3ac6f77
Add state description actions
adamint May 14, 2026
cec54b4
Use localized strings in state detail tests
adamint May 14, 2026
e9a8191
Refine state description start action
adamint May 14, 2026
f5e6cbb
Add waiting dependency coverage for all wait types
adamint May 14, 2026
3e277f1
Normalize state description punctuation
adamint May 14, 2026
cc42763
Align state description start action
adamint May 14, 2026
0dac353
Make state action fully inline
adamint May 14, 2026
bdbc841
Align inline state action icon
adamint May 14, 2026
0fae2e2
Allow BasketService HTTP health checks
adamint May 14, 2026
3818870
Rename state action to Start anyway
adamint May 14, 2026
39547d0
Include waiting dependencies in wait cancellation
adamint May 14, 2026
86751b5
Address waiting dependency dashboard feedback
adamint May 15, 2026
a1238ec
Address waiting dependency review feedback
adamint May 15, 2026
9c20407
Address waiting dependency review feedback
adamint May 18, 2026
23d6f2a
Merge main into waiting dependency details
adamint May 18, 2026
12452eb
Fix TestShop build after waiting dependency updates
adamint May 18, 2026
00dcf27
Remove manual TestShop start gate
adamint May 18, 2026
3126b2b
Fix waiting dependency property export
adamint May 18, 2026
d05e4da
Preserve structured resource properties in JSON
adamint May 18, 2026
8d13578
Merge remote-tracking branch 'microsoft/main' into dev/adamint/waitin…
adamint May 18, 2026
0679b59
Fix backchannel property test for JSON values
adamint May 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion playground/TestShop/BasketService/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"AllowedHosts": "*",
"Kestrel": {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

????

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The removed Kestrel HTTP/2-only default was blocking the new HTTP health check on BasketService. This playground change needs WithHttpHealthCheck("/health", endpointName: "http") so the dashboard can show healthy/starting dependency details for the waiting-state demo.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but this breaks the basket service. It needs HTTP/2

Image

"EndpointDefaults": {
"Protocols": "Http2"
"Protocols": "Http1AndHttp2"
Comment thread
adamint marked this conversation as resolved.
Outdated
}
},
"Aspire": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
@using Aspire.Dashboard.Resources
Comment thread
adamint marked this conversation as resolved.
Outdated
@using Aspire.Dashboard.Utils

@if (_waitingResources.Count == 0)
{
<FluentHighlighter HighlightedText="@HighlightText" Text="@Value" />
}
else
{
<FluentHighlighter HighlightedText="@HighlightText" Text="@_prefix" />
@for (var index = 0; index < _waitingResources.Count; index++)
{
var waitingResource = _waitingResources[index];

if (index > 0)
{
<span>, </span>
}

if (waitingResource.Resource is { } resource)
{
<FluentAnchor Href="@DashboardUrls.ResourcesUrl(resource: resource.Name)" Appearance="Appearance.Hypertext">
<FluentHighlighter HighlightedText="@HighlightText" Text="@waitingResource.DisplayName" />
</FluentAnchor>
}
else
{
<FluentHighlighter HighlightedText="@HighlightText" Text="@waitingResource.DisplayName" />
}
}
<FluentHighlighter HighlightedText="@HighlightText" Text="@_suffix" />
}

@if (StartCommand is not null)
{
<span> </span>
<button type="button"
class="state-description-action"
disabled="@IsStartCommandDisabled"
@onclick="OnStartCommandAsync"
title="@StartCommandTitle">
<FluentIcon Value="@(new Icons.Filled.Size16.Play())" Width="0.8em" Class="state-description-action-icon" />
<FluentHighlighter HighlightedText="@HighlightText" Text="@ControlsLoc[nameof(ControlsStrings.ResourceStateDescriptionStartNowAction)]" />
</button>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Dashboard.Model;
using Aspire.Dashboard.Resources;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Localization;

namespace Aspire.Dashboard.Components.Controls.PropertyValues;

public partial class ResourceStateDescriptionValue
{
private const string WaitingResourcePlaceholder = "{0}";
private string _prefix = string.Empty;
private string _suffix = string.Empty;
private List<WaitingResource> _waitingResources = [];
private CommandViewModel? StartCommand { get; set; }
private bool IsStartCommandDisabled => StartCommand is null || StartCommand.State == CommandViewModelState.Disabled || OnExecuteCommandAsync is null || IsStartCommandExecuting;
private bool IsStartCommandExecuting => StartCommand is not null && (IsCommandExecuting?.Invoke(Resource, StartCommand) ?? false);
private string StartCommandTitle => StartCommand?.GetDisplayDescription() ?? StartCommand?.GetDisplayName() ?? string.Empty;

[Parameter, EditorRequired]
public required string Value { get; set; }

[Parameter, EditorRequired]
public required string HighlightText { get; set; }

[Parameter, EditorRequired]
public required ResourceViewModel Resource { get; set; }

[Parameter, EditorRequired]
public required IDictionary<string, ResourceViewModel> ResourceByName { get; set; }

[Parameter]
public bool ShowHiddenResources { get; set; }

[Parameter]
public Func<ResourceViewModel, CommandViewModel, Task>? OnExecuteCommandAsync { get; set; }

[Parameter]
public Func<ResourceViewModel, CommandViewModel, bool>? IsCommandExecuting { get; set; }

[Inject]
public required IStringLocalizer<Columns> Loc { get; init; }

[Inject]
public required IStringLocalizer<ControlsStrings> ControlsLoc { get; init; }

protected override void OnParametersSet()
{
_waitingResources = [];
_prefix = string.Empty;
_suffix = string.Empty;
StartCommand = GetVisibleStartCommand();

if (!Resource.TryGetWaitingForDependencies(out var dependencies))
{
return;
}

var waitingResourceNames = string.Join(", ", dependencies);
if (!TrySplitWaitingForFormat(waitingResourceNames, out _prefix, out _suffix))
{
return;
}

foreach (var dependency in dependencies)
{
if (TryGetVisibleResource(dependency, out var resource))
{
_waitingResources.Add(new WaitingResource(resource, ResourceViewModel.GetResourceName(resource, ResourceByName)));
}
else
{
_waitingResources.Add(new WaitingResource(null, dependency));
}
}
}

private bool TrySplitWaitingForFormat(string waitingResourceNames, out string prefix, out string suffix)
{
var format = Loc[nameof(Columns.StateColumnResourceWaitingFor)].Value;
var placeholderIndex = format.IndexOf(WaitingResourcePlaceholder, StringComparison.Ordinal);

if (placeholderIndex >= 0)
{
prefix = format[..placeholderIndex];
suffix = format[(placeholderIndex + WaitingResourcePlaceholder.Length)..];
return true;
}

var resourceNamesIndex = Value.IndexOf(waitingResourceNames, StringComparison.Ordinal);
if (resourceNamesIndex >= 0)
{
prefix = Value[..resourceNamesIndex];
suffix = Value[(resourceNamesIndex + waitingResourceNames.Length)..];
return true;
}

prefix = string.Empty;
suffix = string.Empty;
return false;
}

private bool TryGetVisibleResource(string resourceName, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out ResourceViewModel? resource)
{
if (ResourceViewModel.TryGetResourceByName(resourceName, ResourceByName, out resource) && !resource.IsResourceHidden(ShowHiddenResources))
{
return true;
}

resource = null;
return false;
}

private CommandViewModel? GetVisibleStartCommand()
{
foreach (var command in Resource.Commands)
{
if (string.Equals(command.Name, CommandViewModel.StartCommand, StringComparisons.CommandName) &&
command.State != CommandViewModelState.Hidden)
{
return command;
}
}

return null;
}

private Task OnStartCommandAsync()
{
if (StartCommand is not { } startCommand ||
IsStartCommandDisabled ||
OnExecuteCommandAsync is not { } onExecuteCommandAsync)
{
return Task.CompletedTask;
}

return onExecuteCommandAsync(Resource, startCommand);
}

private sealed record WaitingResource(ResourceViewModel? Resource, string DisplayName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.state-description-action {
display: inline;
padding: 0;
border: 0;
background: none;
color: var(--accent-foreground-rest);
cursor: pointer;
font: inherit;
font-weight: inherit;
line-height: inherit;
text-decoration: none;
vertical-align: baseline;
}

.state-description-action:hover:not(:disabled) {
color: var(--accent-foreground-hover);
}

.state-description-action:disabled {
cursor: default;
opacity: var(--disabled-opacity);
}

.state-description-action-icon {
display: inline;
margin-inline-end: 0.2em;
vertical-align: text-bottom;
}
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,18 @@ protected override void OnParametersSet()
Type = typeof(ResourceHealthStateValue),
Parameters = { ["Resource"] = _resource }
},
[StateDescriptionPropertyKey] = new ComponentMetadata
{
Type = typeof(ResourceStateDescriptionValue),
Parameters =
{
["Resource"] = _resource,
["ResourceByName"] = ResourceByName,
["ShowHiddenResources"] = ShowHiddenResources,
["OnExecuteCommandAsync"] = (Func<ResourceViewModel, CommandViewModel, Task>)ExecuteResourceCommandAsync,
["IsCommandExecuting"] = IsCommandExecuting,
}
}
};

// For parameter resources whose value is unset, render the same "Value not set" affordance
Expand All @@ -225,7 +237,7 @@ protected override void OnParametersSet()
Parameters =
{
["Resource"] = _resource,
["OnExecuteCommandAsync"] = (Func<ResourceViewModel, CommandViewModel, Task>)ExecuteParameterCommandAsync,
["OnExecuteCommandAsync"] = (Func<ResourceViewModel, CommandViewModel, Task>)ExecuteResourceCommandAsync,
["IsCommandExecuting"] = IsCommandExecuting,
}
};
Expand Down Expand Up @@ -310,7 +322,7 @@ private void UpdateResourceActionsMenu()
showUrls: true);
}

private async Task ExecuteParameterCommandAsync(ResourceViewModel resource, CommandViewModel command)
private async Task ExecuteResourceCommandAsync(ResourceViewModel resource, CommandViewModel command)
{
await CommandSelected.InvokeAsync(command);
}
Expand Down
5 changes: 5 additions & 0 deletions src/Aspire.Dashboard/Model/ResourceStateViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ internal static string GetResourceStateTooltip(ResourceViewModel resource, IStri
}
else if (resource.IsWaiting())
{
if (resource.TryGetWaitingForDependencies(out var dependencies))
{
return loc.GetString(nameof(Columns.StateColumnResourceWaitingFor), string.Join(", ", dependencies));
}

return loc[nameof(Columns.StateColumnResourceWaiting)];
}
else if (resource.IsNotStarted())
Expand Down
5 changes: 5 additions & 0 deletions src/Aspire.Dashboard/Model/ResourceViewModelExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ public static bool TryGetAppArgsSensitivity(this ResourceViewModel resource, out
return resource.TryGetCustomDataBoolArray(KnownProperties.Resource.AppArgsSensitivity, out argParams);
}

public static bool TryGetWaitingForDependencies(this ResourceViewModel resource, out ImmutableArray<string> dependencies)
{
return resource.TryGetCustomDataStringArray(KnownProperties.Resource.WaitingFor, out dependencies) && dependencies.Length > 0;
}

private static bool TryGetCustomDataString(this ResourceViewModel resource, string key, [NotNullWhen(returnValue: true)] out string? s)
{
if (resource.Properties.TryGetValue(key, out var property) && property.Value.TryConvertToString(out var valueString))
Expand Down
21 changes: 15 additions & 6 deletions src/Aspire.Dashboard/Resources/Columns.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 10 additions & 6 deletions src/Aspire.Dashboard/Resources/Columns.resx
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,15 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="StateColumnResourceExitedUnexpectedly" xml:space="preserve">
<value>{0} exited unexpectedly with exit code {1}</value>
<value>{0} exited unexpectedly with exit code {1}.</value>
<comment>{0} is a resource type, {1} is a number</comment>
</data>
<data name="StateColumnResourceExited" xml:space="preserve">
<value>{0} is no longer running</value>
<value>{0} is no longer running.</value>
<comment>{0} is a resource type</comment>
</data>
<data name="StateColumnResourceFailedToStart" xml:space="preserve">
<value>{0} failed to start</value>
<value>{0} failed to start.</value>
<comment>{0} is a resource type</comment>
</data>
<data name="SourceColumnDisplayCopyCommandToClipboard" xml:space="preserve">
Expand Down Expand Up @@ -155,13 +155,17 @@
</data>
<data name="StateColumnResourceContainerRuntimeUnhealthy" xml:space="preserve">
<value>Container runtime was found but appears to be unhealthy. Ensure that it is running.
For more information, see https://aka.ms/aspire/container-runtime-unhealthy</value>
For more information, see https://aka.ms/aspire/container-runtime-unhealthy.</value>
<comment>Contains a new line</comment>
</data>
<data name="StateColumnResourceNotStarted" xml:space="preserve">
<value>Resource has not started because it's configured to not automatically start.</value>
<value>Resource is not configured to start automatically.</value>
</data>
<data name="StateColumnResourceWaiting" xml:space="preserve">
<value>Resource is waiting for other resources to be in a running and healthy state.</value>
<value>Resource is waiting for dependencies.</value>
</data>
<data name="StateColumnResourceWaitingFor" xml:space="preserve">
<value>Waiting for dependencies: {0}.</value>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't put fullstop in messages than end with :

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The period after {0} reads awkwardly because the interpolated value is a list following a colon (e.g., "Waiting for dependencies: nginx, redis."). Consider removing the trailing period: "Waiting for dependencies: {0}".

<comment>{0} is a comma-separated list of dependency resource names.</comment>
</data>
</root>
Loading
Loading