Skip to content

Commit

Permalink
Commands glow up (#5516)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK authored Sep 6, 2024
1 parent 9a59d12 commit bc69e39
Show file tree
Hide file tree
Showing 31 changed files with 515 additions and 293 deletions.
44 changes: 28 additions & 16 deletions src/Aspire.Dashboard/Components/Controls/AspireMenuButton.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,37 @@
@using Aspire.Dashboard.Model
@using Microsoft.FluentUI.AspNetCore.Components.DesignTokens
@inherits FluentComponentBase
@typeparam TItem

<div class="aspire-menubutton-container" style="width:100px;">
<FluentButton Id="@_buttonId" Appearance="@ButtonAppearance" aria-haspopup="true" aria-expanded="@_visible" @onclick="ToggleMenu" @onkeydown="OnKeyDown" Disabled="@(!Items.Any())">
@if (string.IsNullOrWhiteSpace(Text))
<FluentButton Id="@MenuButtonId" Appearance="@ButtonAppearance" aria-haspopup="true" aria-expanded="@_visible" @onclick="ToggleMenu" @onkeydown="OnKeyDown" Disabled="@(!Items.Where(i => !i.IsDivider).Any())">
@if (string.IsNullOrWhiteSpace(Text))
{
<FluentIcon Value="@_icon" />
}
else
{
@Text
<FluentIcon Value="@_icon" Slot="end" />
}
</FluentButton>

<FluentMenu Anchor="@MenuButtonId" aria-labelledby="button" @bind-Open="@_visible" VerticalThreshold="200">
@foreach (var item in Items)
{
@if (item.IsDivider)
{
<FluentIcon Value="@_icon" />
<FluentDivider />
}
else
{
@Text
<FluentIcon Value="@_icon" Slot="end" />
}
</FluentButton>

<FluentMenu Anchor="@_buttonId" aria-labelledby="button" @bind-Open="@_visible" VerticalThreshold="200">
@foreach (TItem command in Items)
{
<FluentMenuItem OnClick="() => HandleItemClicked(command)">@ItemText(command)</FluentMenuItem>
<FluentMenuItem OnClick="() => HandleItemClicked(item)" title="@item.Tooltip">
@item.Text
@if (item.Icon != null)
{
<span slot="start">
<FluentIcon Value="@item.Icon" Style="vertical-align: middle;" />
</span>
}
</FluentMenuItem>
}
</FluentMenu>
</div>
}
</FluentMenu>
19 changes: 7 additions & 12 deletions src/Aspire.Dashboard/Components/Controls/AspireMenuButton.razor.cs
Original file line number Diff line number Diff line change
@@ -1,38 +1,33 @@
// 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.AspNetCore.Components.Web;
using Microsoft.FluentUI.AspNetCore.Components;

namespace Aspire.Dashboard.Components;

public partial class AspireMenuButton<TItem> : FluentComponentBase
public partial class AspireMenuButton : FluentComponentBase
{
private static readonly Icon s_defaultIcon = new Icons.Regular.Size24.ChevronDown();

private bool _visible;
private Icon? _icon;

private readonly string _buttonId = Identifier.NewId();

[Parameter]
public string? Text { get; set; }

[Parameter]
public Icon? Icon { get; set; }

[Parameter]
public required IList<TItem> Items { get; set; }

[Parameter]
public required Func<TItem, string> ItemText { get; set; }
public required IList<MenuButtonItem> Items { get; set; }

[Parameter]
public Appearance? ButtonAppearance { get; set; }

[Parameter]
public EventCallback<TItem> OnItemClicked { get; set; }
public string MenuButtonId { get; } = Identifier.NewId();

protected override void OnParametersSet()
{
Expand All @@ -44,11 +39,11 @@ private void ToggleMenu()
_visible = !_visible;
}

private async Task HandleItemClicked(TItem item)
private async Task HandleItemClicked(MenuButtonItem item)
{
if (item is not null)
if (item.OnClick is {} onClick)
{
await OnItemClicked.InvokeAsync(item);
await onClick();
}
_visible = false;
}
Expand Down
23 changes: 23 additions & 0 deletions src/Aspire.Dashboard/Components/Controls/ResourceActions.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@namespace Aspire.Dashboard.Components
@using Aspire.Dashboard.Model
@using Microsoft.FluentUI.AspNetCore.Components

@foreach (var highlightedCommand in Commands.Where(c => c.IsHighlighted))
{
<FluentButton Appearance="Appearance.Lightweight" Title="@(highlightedCommand.DisplayDescription ?? highlightedCommand.DisplayName)" OnClick="@(() => CommandSelected.InvokeAsync(highlightedCommand))">
@if (!string.IsNullOrEmpty(highlightedCommand.IconName) && CommandViewModel.ResolveIconName(highlightedCommand.IconName) is { } icon)
{
<FluentIcon Value="icon" />
}
else
{
@highlightedCommand.DisplayName
}
</FluentButton>
}

<AspireMenuButton
ButtonAppearance="Appearance.Lightweight"
Icon="@(new Icons.Regular.Size24.MoreHorizontal())"
Items="@_menuItems"
@ref="_menuButton" />
71 changes: 71 additions & 0 deletions src/Aspire.Dashboard/Components/Controls/ResourceActions.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// 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.Extensions.Localization;
using Microsoft.FluentUI.AspNetCore.Components;

namespace Aspire.Dashboard.Components;

public partial class ResourceActions : ComponentBase
{
private static readonly Icon s_viewDetailsIcon = new Icons.Regular.Size20.Info();
private static readonly Icon s_consoleLogsIcon = new Icons.Regular.Size20.SlideText();

private AspireMenuButton? _menuButton;

[Inject]
public required IStringLocalizer<Resources.Resources> Loc { get; set; }

[Parameter]
public required IList<CommandViewModel> Commands { get; set; }

[Parameter]
public required EventCallback<CommandViewModel> CommandSelected { get; set; }

[Parameter]
public required EventCallback<string> OnViewDetails { get; set; }

[Parameter]
public required EventCallback OnConsoleLogs { get; set; }

private readonly List<MenuButtonItem> _menuItems = new();

protected override void OnParametersSet()
{
_menuItems.Clear();

_menuItems.Add(new MenuButtonItem
{
Text = Loc[nameof(Resources.Resources.ResourceActionViewDetailsText)],
Icon = s_viewDetailsIcon,
OnClick = () => OnViewDetails.InvokeAsync(_menuButton?.MenuButtonId)
});
_menuItems.Add(new MenuButtonItem
{
Text = Loc[nameof(Resources.Resources.ResourceActionConsoleLogsText)],
Icon = s_consoleLogsIcon,
OnClick = OnConsoleLogs.InvokeAsync
});

var menuCommands = Commands.Where(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) && CommandViewModel.ResolveIconName(command.IconName) is { } i) ? i : null;

_menuItems.Add(new MenuButtonItem
{
Text = command.DisplayName,
Tooltip = command.DisplayDescription,
Icon = icon,
OnClick = () => CommandSelected.InvokeAsync(command)
});
}
}
}
}
10 changes: 0 additions & 10 deletions src/Aspire.Dashboard/Components/Controls/ResourceCommands.razor

