From 999086f125879d445f08fdee78aca82b7c118135 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 10 Apr 2025 15:58:13 +0800 Subject: [PATCH 1/7] Add context menu to resource graph, improve console logs actions --- .../Components/Controls/AspireMenu.razor | 32 +++++ .../Components/Controls/AspireMenu.razor.cs | 65 ++++++++++ .../Controls/AspireMenuButton.razor | 27 +--- .../Controls/AspireMenuButton.razor.cs | 9 -- .../Components/Controls/ResourceActions.razor | 2 +- .../Controls/ResourceActions.razor.cs | 116 +++++++++++------- .../Components/Pages/ConsoleLogs.razor | 4 +- .../Components/Pages/ConsoleLogs.razor.cs | 39 +++--- .../Components/Pages/Resources.razor | 8 +- .../Components/Pages/Resources.razor.cs | 58 +++++++++ .../Resources/ConsoleLogs.Designer.cs | 6 +- .../Resources/ConsoleLogs.resx | 4 +- .../Resources/xlf/ConsoleLogs.cs.xlf | 6 +- .../Resources/xlf/ConsoleLogs.de.xlf | 6 +- .../Resources/xlf/ConsoleLogs.es.xlf | 6 +- .../Resources/xlf/ConsoleLogs.fr.xlf | 6 +- .../Resources/xlf/ConsoleLogs.it.xlf | 6 +- .../Resources/xlf/ConsoleLogs.ja.xlf | 6 +- .../Resources/xlf/ConsoleLogs.ko.xlf | 6 +- .../Resources/xlf/ConsoleLogs.pl.xlf | 6 +- .../Resources/xlf/ConsoleLogs.pt-BR.xlf | 6 +- .../Resources/xlf/ConsoleLogs.ru.xlf | 6 +- .../Resources/xlf/ConsoleLogs.tr.xlf | 6 +- .../Resources/xlf/ConsoleLogs.zh-Hans.xlf | 6 +- .../Resources/xlf/ConsoleLogs.zh-Hant.xlf | 6 +- .../wwwroot/js/app-resourcegraph.js | 33 ++++- 26 files changed, 332 insertions(+), 149 deletions(-) create mode 100644 src/Aspire.Dashboard/Components/Controls/AspireMenu.razor create mode 100644 src/Aspire.Dashboard/Components/Controls/AspireMenu.razor.cs diff --git a/src/Aspire.Dashboard/Components/Controls/AspireMenu.razor b/src/Aspire.Dashboard/Components/Controls/AspireMenu.razor new file mode 100644 index 00000000000..c1e0fe72210 --- /dev/null +++ b/src/Aspire.Dashboard/Components/Controls/AspireMenu.razor @@ -0,0 +1,32 @@ +@namespace Aspire.Dashboard.Components +@using System.Collections.Immutable +@using Aspire.Dashboard.Model +@using Microsoft.FluentUI.AspNetCore.Components.DesignTokens +@inherits FluentComponentBase + + + @foreach (var item in Items) + { + @if (item.IsDivider) + { + + } + else + { + var additionalMenuItemAttributes = new Dictionary(item.AdditionalAttributes ?? ImmutableDictionary.Empty) + { + { "title", item.Tooltip ?? item.Text ?? string.Empty } + }; + + + @item.Text + @if (item.Icon != null) + { + + + + } + + } + } + diff --git a/src/Aspire.Dashboard/Components/Controls/AspireMenu.razor.cs b/src/Aspire.Dashboard/Components/Controls/AspireMenu.razor.cs new file mode 100644 index 00000000000..36dd9e07202 --- /dev/null +++ b/src/Aspire.Dashboard/Components/Controls/AspireMenu.razor.cs @@ -0,0 +1,65 @@ +// 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 Microsoft.AspNetCore.Components; +using Microsoft.FluentUI.AspNetCore.Components; + +namespace Aspire.Dashboard.Components; + +public partial class AspireMenu : FluentComponentBase +{ + private FluentMenu? _menu; + + [Parameter] + public string? Anchor { get; set; } + + [Parameter] + public bool Open { get; set; } + + [Parameter] + public bool Anchored { get; set; } = true; + + /// + /// Raised when the property changed. + /// + [Parameter] + public EventCallback OpenChanged { get; set; } + + [Parameter] + public required IList Items { get; set; } + + public async Task CloseAsync() + { + if (_menu is { } menu) + { + await menu.CloseAsync(); + } + } + + public async Task OpenAsync(int clientX, int clientY) + { + if (_menu is { } menu) + { + await menu.OpenAsync(clientX, clientY); + } + } + + private async Task HandleItemClicked(MenuButtonItem item) + { + if (item.OnClick is {} onClick) + { + await onClick(); + } + Open = false; + } + + private Task OnOpenChanged(bool open) + { + Open = open; + + return OpenChanged.HasDelegate + ? OpenChanged.InvokeAsync(open) + : Task.CompletedTask; + } +} diff --git a/src/Aspire.Dashboard/Components/Controls/AspireMenuButton.razor b/src/Aspire.Dashboard/Components/Controls/AspireMenuButton.razor index b60f7831436..594008ef9ce 100644 --- a/src/Aspire.Dashboard/Components/Controls/AspireMenuButton.razor +++ b/src/Aspire.Dashboard/Components/Controls/AspireMenuButton.razor @@ -29,29 +29,4 @@ } - - @foreach (var item in Items) - { - @if (item.IsDivider) - { - - } - else - { - var additionalMenuItemAttributes = new Dictionary(item.AdditionalAttributes ?? ImmutableDictionary.Empty) - { - { "title", item.Tooltip ?? item.Text ?? string.Empty } - }; - - - @item.Text - @if (item.Icon != null) - { - - - - } - - } - } - + diff --git a/src/Aspire.Dashboard/Components/Controls/AspireMenuButton.razor.cs b/src/Aspire.Dashboard/Components/Controls/AspireMenuButton.razor.cs index 54c063c8467..51b918de80f 100644 --- a/src/Aspire.Dashboard/Components/Controls/AspireMenuButton.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/AspireMenuButton.razor.cs @@ -56,15 +56,6 @@ private void ToggleMenu() _visible = !_visible; } - private async Task HandleItemClicked(MenuButtonItem item) - { - if (item.OnClick is {} onClick) - { - await onClick(); - } - _visible = false; - } - private void OnKeyDown(KeyboardEventArgs args) { if (args is not null && args.Key == "Escape") diff --git a/src/Aspire.Dashboard/Components/Controls/ResourceActions.razor b/src/Aspire.Dashboard/Components/Controls/ResourceActions.razor index fef77d570bc..95fda3262ac 100644 --- a/src/Aspire.Dashboard/Components/Controls/ResourceActions.razor +++ b/src/Aspire.Dashboard/Components/Controls/ResourceActions.razor @@ -12,7 +12,7 @@ { var highlightedCommand = _highlightedCommands[i]; - + @if (!string.IsNullOrEmpty(highlightedCommand.IconName) && IconResolver.ResolveIconName(highlightedCommand.IconName, IconSize.Size16, highlightedCommand.IconVariant) is { } icon) { diff --git a/src/Aspire.Dashboard/Components/Controls/ResourceActions.razor.cs b/src/Aspire.Dashboard/Components/Controls/ResourceActions.razor.cs index 13f514a8d63..47ba833337d 100644 --- a/src/Aspire.Dashboard/Components/Controls/ResourceActions.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/ResourceActions.razor.cs @@ -34,16 +34,13 @@ public partial class ResourceActions : ComponentBase public required TelemetryRepository TelemetryRepository { get; set; } [Parameter] - public required IList Commands { get; set; } - - [Parameter] - public required EventCallback CommandSelected { get; set; } + public required Func CommandSelected { get; set; } [Parameter] public required Func IsCommandExecuting { get; set; } [Parameter] - public required EventCallback OnViewDetails { get; set; } + public required Func OnViewDetails { get; set; } [Parameter] public required ResourceViewModel Resource { get; set; } @@ -65,86 +62,121 @@ protected override void OnParametersSet() _menuItems.Clear(); _highlightedCommands.Clear(); - _menuItems.Add(new MenuButtonItem + AddMenuItems( + _menuItems, + _menuButton?.MenuButtonId, + Resource, + NavigationManager, + TelemetryRepository, + GetResourceName, + ControlLoc, + Loc, + OnViewDetails, + CommandSelected, + IsCommandExecuting, + showConsoleLogsItem: true); + + // If display is desktop then we display highlighted commands next to the ... button. + if (ViewportInformation.IsDesktop) + { + _highlightedCommands.AddRange(Resource.Commands.Where(c => c.IsHighlighted && c.State != CommandViewModelState.Hidden).Take(MaxHighlightedCount)); + } + } + + public static void AddMenuItems( + List menuItems, + string? openingMenuButtonId, + ResourceViewModel resource, + NavigationManager navigationManager, + TelemetryRepository telemetryRepository, + Func getResourceName, + IStringLocalizer controlLoc, + IStringLocalizer loc, + Func onViewDetails, + Func commandSelected, + Func isCommandExecuting, + bool showConsoleLogsItem) + { + menuItems.Add(new MenuButtonItem { - Text = ControlLoc[nameof(Resources.ControlsStrings.ActionViewDetailsText)], + Text = controlLoc[nameof(Resources.ControlsStrings.ActionViewDetailsText)], Icon = s_viewDetailsIcon, - OnClick = () => OnViewDetails.InvokeAsync(_menuButton?.MenuButtonId) + OnClick = () => onViewDetails(openingMenuButtonId) }); - - _menuItems.Add(new MenuButtonItem + + if (showConsoleLogsItem) { - Text = Loc[nameof(Resources.Resources.ResourceActionConsoleLogsText)], - Icon = s_consoleLogsIcon, - OnClick = () => + menuItems.Add(new MenuButtonItem { - NavigationManager.NavigateTo(DashboardUrls.ConsoleLogsUrl(resource: Resource.Name)); - return Task.CompletedTask; - } - }); + Text = loc[nameof(Resources.Resources.ResourceActionConsoleLogsText)], + Icon = s_consoleLogsIcon, + OnClick = () => + { + navigationManager.NavigateTo(DashboardUrls.ConsoleLogsUrl(resource: resource.Name)); + return Task.CompletedTask; + } + }); + } // Show telemetry menu items if there is telemetry for the resource. - var hasTelemetryApplication = TelemetryRepository.GetApplicationByCompositeName(Resource.Name) != null; + var hasTelemetryApplication = telemetryRepository.GetApplicationByCompositeName(resource.Name) != null; if (hasTelemetryApplication) { - _menuItems.Add(new MenuButtonItem { IsDivider = true }); - _menuItems.Add(new MenuButtonItem + menuItems.Add(new MenuButtonItem { IsDivider = true }); + menuItems.Add(new MenuButtonItem { - Text = Loc[nameof(Resources.Resources.ResourceActionStructuredLogsText)], - Tooltip = Loc[nameof(Resources.Resources.ResourceActionStructuredLogsText)], + Text = loc[nameof(Resources.Resources.ResourceActionStructuredLogsText)], + Tooltip = loc[nameof(Resources.Resources.ResourceActionStructuredLogsText)], Icon = s_structuredLogsIcon, OnClick = () => { - NavigationManager.NavigateTo(DashboardUrls.StructuredLogsUrl(resource: GetResourceName(Resource))); + navigationManager.NavigateTo(DashboardUrls.StructuredLogsUrl(resource: getResourceName(resource))); return Task.CompletedTask; } }); - _menuItems.Add(new MenuButtonItem + menuItems.Add(new MenuButtonItem { - Text = Loc[nameof(Resources.Resources.ResourceActionTracesText)], - Tooltip = Loc[nameof(Resources.Resources.ResourceActionTracesText)], + Text = loc[nameof(Resources.Resources.ResourceActionTracesText)], + Tooltip = loc[nameof(Resources.Resources.ResourceActionTracesText)], Icon = s_tracesIcon, OnClick = () => { - NavigationManager.NavigateTo(DashboardUrls.TracesUrl(resource: GetResourceName(Resource))); + navigationManager.NavigateTo(DashboardUrls.TracesUrl(resource: getResourceName(resource))); return Task.CompletedTask; } }); - _menuItems.Add(new MenuButtonItem + menuItems.Add(new MenuButtonItem { - Text = Loc[nameof(Resources.Resources.ResourceActionMetricsText)], - Tooltip = Loc[nameof(Resources.Resources.ResourceActionMetricsText)], + Text = loc[nameof(Resources.Resources.ResourceActionMetricsText)], + Tooltip = loc[nameof(Resources.Resources.ResourceActionMetricsText)], Icon = s_metricsIcon, OnClick = () => { - NavigationManager.NavigateTo(DashboardUrls.MetricsUrl(resource: GetResourceName(Resource))); + navigationManager.NavigateTo(DashboardUrls.MetricsUrl(resource: getResourceName(resource))); return Task.CompletedTask; } }); } - // If display is desktop then we display highlighted commands next to the ... button. - if (ViewportInformation.IsDesktop) - { - _highlightedCommands.AddRange(Commands.Where(c => c.IsHighlighted && c.State != CommandViewModelState.Hidden).Take(MaxHighlightedCount)); - } - - var menuCommands = Commands.Where(c => !_highlightedCommands.Contains(c) && c.State != CommandViewModelState.Hidden).ToList(); + var menuCommands = resource.Commands + .Where(c => c.State != CommandViewModelState.Hidden) + .OrderBy(c => !c.IsHighlighted) + .ToList(); if (menuCommands.Count > 0) { - _menuItems.Add(new MenuButtonItem { IsDivider = true }); + menuItems.Add(new MenuButtonItem { IsDivider = true }); foreach (var command in menuCommands) { var icon = (!string.IsNullOrEmpty(command.IconName) && IconResolver.ResolveIconName(command.IconName, IconSize.Size16, command.IconVariant) is { } i) ? i : null; - _menuItems.Add(new MenuButtonItem + menuItems.Add(new MenuButtonItem { Text = command.DisplayName, Tooltip = command.DisplayDescription, Icon = icon, - OnClick = () => CommandSelected.InvokeAsync(command), - IsDisabled = command.State == CommandViewModelState.Disabled || IsCommandExecuting(Resource, command) + OnClick = () => commandSelected(command), + IsDisabled = command.State == CommandViewModelState.Disabled || isCommandExecuting(resource, command) }); } } diff --git a/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor b/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor index 64c89e164e3..a4cd23b2c22 100644 --- a/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor +++ b/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor @@ -34,7 +34,7 @@ { if (_highlightedCommands.Count > 0) { - @Loc[nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsResourceCommands)] + @Loc[nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsResourceActions)] @foreach (var command in _highlightedCommands) { @@ -60,7 +60,7 @@ + Title="@Loc[nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsResourceActions)]" /> } } diff --git a/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs b/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs index 35fc1050a5e..a1c58e7b432 100644 --- a/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs @@ -19,7 +19,6 @@ using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; -using Microsoft.FluentUI.AspNetCore.Components; using Microsoft.JSInterop; using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons; @@ -57,12 +56,18 @@ private sealed class ConsoleLogsSubscription [Inject] public required NavigationManager NavigationManager { get; init; } + [Inject] + public required TelemetryRepository TelemetryRepository { get; init; } + [Inject] public required ILogger Logger { get; init; } [Inject] public required IStringLocalizer Loc { get; init; } + [Inject] + public required IStringLocalizer ResourcesLoc { get; init; } + [Inject] public required IStringLocalizer ControlsStringsLoc { get; init; } @@ -331,23 +336,23 @@ private void UpdateMenuButtons() _highlightedCommands.AddRange(PageViewModel.SelectedResource.Commands.Where(c => c.IsHighlighted && c.State != CommandViewModelState.Hidden).Take(DashboardUIHelpers.MaxHighlightedCommands)); } - var menuCommands = PageViewModel.SelectedResource.Commands.Where(c => !_highlightedCommands.Contains(c) && c.State != CommandViewModelState.Hidden).ToList(); - if (menuCommands.Count > 0) - { - foreach (var command in menuCommands) + ResourceActions.AddMenuItems( + _resourceMenuItems, + openingMenuButtonId: null, + PageViewModel.SelectedResource, + NavigationManager, + TelemetryRepository, + GetResourceName, + ControlsStringsLoc, + ResourcesLoc, + buttonId => { - var icon = (!string.IsNullOrEmpty(command.IconName) && IconResolver.ResolveIconName(command.IconName, IconSize.Size16, command.IconVariant) is { } i) ? i : null; - - _resourceMenuItems.Add(new MenuButtonItem - { - Text = command.DisplayName, - Tooltip = command.DisplayDescription, - Icon = icon, - OnClick = () => ExecuteResourceCommandAsync(command), - IsDisabled = command.State == CommandViewModelState.Disabled || DashboardCommandExecutor.IsExecuting(PageViewModel.SelectedResource!.Name, command.Name) - }); - } - } + NavigationManager.NavigateTo(DashboardUrls.ResourcesUrl(resource: PageViewModel.SelectedResource.Name)); + return Task.CompletedTask; + }, + ExecuteResourceCommandAsync, + (resource, command) => DashboardCommandExecutor.IsExecuting(resource.Name, command.Name), + showConsoleLogsItem: false); } } diff --git a/src/Aspire.Dashboard/Components/Pages/Resources.razor b/src/Aspire.Dashboard/Components/Pages/Resources.razor index 7c911708a29..1a6968555e6 100644 --- a/src/Aspire.Dashboard/Components/Pages/Resources.razor +++ b/src/Aspire.Dashboard/Components/Pages/Resources.razor @@ -6,6 +6,7 @@ @using Aspire.Dashboard.Components.Controls.Grid @using Aspire.Dashboard.Model @using Humanizer +@using System.Collections.Immutable @inject IStringLocalizer Loc @inject IStringLocalizer ControlsStringsLoc @inject IStringLocalizer ColumnsLoc @@ -94,7 +95,7 @@ ViewKey="ResourcesList" OnResize="@(r => _manager.SetWidthFraction(r.Orientation == Orientation.Horizontal ? r.Panel1Fraction : 1))"> -
+
@* Tab content isn't nested inside FluentTab elements. The tab control is just used to display the tabs. @@ -177,8 +178,7 @@
-
+ +
diff --git a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs index 2d4a0504fe0..646ae8c8f69 100644 --- a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs @@ -99,6 +99,11 @@ public partial class Resources : ComponentBase, IAsyncDisposable, IPageWithSessi private bool _graphInitialized; private AspirePageContentLayout? _contentLayout; + private AspireMenu? _contextMenu; + private bool _contextMenuOpen; + private readonly List _contextMenuItems = new(); + private TaskCompletionSource? _contextMenuClosedTcs; + private ColumnResizeLabels _resizeLabels = ColumnResizeLabels.Default; private ColumnSortLabels _sortLabels = ColumnSortLabels.Default; private bool _showResourceTypeColumn; @@ -346,6 +351,18 @@ await resources.InvokeAsync(async () => }); } } + + [JSInvokable] + public async Task ResourceContextMenu(string id, int clientX, int clientY) + { + if (resources._resourceByName.TryGetValue(id, out var resource)) + { + await resources.InvokeAsync(async () => + { + await resources.ShowContextMenuAsync(resource, clientX, clientY); + }); + } + } } internal IEnumerable GetFilteredResources() @@ -494,6 +511,36 @@ private bool ApplicationErrorCountsChanged(Dictionary newAp return false; } + private async Task ShowContextMenuAsync(ResourceViewModel resource, int clientX, int clientY) + { + if (_contextMenu is { } contextMenu) + { + _contextMenuItems.Clear(); + ResourceActions.AddMenuItems( + _contextMenuItems, + openingMenuButtonId: null, + resource, + NavigationManager, + TelemetryRepository, + GetResourceName, + ControlsStringsLoc, + Loc, + (buttonId) => ShowResourceDetailsAsync(resource, buttonId), + (command) => ExecuteResourceCommandAsync(resource, command), + (resource, command) => DashboardCommandExecutor.IsExecuting(resource.Name, command.Name), + showConsoleLogsItem: true); + + Debug.Assert(_contextMenuClosedTcs == null, "Shouldn't be a TCS for an open context menu."); + _contextMenuClosedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await contextMenu.OpenAsync(clientX, clientY); + StateHasChanged(); + + // Completed when the overlay closes. + await _contextMenuClosedTcs.Task; + } + } + private async Task ShowResourceDetailsAsync(ResourceViewModel resource, string? buttonId) { _elementIdBeforeDetailsViewOpened = buttonId; @@ -753,4 +800,15 @@ public async ValueTask DisposeAsync() await TaskHelpers.WaitIgnoreCancelAsync(_resourceSubscriptionTask); } + + private async Task ContextMenuClosed(Microsoft.AspNetCore.Components.Web.MouseEventArgs args) + { + if (_contextMenu is { } menu) + { + await menu.CloseAsync(); + } + + _contextMenuClosedTcs?.SetResult(); + _contextMenuClosedTcs = null; + } } diff --git a/src/Aspire.Dashboard/Resources/ConsoleLogs.Designer.cs b/src/Aspire.Dashboard/Resources/ConsoleLogs.Designer.cs index 230345f3056..16eadd0740c 100644 --- a/src/Aspire.Dashboard/Resources/ConsoleLogs.Designer.cs +++ b/src/Aspire.Dashboard/Resources/ConsoleLogs.Designer.cs @@ -151,11 +151,11 @@ public static string ConsoleLogsPauseDetails { } /// - /// Looks up a localized string similar to Resource commands. + /// Looks up a localized string similar to Resource actions. /// - public static string ConsoleLogsResourceCommands { + public static string ConsoleLogsResourceActions { get { - return ResourceManager.GetString("ConsoleLogsResourceCommands", resourceCulture); + return ResourceManager.GetString("ConsoleLogsResourceActions", resourceCulture); } } diff --git a/src/Aspire.Dashboard/Resources/ConsoleLogs.resx b/src/Aspire.Dashboard/Resources/ConsoleLogs.resx index 0560ea925fb..1baee31e471 100644 --- a/src/Aspire.Dashboard/Resources/ConsoleLogs.resx +++ b/src/Aspire.Dashboard/Resources/ConsoleLogs.resx @@ -160,8 +160,8 @@ Console logs settings - - Resource commands + + Resource actions Show timestamps diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.cs.xlf index e7237d26380..8e38f9b9606 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.cs.xlf @@ -52,9 +52,9 @@ <Zachytávání protokolů bylo pozastaveno mezi {0} a {1}, odfiltrované protokoly: {2} > {0} is a date, {1} is a date, {2} is a number. - - Resource commands - Příkazy prostředku + + Resource actions + Resource actions diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.de.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.de.xlf index 053803343bb..55233793de9 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.de.xlf @@ -52,9 +52,9 @@ <Protokollerfassung wurde zwischen {0} und {1} angehalten, {2} Protokoll(e) herausgefiltert> {0} is a date, {1} is a date, {2} is a number. - - Resource commands - Ressourcenbefehle + + Resource actions + Resource actions diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.es.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.es.xlf index e7e829f0c81..9feeebf78c2 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.es.xlf @@ -52,9 +52,9 @@ < Captura de registro en pausa entre {0} y {1}, {2} registros filtrados> {0} is a date, {1} is a date, {2} is a number. - - Resource commands - Comandos de recursos + + Resource actions + Resource actions diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.fr.xlf index e0e39b83395..4b5435682a9 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.fr.xlf @@ -52,9 +52,9 @@ <Capture de journal suspendue entre le ou les journaux {0} et {1}, {2} filtrés> {0} is a date, {1} is a date, {2} is a number. - - Resource commands - Commandes de ressource + + Resource actions + Resource actions diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.it.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.it.xlf index d43a2e194a1..bee1937e4a4 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.it.xlf @@ -52,9 +52,9 @@ <L'acquisizione dei log è stata sospesa tra {0} e {1}, {2} log filtrati> {0} is a date, {1} is a date, {2} is a number. - - Resource commands - Comandi risorsa + + Resource actions + Resource actions diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ja.xlf index e073e012dfa..0254c67e242 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ja.xlf @@ -52,9 +52,9 @@ <ログ キャプチャが {0} と {1} の間で一時停止されました。{2} 件のログがフィルターで除外されました> {0} is a date, {1} is a date, {2} is a number. - - Resource commands - リソース コマンド + + Resource actions + Resource actions diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ko.xlf index 2b21feab61a..926447845c9 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ko.xlf @@ -52,9 +52,9 @@ <로그 캡처가 {0} 및 {1}간에 일시 중지되었습니다. {2} 로그가 필터링되었습니다> {0} is a date, {1} is a date, {2} is a number. - - Resource commands - 리소스 명령 + + Resource actions + Resource actions diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.pl.xlf index 67c717d6750..0bf0bc7fcac 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.pl.xlf @@ -52,9 +52,9 @@ <Przechwytywanie dziennika zostało wstrzymane między {0} a {1}, liczba odfiltrowanych dzienników: {2}> {0} is a date, {1} is a date, {2} is a number. - - Resource commands - Polecenia zasobów + + Resource actions + Resource actions diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.pt-BR.xlf index 90b0547e47b..aace822da21 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.pt-BR.xlf @@ -52,9 +52,9 @@ <Captura de log em pausa entre {0} e {1}, {2} logs filtrados> {0} is a date, {1} is a date, {2} is a number. - - Resource commands - Comandos de recursos + + Resource actions + Resource actions diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ru.xlf index a18363e8c2f..ce67e837680 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.ru.xlf @@ -52,9 +52,9 @@ <Запись журнала приостановлена между {0} и {1}, отфильтровано журналов: {2}> {0} is a date, {1} is a date, {2} is a number. - - Resource commands - Команды ресурсов + + Resource actions + Resource actions diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.tr.xlf index c8427486460..80562ce80c7 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.tr.xlf @@ -52,9 +52,9 @@ <Günlük yakalama, {0} ile {1} arasında duraklatıldı, {2} günlük filtrelendi> {0} is a date, {1} is a date, {2} is a number. - - Resource commands - Kaynak komutları + + Resource actions + Resource actions diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.zh-Hans.xlf index 5e6279c3dbc..5e3ab9aeeff 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.zh-Hans.xlf @@ -52,9 +52,9 @@ <日志捕获已在 {0} 和 {1} 之间暂停,筛选掉 {2} 条日志> {0} is a date, {1} is a date, {2} is a number. - - Resource commands - 资源命令 + + Resource actions + Resource actions diff --git a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.zh-Hant.xlf index c6183911ad8..da4dad0ca14 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ConsoleLogs.zh-Hant.xlf @@ -52,9 +52,9 @@ <{0} 與 {1} 之間的記錄擷取已暫停,篩選出了 {2} 筆記錄> {0} is a date, {1} is a date, {2} is a number. - - Resource commands - 資源命令 + + Resource actions + Resource actions diff --git a/src/Aspire.Dashboard/wwwroot/js/app-resourcegraph.js b/src/Aspire.Dashboard/wwwroot/js/app-resourcegraph.js index 36e973cf3aa..382876fd730 100644 --- a/src/Aspire.Dashboard/wwwroot/js/app-resourcegraph.js +++ b/src/Aspire.Dashboard/wwwroot/js/app-resourcegraph.js @@ -31,6 +31,7 @@ class ResourceGraph { constructor(resourcesInterop) { this.resources = []; this.resourcesInterop = resourcesInterop; + this.openContextMenu = false; this.nodes = []; this.links = []; @@ -311,6 +312,7 @@ class ResourceGraph { .append("g") .attr("class", "resource-scale") .on('click', this.selectNode) + .on('contextmenu', this.nodeContextMenu) .on('mouseover', this.hoverNode) .on('mouseout', this.unHoverNode); newNodesContainer @@ -358,12 +360,10 @@ class ResourceGraph { var resourceNameGroup = newNodesContainer .append("g") .attr("transform", "translate(0,71)") - .attr("class", "resource-name") - .on('click', this.selectNode); + .attr("class", "resource-name"); resourceNameGroup .append("text") - .text(n => trimText(n.label, 30)) - .on('click', this.selectNode); + .text(n => trimText(n.label, 30)); resourceNameGroup .append("title") .text(n => n.label); @@ -473,6 +473,25 @@ class ResourceGraph { return 'resource-link'; } + nodeContextMenu = async (event) => { + console.log(event); + + var data = event.target.__data__; + + event.preventDefault(); + + this.openContextMenu = true; + + try { + // Wait for method completion. It completes when the context menu is closed. + await this.resourcesInterop.invokeMethodAsync('ResourceContextMenu', data.id, event.clientX, event.clientY); + } finally { + this.openContextMenu = false; + + this.updateNodeHighlights(null); + } + }; + selectNode = (event) => { var data = event.target.__data__; @@ -518,7 +537,11 @@ class ResourceGraph { } unHoverNode = (event) => { - this.updateNodeHighlights(null); + // Don't unhover the selected node when the context menu is open. + // This is done to keep the node selected until the context menu is closed. + if (!this.openContextMenu) { + this.updateNodeHighlights(null); + } }; nodeEquals(resource1, resource2) { From 952be07b41f05433c2af1221d6fdd9390cc5ba18 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 10 Apr 2025 16:10:07 +0800 Subject: [PATCH 2/7] Update --- src/Aspire.Dashboard/Components/Pages/Resources.razor | 1 - src/Aspire.Dashboard/Components/Pages/Resources.razor.cs | 6 ++++-- src/Aspire.Dashboard/wwwroot/js/app-resourcegraph.js | 3 +-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Pages/Resources.razor b/src/Aspire.Dashboard/Components/Pages/Resources.razor index 1a6968555e6..ba9a7d83959 100644 --- a/src/Aspire.Dashboard/Components/Pages/Resources.razor +++ b/src/Aspire.Dashboard/Components/Pages/Resources.razor @@ -6,7 +6,6 @@ @using Aspire.Dashboard.Components.Controls.Grid @using Aspire.Dashboard.Model @using Humanizer -@using System.Collections.Immutable @inject IStringLocalizer Loc @inject IStringLocalizer ControlsStringsLoc @inject IStringLocalizer ColumnsLoc diff --git a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs index 646ae8c8f69..9e738694bde 100644 --- a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs @@ -530,7 +530,9 @@ private async Task ShowContextMenuAsync(ResourceViewModel resource, int clientX, (resource, command) => DashboardCommandExecutor.IsExecuting(resource.Name, command.Name), showConsoleLogsItem: true); - Debug.Assert(_contextMenuClosedTcs == null, "Shouldn't be a TCS for an open context menu."); + // The previous context menu should always be closed by this point but complete just in case. + _contextMenuClosedTcs?.TrySetResult(); + _contextMenuClosedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); await contextMenu.OpenAsync(clientX, clientY); @@ -808,7 +810,7 @@ private async Task ContextMenuClosed(Microsoft.AspNetCore.Components.Web.MouseEv await menu.CloseAsync(); } - _contextMenuClosedTcs?.SetResult(); + _contextMenuClosedTcs?.TrySetResult(); _contextMenuClosedTcs = null; } } diff --git a/src/Aspire.Dashboard/wwwroot/js/app-resourcegraph.js b/src/Aspire.Dashboard/wwwroot/js/app-resourcegraph.js index 382876fd730..4042f5923c8 100644 --- a/src/Aspire.Dashboard/wwwroot/js/app-resourcegraph.js +++ b/src/Aspire.Dashboard/wwwroot/js/app-resourcegraph.js @@ -474,10 +474,9 @@ class ResourceGraph { } nodeContextMenu = async (event) => { - console.log(event); - var data = event.target.__data__; + // Prevent default browser context menu. event.preventDefault(); this.openContextMenu = true; From 343c399685aa1d43d7fefc17614e077b85fbd1b4 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 10 Apr 2025 16:24:11 +0800 Subject: [PATCH 3/7] Update --- .../Aspire.Dashboard.Components.Tests/Pages/ConsoleLogsTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Aspire.Dashboard.Components.Tests/Pages/ConsoleLogsTests.cs b/tests/Aspire.Dashboard.Components.Tests/Pages/ConsoleLogsTests.cs index 6c27ea552f4..234cb620c3f 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Pages/ConsoleLogsTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Pages/ConsoleLogsTests.cs @@ -8,6 +8,7 @@ using Aspire.Dashboard.Configuration; using Aspire.Dashboard.Model; using Aspire.Dashboard.Model.BrowserStorage; +using Aspire.Dashboard.Otlp.Storage; using Aspire.Dashboard.Utils; using Aspire.Hosting.ConsoleLogs; using Aspire.Tests.Shared.DashboardModel; @@ -538,6 +539,7 @@ private void SetupConsoleLogsServices(TestDashboardClient? dashboardClient = nul Services.AddSingleton(); Services.AddSingleton>(Options.Create(new DashboardOptions())); Services.AddSingleton(); + Services.AddSingleton(); Services.AddSingleton(); Services.AddSingleton(); Services.AddSingleton(); From 3d7f95417518bb0a38a1390846b70ab8876504b5 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 11 Apr 2025 06:21:55 +0800 Subject: [PATCH 4/7] Clean up --- .../Controls/ResourceActions.razor.cs | 106 +--------------- .../Components/Pages/ConsoleLogs.razor.cs | 2 +- .../Components/Pages/Resources.razor.cs | 2 +- .../Model/ResourceMenuItems.cs | 119 ++++++++++++++++++ 4 files changed, 122 insertions(+), 107 deletions(-) create mode 100644 src/Aspire.Dashboard/Model/ResourceMenuItems.cs diff --git a/src/Aspire.Dashboard/Components/Controls/ResourceActions.razor.cs b/src/Aspire.Dashboard/Components/Controls/ResourceActions.razor.cs index 47ba833337d..852684a89e5 100644 --- a/src/Aspire.Dashboard/Components/Controls/ResourceActions.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/ResourceActions.razor.cs @@ -3,7 +3,6 @@ using Aspire.Dashboard.Model; using Aspire.Dashboard.Otlp.Storage; -using Aspire.Dashboard.Utils; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Localization; using Microsoft.FluentUI.AspNetCore.Components; @@ -13,11 +12,7 @@ namespace Aspire.Dashboard.Components; public partial class ResourceActions : ComponentBase { - private static readonly Icon s_viewDetailsIcon = new Icons.Regular.Size16.Info(); private static readonly Icon s_consoleLogsIcon = new Icons.Regular.Size16.SlideText(); - private static readonly Icon s_structuredLogsIcon = new Icons.Regular.Size16.SlideTextSparkle(); - private static readonly Icon s_tracesIcon = new Icons.Regular.Size16.GanttChart(); - private static readonly Icon s_metricsIcon = new Icons.Regular.Size16.ChartMultiple(); private AspireMenuButton? _menuButton; @@ -62,7 +57,7 @@ protected override void OnParametersSet() _menuItems.Clear(); _highlightedCommands.Clear(); - AddMenuItems( + ResourceMenuItems.AddMenuItems( _menuItems, _menuButton?.MenuButtonId, Resource, @@ -82,103 +77,4 @@ protected override void OnParametersSet() _highlightedCommands.AddRange(Resource.Commands.Where(c => c.IsHighlighted && c.State != CommandViewModelState.Hidden).Take(MaxHighlightedCount)); } } - - public static void AddMenuItems( - List menuItems, - string? openingMenuButtonId, - ResourceViewModel resource, - NavigationManager navigationManager, - TelemetryRepository telemetryRepository, - Func getResourceName, - IStringLocalizer controlLoc, - IStringLocalizer loc, - Func onViewDetails, - Func commandSelected, - Func isCommandExecuting, - bool showConsoleLogsItem) - { - menuItems.Add(new MenuButtonItem - { - Text = controlLoc[nameof(Resources.ControlsStrings.ActionViewDetailsText)], - Icon = s_viewDetailsIcon, - OnClick = () => onViewDetails(openingMenuButtonId) - }); - - if (showConsoleLogsItem) - { - menuItems.Add(new MenuButtonItem - { - Text = loc[nameof(Resources.Resources.ResourceActionConsoleLogsText)], - Icon = s_consoleLogsIcon, - OnClick = () => - { - navigationManager.NavigateTo(DashboardUrls.ConsoleLogsUrl(resource: resource.Name)); - return Task.CompletedTask; - } - }); - } - - // Show telemetry menu items if there is telemetry for the resource. - var hasTelemetryApplication = telemetryRepository.GetApplicationByCompositeName(resource.Name) != null; - if (hasTelemetryApplication) - { - menuItems.Add(new MenuButtonItem { IsDivider = true }); - menuItems.Add(new MenuButtonItem - { - Text = loc[nameof(Resources.Resources.ResourceActionStructuredLogsText)], - Tooltip = loc[nameof(Resources.Resources.ResourceActionStructuredLogsText)], - Icon = s_structuredLogsIcon, - OnClick = () => - { - navigationManager.NavigateTo(DashboardUrls.StructuredLogsUrl(resource: getResourceName(resource))); - return Task.CompletedTask; - } - }); - menuItems.Add(new MenuButtonItem - { - Text = loc[nameof(Resources.Resources.ResourceActionTracesText)], - Tooltip = loc[nameof(Resources.Resources.ResourceActionTracesText)], - Icon = s_tracesIcon, - OnClick = () => - { - navigationManager.NavigateTo(DashboardUrls.TracesUrl(resource: getResourceName(resource))); - return Task.CompletedTask; - } - }); - menuItems.Add(new MenuButtonItem - { - Text = loc[nameof(Resources.Resources.ResourceActionMetricsText)], - Tooltip = loc[nameof(Resources.Resources.ResourceActionMetricsText)], - Icon = s_metricsIcon, - OnClick = () => - { - navigationManager.NavigateTo(DashboardUrls.MetricsUrl(resource: getResourceName(resource))); - return Task.CompletedTask; - } - }); - } - - var menuCommands = resource.Commands - .Where(c => c.State != CommandViewModelState.Hidden) - .OrderBy(c => !c.IsHighlighted) - .ToList(); - if (menuCommands.Count > 0) - { - menuItems.Add(new MenuButtonItem { IsDivider = true }); - - foreach (var command in menuCommands) - { - var icon = (!string.IsNullOrEmpty(command.IconName) && IconResolver.ResolveIconName(command.IconName, IconSize.Size16, command.IconVariant) is { } i) ? i : null; - - menuItems.Add(new MenuButtonItem - { - Text = command.DisplayName, - Tooltip = command.DisplayDescription, - Icon = icon, - OnClick = () => commandSelected(command), - IsDisabled = command.State == CommandViewModelState.Disabled || isCommandExecuting(resource, command) - }); - } - } - } } diff --git a/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs b/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs index a1c58e7b432..cb319ed4864 100644 --- a/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs @@ -336,7 +336,7 @@ private void UpdateMenuButtons() _highlightedCommands.AddRange(PageViewModel.SelectedResource.Commands.Where(c => c.IsHighlighted && c.State != CommandViewModelState.Hidden).Take(DashboardUIHelpers.MaxHighlightedCommands)); } - ResourceActions.AddMenuItems( + ResourceMenuItems.AddMenuItems( _resourceMenuItems, openingMenuButtonId: null, PageViewModel.SelectedResource, diff --git a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs index 9e738694bde..9955ac38b1a 100644 --- a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs @@ -516,7 +516,7 @@ private async Task ShowContextMenuAsync(ResourceViewModel resource, int clientX, if (_contextMenu is { } contextMenu) { _contextMenuItems.Clear(); - ResourceActions.AddMenuItems( + ResourceMenuItems.AddMenuItems( _contextMenuItems, openingMenuButtonId: null, resource, diff --git a/src/Aspire.Dashboard/Model/ResourceMenuItems.cs b/src/Aspire.Dashboard/Model/ResourceMenuItems.cs new file mode 100644 index 00000000000..6d8cafac0c7 --- /dev/null +++ b/src/Aspire.Dashboard/Model/ResourceMenuItems.cs @@ -0,0 +1,119 @@ +// 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.Otlp.Storage; +using Aspire.Dashboard.Utils; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Localization; +using Microsoft.FluentUI.AspNetCore.Components; +using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons; + +namespace Aspire.Dashboard.Model; + +public static class ResourceMenuItems +{ + private static readonly Icon s_viewDetailsIcon = new Icons.Regular.Size16.Info(); + private static readonly Icon s_consoleLogsIcon = new Icons.Regular.Size16.SlideText(); + private static readonly Icon s_structuredLogsIcon = new Icons.Regular.Size16.SlideTextSparkle(); + private static readonly Icon s_tracesIcon = new Icons.Regular.Size16.GanttChart(); + private static readonly Icon s_metricsIcon = new Icons.Regular.Size16.ChartMultiple(); + + public static void AddMenuItems( + List menuItems, + string? openingMenuButtonId, + ResourceViewModel resource, + NavigationManager navigationManager, + TelemetryRepository telemetryRepository, + Func getResourceName, + IStringLocalizer controlLoc, + IStringLocalizer loc, + Func onViewDetails, + Func commandSelected, + Func isCommandExecuting, + bool showConsoleLogsItem) + { + menuItems.Add(new MenuButtonItem + { + Text = controlLoc[nameof(Resources.ControlsStrings.ActionViewDetailsText)], + Icon = s_viewDetailsIcon, + OnClick = () => onViewDetails(openingMenuButtonId) + }); + + if (showConsoleLogsItem) + { + menuItems.Add(new MenuButtonItem + { + Text = loc[nameof(Resources.Resources.ResourceActionConsoleLogsText)], + Icon = s_consoleLogsIcon, + OnClick = () => + { + navigationManager.NavigateTo(DashboardUrls.ConsoleLogsUrl(resource: resource.Name)); + return Task.CompletedTask; + } + }); + } + + // Show telemetry menu items if there is telemetry for the resource. + var hasTelemetryApplication = telemetryRepository.GetApplicationByCompositeName(resource.Name) != null; + if (hasTelemetryApplication) + { + menuItems.Add(new MenuButtonItem { IsDivider = true }); + menuItems.Add(new MenuButtonItem + { + Text = loc[nameof(Resources.Resources.ResourceActionStructuredLogsText)], + Tooltip = loc[nameof(Resources.Resources.ResourceActionStructuredLogsText)], + Icon = s_structuredLogsIcon, + OnClick = () => + { + navigationManager.NavigateTo(DashboardUrls.StructuredLogsUrl(resource: getResourceName(resource))); + return Task.CompletedTask; + } + }); + menuItems.Add(new MenuButtonItem + { + Text = loc[nameof(Resources.Resources.ResourceActionTracesText)], + Tooltip = loc[nameof(Resources.Resources.ResourceActionTracesText)], + Icon = s_tracesIcon, + OnClick = () => + { + navigationManager.NavigateTo(DashboardUrls.TracesUrl(resource: getResourceName(resource))); + return Task.CompletedTask; + } + }); + menuItems.Add(new MenuButtonItem + { + Text = loc[nameof(Resources.Resources.ResourceActionMetricsText)], + Tooltip = loc[nameof(Resources.Resources.ResourceActionMetricsText)], + Icon = s_metricsIcon, + OnClick = () => + { + navigationManager.NavigateTo(DashboardUrls.MetricsUrl(resource: getResourceName(resource))); + return Task.CompletedTask; + } + }); + } + + var menuCommands = resource.Commands + .Where(c => c.State != CommandViewModelState.Hidden) + .OrderBy(c => !c.IsHighlighted) + .ToList(); + if (menuCommands.Count > 0) + { + menuItems.Add(new MenuButtonItem { IsDivider = true }); + + foreach (var command in menuCommands) + { + var icon = (!string.IsNullOrEmpty(command.IconName) && IconResolver.ResolveIconName(command.IconName, IconSize.Size16, command.IconVariant) is { } i) ? i : null; + + menuItems.Add(new MenuButtonItem + { + Text = command.DisplayName, + Tooltip = command.DisplayDescription, + Icon = icon, + OnClick = () => commandSelected(command), + IsDisabled = command.State == CommandViewModelState.Disabled || isCommandExecuting(resource, command) + }); + } + } + } +} From af0361fbde8b137d50f83eab5e42eaae3a95bc9e Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 11 Apr 2025 06:32:23 +0800 Subject: [PATCH 5/7] Comments --- src/Aspire.Dashboard/Components/Pages/Resources.razor.cs | 3 +++ src/Aspire.Dashboard/wwwroot/js/app-resourcegraph.js | 1 + 2 files changed, 4 insertions(+) diff --git a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs index 9955ac38b1a..5d87f0012e8 100644 --- a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs @@ -513,6 +513,9 @@ private bool ApplicationErrorCountsChanged(Dictionary newAp private async Task ShowContextMenuAsync(ResourceViewModel resource, int clientX, int clientY) { + // This is called when the browser requests to show the context menu for a resource. + // The method doesn't complete until the context menu is closed so the browser can await + // it and perform clean up when the context menu is closed. if (_contextMenu is { } contextMenu) { _contextMenuItems.Clear(); diff --git a/src/Aspire.Dashboard/wwwroot/js/app-resourcegraph.js b/src/Aspire.Dashboard/wwwroot/js/app-resourcegraph.js index 4042f5923c8..7f15ffae5d7 100644 --- a/src/Aspire.Dashboard/wwwroot/js/app-resourcegraph.js +++ b/src/Aspire.Dashboard/wwwroot/js/app-resourcegraph.js @@ -487,6 +487,7 @@ class ResourceGraph { } finally { this.openContextMenu = false; + // Unselect the node when the context menu is closed to reset mouseover state. this.updateNodeHighlights(null); } }; From 9564d9aa7dcee4038bf9328a28680519ead35e95 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 11 Apr 2025 06:41:26 +0800 Subject: [PATCH 6/7] Clean up --- src/Aspire.Dashboard/Components/Controls/AspireMenu.razor | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Aspire.Dashboard/Components/Controls/AspireMenu.razor b/src/Aspire.Dashboard/Components/Controls/AspireMenu.razor index c1e0fe72210..c8cf3ae23db 100644 --- a/src/Aspire.Dashboard/Components/Controls/AspireMenu.razor +++ b/src/Aspire.Dashboard/Components/Controls/AspireMenu.razor @@ -1,7 +1,6 @@ @namespace Aspire.Dashboard.Components @using System.Collections.Immutable @using Aspire.Dashboard.Model -@using Microsoft.FluentUI.AspNetCore.Components.DesignTokens @inherits FluentComponentBase From 594a9f33b212e1d4568a4f1bfe55bef94ab82703 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 11 Apr 2025 06:42:16 +0800 Subject: [PATCH 7/7] Clean up --- src/Aspire.Dashboard/Components/Controls/AspireMenuButton.razor | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Aspire.Dashboard/Components/Controls/AspireMenuButton.razor b/src/Aspire.Dashboard/Components/Controls/AspireMenuButton.razor index 594008ef9ce..8a1c0d65d36 100644 --- a/src/Aspire.Dashboard/Components/Controls/AspireMenuButton.razor +++ b/src/Aspire.Dashboard/Components/Controls/AspireMenuButton.razor @@ -1,7 +1,6 @@ @namespace Aspire.Dashboard.Components @using System.Collections.Immutable @using Aspire.Dashboard.Model -@using Microsoft.FluentUI.AspNetCore.Components.DesignTokens @inherits FluentComponentBase @{