File: Components\Controls\Chart\MeterOverview.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.Otlp.Model;
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
using Microsoft.JSInterop;
using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons;
using MetricsResources = Aspire.Dashboard.Resources.Metrics;
 
namespace Aspire.Dashboard.Components;
 
/// <summary>
/// Shows instruments as cards with their latest value and a live sparkline,
/// so the user can scan a group at a glance and jump to the instrument worth investigating.
/// </summary>
public partial class MeterOverview : LiveSparklinesBase
{
    private static readonly Icon s_gaugeIcon = new Icons.Regular.Size16.Gauge();
    private static readonly Icon s_counterIcon = new Icons.Regular.Size16.DataTrending();
    private static readonly Icon s_histogramIcon = new Icons.Regular.Size16.DataHistogram();
 
    /// <summary>
    /// Gets or sets whether cards show their meter name, for groups mixing instruments from several meters.
    /// </summary>
    [Parameter, EditorRequired]
    public required bool ShowMeterName { get; set; }
 
    [Parameter, EditorRequired]
    public required Func<OtlpInstrumentSummary, string> GetInstrumentUrl { get; set; }
 
    /// <summary>
    /// Gets or sets the resource label shown as a pill on each card. Null hides it, for lists of a single resource.
    /// </summary>
    [Parameter]
    public Func<OtlpInstrumentSummary, string>? GetResourceLabel { get; set; }
 
    /// <summary>
    /// Gets or sets how cards are ordered. Null keeps the order of <see cref="LiveSparklinesBase.Instruments"/>.
    /// Cards without data always go last.
    /// </summary>
    [Parameter]
    public MetricSortBy? SortBy { get; set; }
 
    /// <summary>
    /// Gets or sets the most cards to render, after ordering. Null renders every instrument.
    /// </summary>
    [Parameter]
    public int? MaxCount { get; set; }
 
    [Inject]
    public required IJSRuntime JS { get; init; }
 
    private ElementReference _container;
    private IJSObjectReference? _jsModule;
    private IJSObjectReference? _cardObserver;
    private DotNetObjectReference<MeterOverview>? _selfReference;
 
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender)
        {
            return;
        }
 
        // The browser reports which cards are near the viewport, so only those keep refreshing. Without the report,
        // for example when the module fails to load, every card keeps refreshing.
        try
        {
            _jsModule = await JS.InvokeAsync<IJSObjectReference>("import", $"/{Assets["Components/Controls/Chart/MeterOverview.razor.js"]}");
            _selfReference = DotNetObjectReference.Create(this);
            _cardObserver = await _jsModule.InvokeAsync<IJSObjectReference>("observeCards", _container, _selfReference);
        }
        catch (JSDisconnectedException)
        {
        }
    }
 
    [JSInvokable]
    public Task OnVisibleCardsChanged(string[] instrumentIds) => SetVisibleInstrumentsAsync(instrumentIds);
 
    protected override async ValueTask DisposeCoreAsync()
    {
        try
        {
            if (_cardObserver is not null)
            {
                await _cardObserver.InvokeVoidAsync("dispose");
                await _cardObserver.DisposeAsync();
            }
            if (_jsModule is not null)
            {
                await _jsModule.DisposeAsync();
            }
        }
        catch (JSDisconnectedException)
        {
            // The circuit is gone, and the browser's observer with it.
        }
 
        _selfReference?.Dispose();
    }
 
    private IEnumerable<OtlpInstrumentSummary> GetOrderedInstruments()
    {
        // OrderBy is stable, so each key keeps the order of the keys before it.
        var ordered = Instruments.OrderBy(i => GetSparkline(i).LatestValue is null);
        string ResourceLabel(OtlpInstrumentSummary i) => GetResourceLabel?.Invoke(i) ?? string.Empty;
 
        IEnumerable<OtlpInstrumentSummary> sorted = SortBy switch
        {
            MetricSortBy.Instrument => ordered.ThenBy(i => i.Name, StringComparer.OrdinalIgnoreCase).ThenBy(ResourceLabel, StringComparer.OrdinalIgnoreCase),
            MetricSortBy.Resource => ordered.ThenBy(ResourceLabel, StringComparer.OrdinalIgnoreCase).ThenBy(i => i.Name, StringComparer.OrdinalIgnoreCase),
            // Values of different units aren't comparable, but within a group they usually are, e.g. request counts.
            MetricSortBy.ValueDescending => ordered.ThenByDescending(i => GetSparkline(i).LatestValue).ThenBy(i => i.Name, StringComparer.OrdinalIgnoreCase),
            MetricSortBy.ValueAscending => ordered.ThenBy(i => GetSparkline(i).LatestValue).ThenBy(i => i.Name, StringComparer.OrdinalIgnoreCase),
            _ => ordered
        };
 
        return MaxCount is { } maxCount ? sorted.Take(maxCount) : sorted;
    }
 
    private string? GetInstrumentTypeName(OtlpInstrumentType type) => type switch
    {
        OtlpInstrumentType.Gauge => Loc[nameof(MetricsResources.MetricsInstrumentTypeGauge)].Value,
        OtlpInstrumentType.Sum => Loc[nameof(MetricsResources.MetricsInstrumentTypeCounter)].Value,
        OtlpInstrumentType.Histogram => Loc[nameof(MetricsResources.MetricsInstrumentTypeHistogram)].Value,
        _ => null
    };
 
    private static Icon? GetInstrumentIcon(OtlpInstrumentType type)
    {
        return type switch
        {
            OtlpInstrumentType.Gauge => s_gaugeIcon,
            OtlpInstrumentType.Sum => s_counterIcon,
            OtlpInstrumentType.Histogram => s_histogramIcon,
            _ => null
        };
    }
}
 
public enum MetricSortBy
{
    Instrument = 0,
    Resource = 1,
    ValueDescending = 3,
    ValueAscending = 4
}
 
public enum MetricGroupBy
{
    None,
    Resource,
    Meter,
    Instrument
}