This file was deleted.

16 changes: 0 additions & 16 deletions src/Aspire.Dashboard/Components/Controls/ResourceCommands.razor.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<div class="resource-details-layout">

<FluentToolbar Orientation="Orientation.Horizontal">
<FluentAnchor Appearance="Appearance.Lightweight" Href="@DashboardUrls.ConsoleLogsUrl(Resource?.Name)" slot="end">View logs</FluentAnchor>
<FluentAnchor Appearance="Appearance.Lightweight" Href="@DashboardUrls.ConsoleLogsUrl(Resource?.Name)" slot="end">@Loc[Resources.ResourceDetailsViewConsoleLogs]</FluentAnchor>

@if (ShowSpecOnlyToggle)
{
Expand Down
27 changes: 6 additions & 21 deletions src/Aspire.Dashboard/Components/Pages/Resources.razor
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,6 @@
SelectedValue="@SelectedResource"
ViewKey="ResourcesList">
<Summary>
@{
var gridTemplateColumns = HasResourcesWithCommands ? "1fr 1.5fr 1.25fr 1.5fr 2.5fr 2.5fr 1fr 1fr 1fr" : "1fr 1.5fr 1.25fr 1.5fr 2.5fr 2.5fr 1fr 1fr";
}
<FluentDataGrid ResizeLabel="@AspireFluentDataGridHeaderCell.GetResizeLabel(ControlsStringsLoc)"
ResizeType="DataGridResizeType.Discrete"
Virtualize="true"
Expand Down Expand Up @@ -113,24 +110,12 @@
DisplayedEndpoints="@GetDisplayedEndpoints(context, out var additionalMessage)"
AdditionalMessage="@additionalMessage" />
</AspireTemplateColumn>
<AspireTemplateColumn ColumnId="@LogsColumn" ColumnManager="@_manager" Title="@Loc[nameof(Dashboard.Resources.Resources.ResourcesLogsColumnHeader)]" Class="no-ellipsis">
<FluentAnchor Appearance="Appearance.Lightweight" Href="@DashboardUrls.ConsoleLogsUrl(resource: context.Name)" @onclick:stopPropagation="true">@ControlsStringsLoc[ControlsStrings.ViewAction]</FluentAnchor>
</AspireTemplateColumn>
<AspireTemplateColumn ColumnId="@DetailsColumn" ColumnManager="@_manager" Title="@Loc[nameof(Dashboard.Resources.Resources.ResourcesDetailsColumnHeader)]" Sortable="false" Class="no-ellipsis">
@{
var id = $"details-button-{context.Uid}";
}
<div @onclick:stopPropagation="true">
<FluentButton Appearance="Appearance.Lightweight"
Id="@id"
Title="@ControlsStringsLoc[nameof(ControlsStrings.ViewAction)]"
OnClick="@(() => ShowResourceDetailsAsync(context, id))">@ControlsStringsLoc[nameof(ControlsStrings.ViewAction)]</FluentButton>
</div>
</AspireTemplateColumn>
<AspireTemplateColumn ColumnId="@CommandsColumn" ColumnManager="@_manager" Title="@Loc[nameof(Dashboard.Resources.Resources.ResourcesCommandsColumnHeader)]">
<div @onclick:stopPropagation="true">
<ResourceCommands Commands="@context.Commands"
CommandSelected="async (command) => await ExecuteResourceCommandAsync(context, command)" />
<AspireTemplateColumn ColumnId="@ActionsColumn" ColumnManager="@_manager" Title="@Loc[nameof(Dashboard.Resources.Resources.ResourcesActionsColumnHeader)]">
<div style="display: inline-block;" @onclick:stopPropagation="true">
<ResourceActions Commands="@context.Commands"
CommandSelected="async (command) => await ExecuteResourceCommandAsync(context, command)"
OnViewDetails="@((buttonId) => ShowResourceDetailsAsync(context, buttonId))"
OnConsoleLogs="@(() => NavigationManager.NavigateTo(DashboardUrls.ConsoleLogsUrl(resource: context.Name)))" />
</div>
</AspireTemplateColumn>
</ChildContent>
Expand Down
10 changes: 2 additions & 8 deletions src/Aspire.Dashboard/Components/Pages/Resources.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ public partial class Resources : ComponentBase, IAsyncDisposable
private const string StartTimeColumn = nameof(StartTimeColumn);
private const string SourceColumn = nameof(SourceColumn);
private const string EndpointsColumn = nameof(EndpointsColumn);
private const string LogsColumn = nameof(LogsColumn);
private const string DetailsColumn = nameof(DetailsColumn);
private const string CommandsColumn = nameof(CommandsColumn);
private const string ActionsColumn = nameof(ActionsColumn);

private Subscription? _logsSubscription;
private Dictionary<OtlpApplication, int>? _applicationUnviewedErrorCounts;
Expand Down Expand Up @@ -139,8 +137,6 @@ static bool UnionWithKeys(ConcurrentDictionary<string, bool> left, ConcurrentDic
}
}

