File: Components\Controls\MetricGroupList.razor.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.Pages;
using Aspire.Dashboard.Model;
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons;
using MetricsResources = Aspire.Dashboard.Resources.Metrics;
 
namespace Aspire.Dashboard.Components.Controls;
 
/// <summary>
/// Lists the metric groups a resource reports: the user's pinned instruments, then one group per meter.
/// Selecting a group shows its instruments as cards; a card opens the instrument's chart.
/// </summary>
public partial class MetricGroupList : IDisposable
{
    private static readonly Icon s_pinnedIcon = new Icons.Filled.Size16.Pin();
    private static readonly Icon s_meterIcon = new Icons.Regular.Size16.ChartMultiple();
    // Fluent UI has no 16px AppsList, so the "all" row draws the 20px one at 16px, like the resource list's "All resources" row.
    private static readonly Icon s_allIcon = new Icons.Regular.Size20.AppsList();
 
    [Parameter, EditorRequired]
    public required Metrics.MetricsViewModel PageViewModel { get; set; }
 
    [Parameter, EditorRequired]
    public required Func<Metrics.MetricGroupKind, string?, Task> SelectGroupAsync { get; set; }
 
    [Parameter, EditorRequired]
    public required Func<Metrics.MetricGroupKind, string?, string> GetGroupUrl { get; set; }
 
    /// <summary>
    /// Raised when the filter text changes, so the page can filter the selected group's cards too.
    /// </summary>
    [Parameter]
    public EventCallback TextFilterChanged { get; set; }
 
    [Parameter]
    public bool IncludeLabel { get; set; }
 
    [Inject]
    public required MetricPinStore PinStore { get; init; }
 
    protected override void OnInitialized()
    {
        PinStore.Changed += OnPinsChanged;
    }
 
    private void OnPinsChanged()
    {
        _ = InvokeAsync(StateHasChanged);
    }
 
    public void OnResourceChanged()
    {
        StateHasChanged();
    }
 
    private Task OnTextFilterChanged() => TextFilterChanged.InvokeAsync();
 
    // The instruments the "all" row would show: every one of the resource's that matches the filter.
    private int GetAllCount() => PageViewModel.Instruments!.Count(i => MetricFilter.Matches(i, PageViewModel.TextFilter));
 
    private List<MetricGroup> GetGroups()
    {
        var instruments = PageViewModel.Instruments!;
        var filter = PageViewModel.TextFilter;
        var groups = new List<MetricGroup>();
 
        // Pins are stored by meter and instrument name across resources, so count this resource's instruments
        // that are pinned rather than the stored pins.
        var pinnedCount = instruments.Count(i => MetricFilter.Matches(i, filter) && PinStore.IsPinned(i.GetKey()));
 
        // Keep the pinned group while it's selected, even with no matches, so it doesn't disappear under the user.
        if (pinnedCount > 0 || PageViewModel.ShowPinned)
        {
            groups.Add(new MetricGroup(Name: Loc[nameof(MetricsResources.MetricsPinnedHeader)], MeterName: null, IsPinned: true, InstrumentCount: pinnedCount));
        }
 
        foreach (var meterGroup in instruments.GroupBy(i => i.Parent.Name).OrderBy(g => g.Key, StringComparer.Ordinal))
        {
            var count = meterGroup.Count(i => MetricFilter.Matches(i, filter));
            if (count > 0)
            {
                groups.Add(new MetricGroup(Name: meterGroup.Key, MeterName: meterGroup.Key, IsPinned: false, InstrumentCount: count));
            }
        }
 
        return groups;
    }
 
    private bool IsSelected(MetricGroup group)
    {
        // Filter results and the all-instruments view span every group, so none of them is the current one.
        if (PageViewModel.ShowFilterResults || PageViewModel.ShowAll)
        {
            return false;
        }
 
        return group.IsPinned
            ? PageViewModel.ShowPinned
            // The meter stays selected while one of its instruments is open, to show where the chart belongs
            // and to let the user go back to the meter's cards.
            : !PageViewModel.ShowPinned && string.Equals(PageViewModel.SelectedMeter, group.MeterName, StringComparison.Ordinal);
    }
 
    public void Dispose()
    {
        PinStore.Changed -= OnPinsChanged;
    }
 
    private sealed record MetricGroup(string Name, string? MeterName, bool IsPinned, int InstrumentCount)
    {
        public string Key => IsPinned ? "pinned" : $"meter:{MeterName}";
    }
}