File: Model\ResourceMenuBuilder.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.Components.CustomIcons;
using Aspire.Dashboard.Components.Dialogs;
using Aspire.Dashboard.Model.Assistant;
using Aspire.Dashboard.Model.Assistant.Prompts;
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;
 
/// <summary>
/// Builds menu items for resource context menus and action buttons.
/// </summary>
public sealed class ResourceMenuBuilder
{
    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_gitHubCopilotIcon = new AspireIcons.Size16.GitHubCopilot();
    private static readonly Icon s_toolboxIcon = new Icons.Regular.Size16.Toolbox();
    private static readonly Icon s_linkMultipleIcon = new Icons.Regular.Size16.LinkMultiple();
    private static readonly Icon s_bracesIcon = new Icons.Regular.Size16.Braces();
    private static readonly Icon s_exportEnvIcon = new Icons.Regular.Size16.DocumentText();
 
    private readonly NavigationManager _navigationManager;
    private readonly TelemetryRepository _telemetryRepository;
    private readonly IAIContextProvider _aiContextProvider;
    private readonly IStringLocalizer<ControlsStrings> _controlLoc;
    private readonly IStringLocalizer<Resources.Resources> _loc;
    private readonly IStringLocalizer<Resources.AIAssistant> _aiAssistantLoc;
    private readonly IStringLocalizer<Resources.AIPrompts> _aiPromptsLoc;
    private readonly IStringLocalizer<Commands> _commandsLoc;
    private readonly IconResolver _iconResolver;
    private readonly DashboardDialogService _dialogService;
 
    /// <summary>
    /// Initializes a new instance of the <see cref="ResourceMenuBuilder"/> class.
    /// </summary>
    public ResourceMenuBuilder(
        NavigationManager navigationManager,
        TelemetryRepository telemetryRepository,
        IAIContextProvider aiContextProvider,
        IStringLocalizer<ControlsStrings> controlLoc,
        IStringLocalizer<Resources.Resources> loc,
        IStringLocalizer<Resources.AIAssistant> aiAssistantLoc,
        IStringLocalizer<Resources.AIPrompts> aiPromptsLoc,
        IStringLocalizer<Commands> commandsLoc,
        IconResolver iconResolver,
        DashboardDialogService dialogService)
    {
        _navigationManager = navigationManager;
        _telemetryRepository = telemetryRepository;
        _aiContextProvider = aiContextProvider;
        _controlLoc = controlLoc;
        _loc = loc;
        _aiAssistantLoc = aiAssistantLoc;
        _aiPromptsLoc = aiPromptsLoc;
        _commandsLoc = commandsLoc;
        _iconResolver = iconResolver;
        _dialogService = dialogService;
    }
 
    /// <summary>
    /// Adds menu items for a resource to the provided list.
    /// </summary>
    public void AddMenuItems(
        List<MenuButtonItem> menuItems,
        ResourceViewModel resource,
        Func<ResourceViewModel, string> getResourceName,
        EventCallback onViewDetails,
        EventCallback<CommandViewModel> commandSelected,
        Func<ResourceViewModel, CommandViewModel, bool> isCommandExecuting,
        bool showViewDetails,
        bool showConsoleLogsItem,
        bool showUrls)
    {
        if (showViewDetails)
        {
            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;
                }
            });
        }
 
        menuItems.Add(new MenuButtonItem
        {
            Text = _controlLoc[nameof(ControlsStrings.ExportJson)],
            Icon = s_bracesIcon,
            OnClick = async () =>
            {
                var result = ExportHelpers.GetResourceAsJson(resource, getResourceName);
                await TextVisualizerDialog.OpenDialogAsync(new OpenTextVisualizerDialogOptions
                {
                    DialogService = _dialogService,
                    ValueDescription = result.FileName,
                    Value = result.Content,
                    DownloadFileName = result.FileName,
                    ContainsSecret = true
                }).ConfigureAwait(false);
            }
        });
 
        if (resource.Environment.Length > 0)
        {
            menuItems.Add(new MenuButtonItem
            {
                Text = _controlLoc[nameof(ControlsStrings.ExportEnv)],
                Icon = s_exportEnvIcon,
                OnClick = async () =>
                {
                    var result = ExportHelpers.GetEnvironmentVariablesAsEnvFile(resource, getResourceName);
                    await TextVisualizerDialog.OpenDialogAsync(new OpenTextVisualizerDialogOptions
                    {
                        DialogService = _dialogService,
                        ValueDescription = result.FileName,
                        Value = result.Content,
                        DownloadFileName = result.FileName,
                        ContainsSecret = true
                    }).ConfigureAwait(false);
                }
            });
        }
 
        if (_aiContextProvider.Enabled)
        {
            menuItems.Add(new MenuButtonItem
            {
                Text = _aiAssistantLoc[nameof(AIAssistant.MenuTextAskGitHubCopilot)],
                Icon = s_gitHubCopilotIcon,
                OnClick = async () =>
                {
                    await _aiContextProvider.LaunchAssistantSidebarAsync(
                        promptContext => PromptContextsBuilder.AnalyzeResource(
                            promptContext,
                            _aiPromptsLoc.GetString(nameof(AIPrompts.PromptAnalyzeResource), resource.Name),
                            resource)).ConfigureAwait(false);
                }
            });
        }
 
        AddTelemetryMenuItems(menuItems, resource, getResourceName);
 
        AddCommandMenuItems(menuItems, resource, commandSelected, isCommandExecuting);
 
        if (showUrls)
        {
            AddUrlMenuItems(menuItems, resource);
        }
    }
 
    private void AddUrlMenuItems(List<MenuButtonItem> menuItems, ResourceViewModel resource)
    {
        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 void AddTelemetryMenuItems(List<MenuButtonItem> menuItems, ResourceViewModel resource, Func<ResourceViewModel, string> getResourceName)
    {
        // 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 void AddCommandMenuItems(List<MenuButtonItem> menuItems, ResourceViewModel resource, 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)
            };
        }
    }
}