Skip to content

Commit f7a006b

Browse files
authored
Add commands to console logs page (#6819)
1 parent 9aae13a commit f7a006b

23 files changed

+460
-194
lines changed

src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@
88
<PageTitle><ApplicationName ResourceName="@nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsPageTitle)" Loc="@Loc" /></PageTitle>
99

1010
<div class="page-content-container">
11-
<AspirePageContentLayout
12-
AddNewlineOnToolbar="true"
13-
@ref="@_contentLayout"
14-
MainContentStyle="margin-top: 10px;"
15-
MobileToolbarButtonText="@Loc[nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsSelectResourceToolbar)]">
11+
<AspirePageContentLayout AddNewlineOnToolbar="true"
12+
@ref="@_contentLayout"
13+
MainContentStyle="margin-top: 10px;"
14+
MobileToolbarButtonText="@Loc[nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsSelectResourceToolbar)]">
1615
<PageTitleSection>
1716
<h1 class="page-header">@Loc[nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsHeader)]</h1>
1817
</PageTitleSection>
@@ -21,34 +20,42 @@
2120
AriaLabel="@ControlsStringsLoc[nameof(ControlsStrings.ResourceLabel)]"
2221
@bind-SelectedResource="PageViewModel.SelectedOption"
2322
@bind-SelectedResource:after="HandleSelectedOptionChangedAsync" />
24-
@if (ViewportInformation.IsDesktop)
25-
{
26-
// This takes up too much horizontal space on mobile, so show on a new line on mobile
27-
<FluentLabel Typo="Typography.Body" aria-live="polite" aria-label="@Loc[nameof(Dashboard.Resources.ConsoleLogs.LogStatusLabel)]">@PageViewModel.Status</FluentLabel>
28-
}
2923

30-
@{
31-
var menuItems = new List<MenuButtonItem>
32-
{
33-
new()
24+
@foreach (var command in _highlightedCommands)
25+
{
26+
<FluentButton Appearance="Appearance.Lightweight"
27+
Title="@(!string.IsNullOrEmpty(command.DisplayDescription) ? command.DisplayDescription : command.DisplayName)"
28+
Disabled="@(command.State == CommandViewModelState.Disabled)"
29+
OnClick="@(() => ExecuteResourceCommandAsync(command))">
30+
@if (!string.IsNullOrEmpty(command.IconName) && CommandViewModel.ResolveIconName(command.IconName, command.IconVariant) is { } icon)
31+
{
32+
<FluentIcon Value="@icon" Width="16px" />
33+
}
34+
else
3435
{
35-
IsDisabled = PageViewModel.SelectedResource is null,
36-
OnClick = DownloadLogsAsync,
37-
AdditionalAttributes = new Dictionary<string, object>
38-
{
39-
{ "data-action", "download" },
40-
{ "data-resource", PageViewModel.SelectedResource?.Name ?? string.Empty }
41-
},
42-
Text = Loc[nameof(Dashboard.Resources.ConsoleLogs.DownloadLogs)],
43-
Icon = new Icons.Regular.Size16.ArrowDownload()
36+
@command.DisplayName
4437
}
45-
};
38+
</FluentButton>
39+
}
40+
41+
@if (_resourceMenuItems.Count > 0)
42+
{
43+
<AspireMenuButton ButtonAppearance="Appearance.Lightweight"
44+
Icon="@(new Icons.Regular.Size20.MoreHorizontal())"
45+
Items="@_resourceMenuItems"
46+
Title="@Loc[nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsResourceCommands)]" />
47+
}
48+
49+
@if (ViewportInformation.IsDesktop)
50+
{
51+
// This takes up too much horizontal space on mobile, so show on a new line on mobile
52+
<FluentLabel Typo="Typography.Body" aria-live="polite" aria-label="@Loc[nameof(Dashboard.Resources.ConsoleLogs.LogStatusLabel)]" slot="end">@PageViewModel.Status</FluentLabel>
4653
}
4754

4855
<AspireMenuButton ButtonAppearance="Appearance.Lightweight"
49-
Icon="@(new Icons.Regular.Size20.MoreHorizontal())"
50-
Items="@menuItems"
51-
Title="@ControlsStringsLoc[nameof(ControlsStrings.ActionsButtonText)]"
56+
Icon="@(new Icons.Regular.Size20.Settings())"
57+
Items="@_logsMenuItems"
58+
Title="@Loc[nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsSettings)]"
5259
slot="end" />
5360
</ToolbarSection>
5461

src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using Microsoft.AspNetCore.Components;
1919
using Microsoft.Extensions.Localization;
2020
using Microsoft.Extensions.Options;
21+
using Microsoft.FluentUI.AspNetCore.Components;
2122
using Microsoft.JSInterop;
2223

2324
namespace Aspire.Dashboard.Components.Pages;
@@ -63,6 +64,9 @@ private sealed class ConsoleLogsSubscription
6364
[Inject]
6465
public required IJSRuntime JS { get; init; }
6566

67+
[Inject]
68+
public required DashboardCommandExecutor DashboardCommandExecutor { get; init; }
69+
6670
[CascadingParameter]
6771
public required ViewportInformation ViewportInformation { get; init; }
6872

@@ -80,6 +84,9 @@ private sealed class ConsoleLogsSubscription
8084
// UI
8185
private SelectViewModel<ResourceTypeDetails> _noSelection = null!;
8286
private AspirePageContentLayout? _contentLayout;
87+
private readonly List<CommandViewModel> _highlightedCommands = new();
88+
private readonly List<MenuButtonItem> _logsMenuItems = new();
89+
private readonly List<MenuButtonItem> _resourceMenuItems = new();
8390

8491
// State
8592
public ConsoleLogsViewModel PageViewModel { get; set; } = null!;
@@ -166,6 +173,7 @@ async Task TrackResourceSnapshotsAsync()
166173
}
167174
}
168175