private bool HasResourcesWithCommands => _resourceByName.Any(r => r.Value.Commands.Any());

private IQueryable<ResourceViewModel>? FilteredResources => _resourceByName.Values.Where(Filter).OrderBy(e => e.ResourceType).ThenBy(e => e.Name).AsQueryable();

private readonly GridSort<ResourceViewModel> _nameSort = GridSort<ResourceViewModel>.ByAscending(p => p.Name);
Expand All @@ -156,9 +152,7 @@ protected override async Task OnInitializedAsync()
new GridColumn(Name: StartTimeColumn, DesktopWidth: "1.5fr"),
new GridColumn(Name: SourceColumn, DesktopWidth: "2.5fr"),
new GridColumn(Name: EndpointsColumn, DesktopWidth: "2.5fr", MobileWidth: "2fr"),
new GridColumn(Name: LogsColumn, DesktopWidth: "1fr"),
new GridColumn(Name: DetailsColumn, DesktopWidth: "1fr", MobileWidth: "1fr"),
new GridColumn(Name: CommandsColumn, DesktopWidth: "1fr", IsVisible: () => HasResourcesWithCommands)
new GridColumn(Name: ActionsColumn, DesktopWidth: "1.5fr", MobileWidth: "1fr")
], DimensionManager);

_applicationUnviewedErrorCounts = TelemetryRepository.GetApplicationUnviewedErrorLogsCount();
Expand Down
15 changes: 15 additions & 0 deletions src/Aspire.Dashboard/Model/MenuButtonItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.FluentUI.AspNetCore.Components;

