Skip to content

Commit 0ad8aa0

Browse files
committed
Rebase
1 parent d10b79e commit 0ad8aa0

25 files changed

+448
-87
lines changed

src/Aspire.Dashboard/Components/Controls/AspireMenuButton.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
}
2525
else
2626
{
27-
<FluentMenuItem OnClick="() => HandleItemClicked(item)" title="@item.Tooltip">
27+
<FluentMenuItem OnClick="() => HandleItemClicked(item)" title="@item.Tooltip" Disabled="@item.IsDisabled">
2828
@item.Text
2929
@if (item.Icon != null)
3030
{

src/Aspire.Dashboard/Components/Controls/ResourceActions.razor

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
@using Aspire.Dashboard.Model
33
@using Microsoft.FluentUI.AspNetCore.Components
44

5-
@foreach (var highlightedCommand in Commands.Where(c => c.IsHighlighted))
5+
@foreach (var highlightedCommand in Commands.Where(c => c.IsHighlighted && c.State != CommandViewModelState.Hidden))
66
{
7-
<FluentButton Appearance="Appearance.Lightweight" Title="@(highlightedCommand.DisplayDescription ?? highlightedCommand.DisplayName)" OnClick="@(() => CommandSelected.InvokeAsync(highlightedCommand))">
7+
<FluentButton Appearance="Appearance.Lightweight" Title="@(highlightedCommand.DisplayDescription ?? highlightedCommand.DisplayName)" OnClick="@(() => CommandSelected.InvokeAsync(highlightedCommand))" Disabled="@(highlightedCommand.State == CommandViewModelState.Disabled)">
88
@if (!string.IsNullOrEmpty(highlightedCommand.IconName) && CommandViewModel.ResolveIconName(highlightedCommand.IconName) is { } icon)
99
{
10-
<FluentIcon Value="icon" />
10+
<FluentIcon Value="@icon" />
1111
}
1212
else
1313
{

src/Aspire.Dashboard/Components/Controls/ResourceActions.razor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ protected override void OnParametersSet()
4949
OnClick = OnConsoleLogs.InvokeAsync
5050
});
5151

52-
var menuCommands = Commands.Where(c => !c.IsHighlighted).ToList();
52+
var menuCommands = Commands.Where(c => !c.IsHighlighted && c.State != CommandViewModelState.Hidden).ToList();
5353
if (menuCommands.Count > 0)
5454
{
5555
_menuItems.Add(new MenuButtonItem { IsDivider = true });

src/Aspire.Dashboard/Model/MenuButtonItem.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ public class MenuButtonItem
1212
public string? Tooltip { get; set; }
1313
public Icon? Icon { get; set; }
1414
public Func<Task>? OnClick { get; set; }
15+
public bool IsDisabled { get; set; }
1516
}

src/Aspire.Dashboard/Model/ResourceViewModel.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,21 @@ public sealed class CommandViewModel
6565
private static readonly ConcurrentDictionary<string, CustomIcon?> s_iconCache = new();
6666

6767
public string CommandType { get; }
68+
public CommandViewModelState State { get; }
6869
public string DisplayName { get; }
6970
public string? DisplayDescription { get; }
7071
public string? ConfirmationMessage { get; }
7172
public Value? Parameter { get; }
7273
public bool IsHighlighted { get; }
7374
public string? IconName { get; }
7475

75-
public CommandViewModel(string commandType, string displayName, string? displayDescription, string? confirmationMessage, Value? parameter, bool isHighlighted, string? iconName)
76+
public CommandViewModel(string commandType, CommandViewModelState state, string displayName, string? displayDescription, string? confirmationMessage, Value? parameter, bool isHighlighted, string? iconName)
7677
{
7778
ArgumentException.ThrowIfNullOrWhiteSpace(commandType);
7879
ArgumentException.ThrowIfNullOrWhiteSpace(displayName);
7980

8081
CommandType = commandType;
82+
State = state;
8183
DisplayName = displayName;
8284
DisplayDescription = displayDescription;
8385
ConfirmationMessage = confirmationMessage;
@@ -109,6 +111,13 @@ public CommandViewModel(string commandType, string displayName, string? displayD
109111
}
110112
}
111113

114+
public enum CommandViewModelState
115+
{
116+
Enabled,
117+
Disabled,
118+
Hidden
119+
}
120+
112121
[DebuggerDisplay("Name = {Name}, Value = {Value}, FromSpec = {FromSpec}, IsValueMasked = {IsValueMasked}")]
113122
public sealed class EnvironmentVariableViewModel
114123
{

src/Aspire.Dashboard/ResourceService/Partials.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,18 @@ ImmutableArray<UrlViewModel> GetUrls()
5858
ImmutableArray<CommandViewModel> GetCommands()
5959
{
6060
return Commands
61-
.Select(c => new CommandViewModel(c.CommandType, c.DisplayName, c.HasDisplayDescription ? c.DisplayDescription : null, c.ConfirmationMessage, c.Parameter, c.IsHighlighted, c.HasIconName ? c.IconName : null))
61+
.Select(c => new CommandViewModel(c.CommandType, Map(c.State), c.DisplayName, c.HasDisplayDescription ? c.DisplayDescription : null, c.ConfirmationMessage, c.Parameter, c.IsHighlighted, c.HasIconName ? c.IconName : null))
6262
.ToImmutableArray();
63+
static CommandViewModelState Map(ResourceCommandState state)
64+
{
65+
return state switch
66+
{
67+
ResourceCommandState.Enabled => CommandViewModelState.Enabled,
68+
ResourceCommandState.Disabled => CommandViewModelState.Disabled,
69+
ResourceCommandState.Hidden => CommandViewModelState.Hidden,
70+
_ => throw new InvalidOperationException("Unknown state: " + state),
71+
};
72+
}
6373
}
6474

6575
T ValidateNotNull<T>(T value, [CallerArgumentExpression(nameof(value))] string? expression = null) where T : class

src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ public sealed record CustomResourceSnapshot
5050
/// The URLs that should show up in the dashboard for this resource.
5151
/// </summary>
5252
public ImmutableArray<UrlSnapshot> Urls { get; init; } = [];
53+
54+
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
55+
#pragma warning disable RS0016 // Add public types and members to the declared API
56+
public ImmutableArray<ResourceCommandSnapshot> Commands { get; init; } = [];
57+
#pragma warning restore RS0016 // Add public types and members to the declared API
58+
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
5359
}
5460

5561
/// <summary>
@@ -90,6 +96,19 @@ public sealed record UrlSnapshot(string Name, string Url, bool IsInternal);
9096
/// <param name="Value">The value of the property.</param>
9197
public sealed record ResourcePropertySnapshot(string Name, object? Value);
9298

99+
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
100+
#pragma warning disable RS0016 // Add public types and members to the declared API
101+
public sealed record ResourceCommandSnapshot(string Type, ResourceCommandState State, string DisplayName, string? IconName, bool IsHighlighted);
102+
103+
public enum ResourceCommandState
104+
{
105+
Enabled,
106+
Disabled,
107+
Hidden
108+
}
109+
#pragma warning restore RS0016 // Add public types and members to the declared API
110+
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
111+
93112
/// <summary>
94113
/// The set of well known resource states.
95114
/// </summary>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
6+
namespace Aspire.Hosting.ApplicationModel;
7+
8+
#pragma warning disable RS0016 // Add public types and members to the declared API
9+
/// <summary>
10+
/// Represents a command annotation for a resource.
11+
/// </summary>
12+
[DebuggerDisplay("Type = {GetType().Name,nq}, Type = {Type}")]
13+
public sealed class ResourceCommandAnnotation : IResourceAnnotation
14+
{
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="ResourceCommandAnnotation"/> class.
17+
/// </summary>
18+
public ResourceCommandAnnotation(
19+
string type,
20+
string displayName,
21+
Func<UpdateCommandStateContext, ResourceCommandState> updateState,
22+
Func<ExecuteCommandContext, Task> executeCommand,
23+
string? iconContent,
24+
bool isHighlighted)
25+
{
26+
ArgumentNullException.ThrowIfNull(type);
27+
ArgumentNullException.ThrowIfNull(displayName);
28+
29+
Type = type;
30+
DisplayName = displayName;
31+
UpdateState = updateState;
32+
ExecuteCommand = executeCommand;
33+
IconContent = iconContent;
34+
IsHighlighted = isHighlighted;
35+
}
36+
37+
/// <summary>
38+
///
39+
/// </summary>
40+
public string Type { get; }
41+
42+
/// <summary>
43+
///
44+
/// </summary>
45+
public string DisplayName { get; }
46+
47+
/// <summary>
48+
///
49+
/// </summary>
50+
public Func<UpdateCommandStateContext, ResourceCommandState> UpdateState { get; }
51+
52+
/// <summary>
53+
///
54+
/// </summary>
55+
public Func<ExecuteCommandContext, Task> ExecuteCommand { get; }
56+
57+
/// <summary>
58+
///
59+
/// </summary>
60+
public string? IconContent { get; }
61+
62+
/// <summary>
63+
///
64+
/// </summary>
65+
public bool IsHighlighted { get; }
66+
}
67+
68+
/// <summary>
69+
///
70+
/// </summary>
71+
public class UpdateCommandStateContext
72+
{
73+
/// <summary>
74+
///
75+
/// </summary>
76+
public required CustomResourceSnapshot ResourceSnapshot { get; init; }
77+
}
78+
79+
/// <summary>
80+
///
81+
/// </summary>
82+
public class ExecuteCommandContext
83+
{
84+
/// <summary>
85+
///
86+
/// </summary>
87+
public required IServiceProvider ServiceProvider { get; init; }
88+
89+
/// <summary>
90+
///
91+
/// </summary>
92+
public required string ResourceName { get; init; }
93+
94+
/// <summary>
95+
///
96+
/// </summary>
97+
public required CancellationToken CancellationToken { get; init; }
98+
}
99+
#pragma warning restore RS0016 // Add public types and members to the declared API

src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Concurrent;
5+
using System.Collections.Immutable;
56
using System.Runtime.CompilerServices;
67
using System.Threading.Channels;
78
using Microsoft.Extensions.Hosting;
@@ -187,6 +188,10 @@ public Task PublishUpdateAsync(IResource resource, string resourceId, Func<Custo
187188
{
188189
var previousState = GetCurrentSnapshot(resource, notificationState);
189190

191+
var commands = BuildCommands(resource, previousState);
192+
193+
previousState = previousState with { Commands = commands };
194+
190195
var newState = stateFactory(previousState);
191196

192197
notificationState.LastSnapshot = newState;
@@ -219,9 +224,57 @@ public Task PublishUpdateAsync(IResource resource, string resourceId, Func<Custo
219224
newState.ExitCode, string.Join(", ", newState.EnvironmentVariables.Select(e => $"{e.Name} = {e.Value}")), string.Join(", ", newState.Urls.Select(u => $"{u.Name} = {u.Url}")),
220225
string.Join(", ", newState.Properties.Select(p => $"{p.Name} = {p.Value}")));
221226
}
227+
}
228+
229+
return Task.CompletedTask;
230+
}
231+
232+
private static ImmutableArray<ResourceCommandSnapshot> BuildCommands(IResource resource, CustomResourceSnapshot previousState)
233+
{
234+
var commandsBuilder = ImmutableArray.CreateBuilder<ResourceCommandSnapshot>();
235+
var commandAnnotations = resource.Annotations.OfType<ResourceCommandAnnotation>().ToList();
236+
237+
// Left outer join of command snapshots and annotations.
238+
var query =
239+
from command in previousState.Commands
240+
join commandAnnotation in commandAnnotations on command.Type equals commandAnnotation.Type into gj
241+
from subgroup in gj.DefaultIfEmpty()
242+
select new
243+
{
244+
Command = command,
245+
Annotation = subgroup
246+
};
247+
248+
foreach (var v in query)
249+
{
250+
var command = v.Command;
251+
if (v.Annotation != null)
252+
{
253+
var newState = v.Annotation.UpdateState(new UpdateCommandStateContext { ResourceSnapshot = previousState });
254+
255+
if (command.State != newState)
256+
{
257+
command = command with
258+
{
259+
State = newState
260+
};
261+
}
262+
263+
commandAnnotations.Remove(v.Annotation);
264+
}
222265

223-
return Task.CompletedTask;
266+
commandsBuilder.Add(command);
224267
}
268+
269+
// Add any remaining annotations as command snapshots.
270+
foreach (var commandAnnotation in commandAnnotations)
271+
{
272+
var state = commandAnnotation.UpdateState(new UpdateCommandStateContext { ResourceSnapshot = previousState });
273+
274+
commandsBuilder.Add(new ResourceCommandSnapshot(commandAnnotation.Type, state, commandAnnotation.DisplayName, commandAnnotation.IconContent, commandAnnotation.IsHighlighted));
275+
}
276+
277+
return commandsBuilder.ToImmutable();
225278
}
226279

227280
/// <summary>

src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Diagnostics.CodeAnalysis;
55
using Aspire.Hosting.ApplicationModel;
6+
using Aspire.Hosting.Dashboard;
67
using Aspire.Hosting.Utils;
78

89
namespace Aspire.Hosting;
@@ -151,6 +152,7 @@ public static IResourceBuilder<T> WithImage<T>(this IResourceBuilder<T> builder,
151152
// if the annotation doesn't exist, create it with the given image and add it to the collection
152153
var containerImageAnnotation = new ContainerImageAnnotation() { Image = image, Tag = tag };
153154
builder.Resource.Annotations.Add(containerImageAnnotation);
155+
builder.WithLifeCycleCommands();
154156
return builder;
155157
}
156158

0 commit comments

Comments
 (0)