176+
UpdateMenuButtons();
169177
await InvokeAsync(StateHasChanged);
170178
}
171179
});
@@ -191,6 +199,8 @@ protected override async Task OnParametersSetAsync()
191199
return;
192200
}
193201

202+
UpdateMenuButtons();
203+
194204
var selectedResourceName = PageViewModel.SelectedResource?.Name;
195205
if (!string.Equals(selectedResourceName, _consoleLogsSubscription?.Name, StringComparisons.ResourceName))
196206
{
@@ -234,6 +244,54 @@ protected override async Task OnParametersSetAsync()
234244
}
235245
}
236246

247+
private void UpdateMenuButtons()
248+
{
249+
_highlightedCommands.Clear();
250+
_logsMenuItems.Clear();
251+
_resourceMenuItems.Clear();
252+
253+
_logsMenuItems.Add(new()
254+
{
255+
IsDisabled = PageViewModel.SelectedResource is null,
256+
OnClick = DownloadLogsAsync,
257+
Text = Loc[nameof(Dashboard.Resources.ConsoleLogs.DownloadLogs)],
258+
Icon = new Icons.Regular.Size16.ArrowDownload()
259+
});
260+
261+
if (PageViewModel.SelectedResource != null)
262+
{
263+
if (ViewportInformation.IsDesktop)
264+
{
265+
_highlightedCommands.AddRange(PageViewModel.SelectedResource.Commands.Where(c => c.IsHighlighted && c.State != CommandViewModelState.Hidden).Take(DashboardUIHelpers.MaxHighlightedCommands));
266+
}
267+
268+
var menuCommands = PageViewModel.SelectedResource.Commands.Where(c => !_highlightedCommands.Contains(c) && c.State != CommandViewModelState.Hidden).ToList();
269+
if (menuCommands.Count > 0)
270+
{
271+
foreach (var command in menuCommands)
272+
{
273+
var icon = (!string.IsNullOrEmpty(command.IconName) && CommandViewModel.ResolveIconName(command.IconName, command.IconVariant) is { } i) ? i : null;
274+
275+
_resourceMenuItems.Add(new MenuButtonItem
276+
{
277+
Text = command.DisplayName,
278+
Tooltip = command.DisplayDescription,
279+
Icon = icon,
280+
OnClick = () => ExecuteResourceCommandAsync(command),
281+
IsDisabled = command.State == CommandViewModelState.Disabled
282+
});
283+
}
284+
}
285+
}
286+
}
287+
288+
private async Task ExecuteResourceCommandAsync(CommandViewModel command)
289+
{
290+
await DashboardCommandExecutor.ExecuteAsync(PageViewModel.SelectedResource!, command, GetResourceName);
291+
}
292+
293+
private string GetResourceName(ResourceViewModel resource) => ResourceViewModel.GetResourceName(resource, _resourceByName);
294+
237295
internal static ImmutableList<SelectViewModel<ResourceTypeDetails>> GetConsoleLogResourceSelectViewModels(
238296
ConcurrentDictionary<string, ResourceViewModel> resourcesByName,
239297
SelectViewModel<ResourceTypeDetails> noSelectionViewModel,

src/Aspire.Dashboard/Components/Pages/Resources.razor.cs

Lines changed: 3 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,7 @@ public partial class Resources : ComponentBase, IAsyncDisposable
3636
[Inject]
3737
public required NavigationManager NavigationManager { get; init; }
3838
[Inject]
39-
public required IDialogService DialogService { get; init; }
40-
[Inject]
41-
public required IToastService ToastService { get; init; }
39+
public required DashboardCommandExecutor DashboardCommandExecutor { get; init; }
4240
[Inject]
4341
public required BrowserTimeProvider TimeProvider { get; init; }
4442
[Inject]
@@ -283,7 +281,7 @@ private void UpdateMaxHighlightedCount()
283281

284282
// Don't attempt to display more than 2 highlighted commands. Many commands will take up too much space.
285283
// Extra highlighted commands are still available in the menu.
286-
_maxHighlightedCount = Math.Min(maxHighlightedCount, 2);
284+
_maxHighlightedCount = Math.Min(maxHighlightedCount, DashboardUIHelpers.MaxHighlightedCommands);
287285
}
288286

