File: Model\ResourceMenuItems.cs
Web Access
Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard)
// 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.Resources;
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();
    private static readonly Icon s_linkIcon = new Icons.Regular.Size16.Link();
    private static readonly Icon s_toolboxIcon = new Icons.Regular.Size16.Toolbox();
    private static readonly Icon s_linkMultipleIcon = new Icons.Regular.Size16.LinkMultiple();
 
    public static void AddMenuItems(
        List<MenuButtonItem> menuItems,
        ResourceViewModel resource,
        NavigationManager navigationManager,
        TelemetryRepository telemetryRepository,
        Func<ResourceViewModel, string> getResourceName,
        IStringLocalizer<ControlsStrings> controlLoc,
        IStringLocalizer<Resources.Resources> loc,
        IStringLocalizer<Commands> commandsLoc,
        EventCallback onViewDetails,
        EventCallback<CommandViewModel> commandSelected,
        Func<ResourceViewModel, CommandViewModel, bool> isCommandExecuting,
        bool showConsoleLogsItem,
        bool showUrls)
    {
        menuItems.Add(new MenuButtonItem
        {
            Text = controlLoc[nameof(ControlsStrings.ActionViewDetailsText)],
            Icon = s_viewDetailsIcon,
            OnClick = onViewDetails.InvokeAsync
        });
 
        if (showConsoleLogsItem)
        {
            menuItems.Add(new MenuButtonItem
            {
                Text = loc[nameof(Resources.Resources.ResourceActionConsoleLogsText)],
                Icon = s_consoleLogsIcon,
                OnClick = () =>
                {
                    navigationManager.NavigateTo(DashboardUrls.ConsoleLogsUrl(resource: getResourceName(resource)));
                    return Task.CompletedTask;
                }
            });
        }
 
        AddTelemetryMenuItems(menuItems, resource, navigationManager, telemetryRepository, getResourceName, loc);
 
        AddCommandMenuItems(menuItems, resource, loc, commandsLoc, commandSelected, isCommandExecuting);
 
        if (showUrls)
        {
            AddUrlMenuItems(menuItems, resource, loc);
        }
    }
 
    private static void AddUrlMenuItems(List<MenuButtonItem> menuItems, ResourceViewModel resource, IStringLocalizer<Resources.Resources> loc)
    {
        var urls = ResourceUrlHelpers.GetUrls(resource, includeInternalUrls: false, includeNonEndpointUrls: true)
            .Where(u => !string.IsNullOrEmpty(u.Url))
            .ToList();
 
        if (urls.Count == 0)
        {
            return;
        }
 
        menuItems.Add(new MenuButtonItem { IsDivider = true });
 
        if (urls.Count > 5)
        {
            var urlItems = new List<MenuButtonItem>();
 
            foreach (var url in urls)
            {
                urlItems.Add(CreateUrlMenuItem(url));
            }
 
            menuItems.Add(new MenuButtonItem
            {
                Text = loc[nameof(Resources.Resources.ResourceActionUrlsText)],
                Tooltip = "", // No tooltip for the commands menu item.
                Icon = s_linkMultipleIcon,
                NestedMenuItems = urlItems
            });
        }
        else
        {
            foreach (var url in urls)
            {
                menuItems.Add(CreateUrlMenuItem(url));
            }
        }
    }
 
    private static MenuButtonItem CreateUrlMenuItem(DisplayedUrl url)
    {
        // Opens the URL in a new window when clicked.
        // It's important that this is done in the onclick event so the browser popup allows it.
        return new MenuButtonItem
        {
            Text = url.Text,
            Tooltip = url.Url,
            Icon = s_linkIcon,
            AdditionalAttributes = new Dictionary<string, object>
            {
                ["data-openbutton"] = "true",
                ["data-url"] = url.Url!,
                ["data-target"] = "_blank"
            }
        };
    }
 
    private static void AddTelemetryMenuItems(List<MenuButtonItem> menuItems, ResourceViewModel resource, NavigationManager navigationManager, TelemetryRepository telemetryRepository, Func<ResourceViewModel, string> getResourceName, IStringLocalizer<Resources.Resources> loc)
    {
        // Show telemetry menu items if there is telemetry for the resource.
        var telemetryResource = telemetryRepository.GetResourceByCompositeName(resource.Name);
        if (telemetryResource != null)
        {
            menuItems.Add(new MenuButtonItem { IsDivider = true });
 
            if (!telemetryResource.UninstrumentedPeer)
            {
                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;
                }
            });
 
            if (!telemetryResource.UninstrumentedPeer)
            {
                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;
                    }
                });
            }
        }
    }
 
    private static void AddCommandMenuItems(List<MenuButtonItem> menuItems, ResourceViewModel resource, IStringLocalizer<Resources.Resources> loc, IStringLocalizer<Commands> commandsLoc, EventCallback<CommandViewModel> commandSelected, Func<ResourceViewModel, CommandViewModel, bool> isCommandExecuting)
    {
        var menuCommands = resource.Commands
                    .Where(c => c.State != CommandViewModelState.Hidden)
                    .ToList();
 
        if (menuCommands.Count == 0)
        {
            return;
        }
 
        var highlightedMenuCommands = menuCommands.Where(c => c.IsHighlighted).ToList();
        var otherMenuCommands = menuCommands.Where(c => !c.IsHighlighted).ToList();
 
        menuItems.Add(new MenuButtonItem { IsDivider = true });
 
        // Always show the highlighted commands first and not in a sub-menu.
        foreach (var highlightedCommand in highlightedMenuCommands)
        {
            menuItems.Add(CreateMenuItem(highlightedCommand));
        }
 
        // If there are more than 5 commands, we group them under a "Commands" menu item. This is done to avoid the menu going off the end of the screen.
        // A scenario where this could happen is viewing the menu for a resource and the resource is in the middle of the screen.
        if (highlightedMenuCommands.Count + otherMenuCommands.Count > 5)
        {
            var commands = new List<MenuButtonItem>();
 
            foreach (var command in otherMenuCommands)
            {
                commands.Add(CreateMenuItem(command));
            }
 
            menuItems.Add(new MenuButtonItem
            {
                Text = loc[nameof(Resources.Resources.ResourceActionCommandsText)],
                Tooltip = "", // No tooltip for the commands menu item.
                Icon = s_toolboxIcon,
                NestedMenuItems = commands
            });
        }
        else
        {
            foreach (var command in otherMenuCommands)
            {
                menuItems.Add(CreateMenuItem(command));
            }
        }
 
        MenuButtonItem CreateMenuItem(CommandViewModel command)
        {
            var icon = (!string.IsNullOrEmpty(command.IconName) && IconResolver.ResolveIconName(command.IconName, IconSize.Size16, command.IconVariant) is { } i) ? i : null;
 
            return new MenuButtonItem
            {
                Text = command.GetDisplayName(commandsLoc),
                Tooltip = command.GetDisplayDescription(commandsLoc),
                Icon = icon,
                OnClick = () => commandSelected.InvokeAsync(command),
                IsDisabled = command.State == CommandViewModelState.Disabled || isCommandExecuting(resource, command)
            };
        }
    }
}