namespace Aspire.Dashboard.Model;

public class MenuButtonItem
{
public bool IsDivider { get; set; }
public string? Text { get; set; }
public string? Tooltip { get; set; }
public Icon? Icon { get; set; }
public Func<Task>? OnClick { get; set; }
}
34 changes: 33 additions & 1 deletion src/Aspire.Dashboard/Model/ResourceViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Collections.Immutable;
using System.Diagnostics;
using Aspire.Dashboard.Extensions;
using Google.Protobuf.WellKnownTypes;
using Microsoft.FluentUI.AspNetCore.Components;

namespace Aspire.Dashboard.Model;

Expand Down Expand Up @@ -60,20 +62,50 @@ public static string GetResourceName(ResourceViewModel resource, IDictionary<str
[DebuggerDisplay("CommandType = {CommandType}, DisplayName = {DisplayName}")]
public sealed class CommandViewModel
{
private static readonly ConcurrentDictionary<string, CustomIcon?> s_iconCache = new();

public string CommandType { get; }
public string DisplayName { get; }
public string? DisplayDescription { get; }
public string? ConfirmationMessage { get; }
public Value? Parameter { get; }
public bool IsHighlighted { get; }
public string? IconName { get; }

public CommandViewModel(string commandType, string displayName, string? confirmationMessage, Value? parameter)
public CommandViewModel(string commandType, string displayName, string? displayDescription, string? confirmationMessage, Value? parameter, bool isHighlighted, string? iconName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(commandType);
ArgumentException.ThrowIfNullOrWhiteSpace(displayName);

CommandType = commandType;
DisplayName = displayName;
DisplayDescription = displayDescription;
ConfirmationMessage = confirmationMessage;
Parameter = parameter;
IsHighlighted = isHighlighted;
IconName = iconName;
}

public static CustomIcon? ResolveIconName(string iconName)
{
// Icons.GetInstance isn't efficent. Cache icon lookup.
return s_iconCache.GetOrAdd(iconName, static name =>
{
try
{
return Icons.GetInstance(new IconInfo
{
Name = name,
Variant = IconVariant.Regular,
Size = IconSize.Size20
});
}
catch
{
// Icon name couldn't be found.
return null;
}
});
}
}

Expand Down
Loading

0 comments on commit bc69e39

Please sign in to comment.