289287
protected override async Task OnParametersSetAsync()
@@ -394,68 +392,7 @@ private string GetRowClass(ResourceViewModel resource)
394392

395393
private async Task ExecuteResourceCommandAsync(ResourceViewModel resource, CommandViewModel command)
396394
{
397-
if (!string.IsNullOrWhiteSpace(command.ConfirmationMessage))
398-
{
399-
var dialogReference = await DialogService.ShowConfirmationAsync(command.ConfirmationMessage);
400-
var result = await dialogReference.Result;
401-
if (result.Cancelled)
402-
{
403-
return;
404-
}
405-
}
406-
407-
var messageResourceName = GetResourceName(resource);
408-
409-
var toastParameters = new ToastParameters<CommunicationToastContent>()
410-
{
411-
Id = Guid.NewGuid().ToString(),
412-
Intent = ToastIntent.Progress,
413-
Title = string.Format(CultureInfo.InvariantCulture, Loc[nameof(Dashboard.Resources.Resources.ResourceCommandStarting)], messageResourceName, command.DisplayName),
414-
Content = new CommunicationToastContent()
415-
};
416-
417-
// Show a toast immediately to indicate the command is starting.
418-
ToastService.ShowCommunicationToast(toastParameters);
419-
420-
var response = await DashboardClient.ExecuteResourceCommandAsync(resource.Name, resource.ResourceType, command, CancellationToken.None);
421-
422-
// Update toast with the result;
423-
if (response.Kind == ResourceCommandResponseKind.Succeeded)
424-
{
425-
toastParameters.Title = string.Format(CultureInfo.InvariantCulture, Loc[nameof(Dashboard.Resources.Resources.ResourceCommandSuccess)], messageResourceName, command.DisplayName);
426-
toastParameters.Intent = ToastIntent.Success;
427-
toastParameters.Icon = GetIntentIcon(ToastIntent.Success);
428-
}
429-
else
430-
{
431-
toastParameters.Title = string.Format(CultureInfo.InvariantCulture, Loc[nameof(Dashboard.Resources.Resources.ResourceCommandFailed)], messageResourceName, command.DisplayName);
432-
toastParameters.Intent = ToastIntent.Error;
433-
toastParameters.Icon = GetIntentIcon(ToastIntent.Error);
434-
toastParameters.Content.Details = response.ErrorMessage;
435-
toastParameters.PrimaryAction = Loc[nameof(Dashboard.Resources.Resources.ResourceCommandToastViewLogs)];
436-
toastParameters.OnPrimaryAction = EventCallback.Factory.Create<ToastResult>(this, () => NavigationManager.NavigateTo(DashboardUrls.ConsoleLogsUrl(resource: resource.Name)));
437-
}
438-
439-
ToastService.UpdateToast(toastParameters.Id, toastParameters);
440-
}
441-
442-
// Copied from FluentUI.
443-
private static (Icon Icon, Color Color)? GetIntentIcon(ToastIntent intent)
444-
{
445-
return intent switch
446-
{
447-
ToastIntent.Success => (new Icons.Filled.Size24.CheckmarkCircle(), Color.Success),
448-
ToastIntent.Warning => (new Icons.Filled.Size24.Warning(), Color.Warning),
449-
ToastIntent.Error => (new Icons.Filled.Size24.DismissCircle(), Color.Error),
450-
ToastIntent.Info => (new Icons.Filled.Size24.Info(), Color.Info),
451-
ToastIntent.Progress => (new Icons.Regular.Size24.Flash(), Color.Neutral),
452-
ToastIntent.Upload => (new Icons.Regular.Size24.ArrowUpload(), Color.Neutral),
453-
ToastIntent.Download => (new Icons.Regular.Size24.ArrowDownload(), Color.Neutral),
454-
ToastIntent.Event => (new Icons.Regular.Size24.CalendarLtr(), Color.Neutral),
455-
ToastIntent.Mention => (new Icons.Regular.Size24.Person(), Color.Neutral),
456-
ToastIntent.Custom => null,
457-
_ => throw new InvalidOperationException()
458-
};
395+
await DashboardCommandExecutor.ExecuteAsync(resource, command, GetResourceName);
459396
}
460397

461398
private static string GetEndpointsTooltip(ResourceViewModel resource)

src/Aspire.Dashboard/DashboardWebApplication.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ public DashboardWebApplication(
233233
// Data from the server.
234234
builder.Services.TryAddScoped<IDashboardClient, DashboardClient>();
235235
builder.Services.TryAddSingleton<IDashboardClientStatus, DashboardClientStatus>();
236+
builder.Services.TryAddScoped<DashboardCommandExecutor>();
236237

237238
// OTLP services.
238239
builder.Services.AddGrpc();
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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.Globalization;
5+
using Aspire.Dashboard.Utils;
6+
using Microsoft.AspNetCore.Components;
7+
using Microsoft.Extensions.Localization;
8+
using Microsoft.FluentUI.AspNetCore.Components;
9+
10+
namespace Aspire.Dashboard.Model;
11+
12+
public sealed class DashboardCommandExecutor(
13+
IDashboardClient dashboardClient,
14+
IDialogService dialogService,
15+
IToastService toastService,
16+
IStringLocalizer<Dashboard.Resources.Resources> loc,
17+
NavigationManager navigationManager)
18+
{
19+
public async Task ExecuteAsync(ResourceViewModel resource, CommandViewModel command, Func<ResourceViewModel, string> getResourceName)
20+
{
21+
if (!string.IsNullOrWhiteSpace(command.ConfirmationMessage))
22+
{
23+
var dialogReference = await dialogService.ShowConfirmationAsync(command.ConfirmationMessage).ConfigureAwait(false);
24+
var result = await dialogReference.Result.ConfigureAwait(false);
25+
if (result.Cancelled)
26+
{
27+
return;
28+
}
29+
}
30+
31+
var messageResourceName = getResourceName(resource);
32+
33+
var toastParameters = new ToastParameters<CommunicationToastContent>()
34+
{
35+
Id = Guid.NewGuid().ToString(),
36+
Intent = ToastIntent.Progress,
37+
Title = string.Format(CultureInfo.InvariantCulture, loc[nameof(Dashboard.Resources.Resources.ResourceCommandStarting)], messageResourceName, command.DisplayName),
38+
Content = new CommunicationToastContent()
39+
};
40+
41+
// Show a toast immediately to indicate the command is starting.
42+
toastService.ShowCommunicationToast(toastParameters);
43+
44+
var response = await dashboardClient.ExecuteResourceCommandAsync(resource.Name, resource.ResourceType, command, CancellationToken.None).ConfigureAwait(false);
45+
46+
// Update toast with the result;
47+
if (response.Kind == ResourceCommandResponseKind.Succeeded)
48+
{
49+
toastParameters.Title = string.Format(CultureInfo.InvariantCulture, loc[nameof(Dashboard.Resources.Resources.ResourceCommandSuccess)], messageResourceName, command.DisplayName);
50+
toastParameters.Intent = ToastIntent.Success;
51+
toastParameters.Icon = GetIntentIcon(ToastIntent.Success);
52+
}
53+
else
54+
{
55+
toastParameters.Title = string.Format(CultureInfo.InvariantCulture, loc[nameof(Dashboard.Resources.Resources.ResourceCommandFailed)], messageResourceName, command.DisplayName);
56+
toastParameters.Intent = ToastIntent.Error;
57+
toastParameters.Icon = GetIntentIcon(ToastIntent.Error);
58+
toastParameters.Content.Details = response.ErrorMessage;
59+
toastParameters.PrimaryAction = loc[nameof(Dashboard.Resources.Resources.ResourceCommandToastViewLogs)];
60+
toastParameters.OnPrimaryAction = EventCallback.Factory.Create<ToastResult>(this, () => navigationManager.NavigateTo(DashboardUrls.ConsoleLogsUrl(resource: resource.Name)));
61+
}
62+
63+
toastService.UpdateToast(toastParameters.Id, toastParameters);
64+
}
65+
66+
// Copied from FluentUI.
67+
private static (Icon Icon, Color Color)? GetIntentIcon(ToastIntent intent)
68+
{
69+
return intent switch
70+
{
71+
ToastIntent.Success => (new Icons.Filled.Size24.CheckmarkCircle(), Color.Success),
72+
ToastIntent.Warning => (new Icons.Filled.Size24.Warning(), Color.Warning),
73+
ToastIntent.Error => (new Icons.Filled.Size24.DismissCircle(), Color.Error),
74+
ToastIntent.Info => (new Icons.Filled.Size24.Info(), Color.Info),
75+
ToastIntent.Progress => (new Icons.Regular.Size24.Flash(), Color.Neutral),
76+
ToastIntent.Upload => (new Icons.Regular.Size24.ArrowUpload(), Color.Neutral),
77+
ToastIntent.Download => (new Icons.Regular.Size24.ArrowDownload(), Color.Neutral),
78+
ToastIntent.Event => (new Icons.Regular.Size24.CalendarLtr(), Color.Neutral),
79+
ToastIntent.Mention => (new Icons.Regular.Size24.Person(), Color.Neutral),
80+
ToastIntent.Custom => null,
81+
_ => throw new InvalidOperationException()
82+
};
83+
}
84+
}

0 commit comments

Comments
 (0)