File: Pages\MetricsTests.cs
Web Access
Project: src\tests\Aspire.Dashboard.Components.Tests\Aspire.Dashboard.Components.Tests.csproj (Aspire.Dashboard.Components.Tests)
// 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.Diagnostics;
using System.Globalization;
using System.Web;
using Aspire.Dashboard.Components.Controls;
using Aspire.Dashboard.Components.Pages;
using Aspire.Dashboard.Components.Resize;
using Aspire.Dashboard.Components.Tests.Shared;
using Aspire.Dashboard.Extensions;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Model.MetricValues;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Dashboard.Tests.Shared;
using Aspire.Dashboard.Utils;
using Bunit;
using Google.Protobuf.Collections;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Time.Testing;
using Microsoft.FluentUI.AspNetCore.Components;
using OpenTelemetry.Proto.Metrics.V1;
using Aspire.Tests;
using Xunit;
using static Aspire.Dashboard.Components.Pages.Metrics;
using static Aspire.Tests.Shared.Telemetry.TelemetryTestHelpers;
 
namespace Aspire.Dashboard.Components.Tests.Pages;
 
[UseCulture("en-US")]
public partial class MetricsTests : DashboardTestContext
{
    private static readonly DateTime s_testTime = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
 
    [Theory]
    [InlineData(false, true)]
    [InlineData(true, false)]
    public async Task SignalActions_UseTelemetryRepositoryReadOnlyState(bool telemetryRepositoryIsReadOnly, bool dashboardClientIsReadOnly)
    {
        MetricsSetupHelpers.SetupMetricsPage(this);
        Services.AddSingleton<IDashboardClient>(new TestDashboardClient(isReadOnly: dashboardClientIsReadOnly));
        await FluentUISetupHelpers.ConfigureTelemetryRepository(this, telemetryRepositoryIsReadOnly, _ => Task.CompletedTask);
 
        var cut = RenderComponent<Metrics>(builder => builder.AddCascadingValue(new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false)));
 
        Assert.Equal(telemetryRepositoryIsReadOnly, cut.FindComponent<PauseIncomingDataSwitch>().Instance.Disabled);
        Assert.Equal(telemetryRepositoryIsReadOnly, cut.FindComponent<ClearSignalsButton>().FindComponent<AspireMenuButton>().Instance.Disabled);
    }
 
    [Fact]
    public async Task ChangeResource_MeterAndInstrumentOnNewResource_InstrumentSet()
    {
        await ChangeResourceAndAssertInstrument(
            app1InstrumentName: "test1",
            app2InstrumentName: "test1",
            expectedMeterNameAfterChange: "test-meter",
            expectedInstrumentNameAfterChange: "test1");
    }
 
    [Fact]
    public async Task ChangeResource_MeterAndInstrumentNotOnNewResources_InstrumentCleared()
    {
        await ChangeResourceAndAssertInstrument(
            app1InstrumentName: "test1",
            app2InstrumentName: "test2",
            expectedMeterNameAfterChange: null,
            expectedInstrumentNameAfterChange: null);
    }
 
    [Fact]
    public async Task ChartContainer_ParametersAndActiveView_OnlyRefreshAndRenderWhenChanged()
    {
        JSInterop.Mode = JSRuntimeMode.Loose;
        MetricsSetupHelpers.SetupMetricsPage(this);
        var chartTimeProvider = new TestTimeProvider { UtcNow = DateTimeOffset.UtcNow };
        Services.AddSingleton<BrowserTimeProvider>(chartTimeProvider);
 
        var telemetryRepository = Services.GetRequiredService<SqliteTelemetryRepository>();
        var metricTime = chartTimeProvider.GetUtcNow().UtcDateTime.AddMinutes(-1);
        await telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
        {
            new ResourceMetrics
            {
                Resource = CreateResource(name: "TestApp"),
                ScopeMetrics =
                {
                    new ScopeMetrics
                    {
                        Scope = CreateScope(name: "test-meter"),
                        Metrics =
                        {
                            CreateSumMetric(
                                metricName: "test-instrument",
                                startTime: metricTime,
                                attributes: [new KeyValuePair<string, string>("http.method", "GET")])
                        }
                    }
                }
            }
        });
 
        var resource = telemetryRepository.GetResources().Single();
 
        var cut = RenderComponent<ChartContainer>(builder =>
        {
            builder.Add(component => component.ResourceKey, resource.ResourceKey);
            builder.Add(component => component.MeterName, "test-meter");
            builder.Add(component => component.InstrumentName, "test-instrument");
            builder.Add(component => component.Duration, TimeSpan.FromMinutes(5));
            builder.Add(component => component.ActiveView, MetricViewKind.Graph);
            builder.Add(component => component.OnViewChangedAsync, _ => Task.CompletedTask);
            builder.Add(component => component.Resources, [resource]);
        });
        cut.WaitForAssertion(() =>
        {
            var dimensionFilter = Assert.Single(cut.Instance.DimensionFilters);
            Assert.Equal("GET", Assert.Single(dimensionFilter.SelectedValues).Value);
            Assert.Single(cut.FindComponents<PlotlyChart>());
            Assert.Empty(cut.FindComponents<MetricTable>());
        });
        var dimensionFilters = cut.Instance.DimensionFilters;
 
        var activities = new ConcurrentQueue<Activity>();
        using var listener = ActivityListenerHelper.Create(telemetryRepository.SqlActivitySource, onActivityStopped: activities.Enqueue);
 
        cut.SetParametersAndRender(builder => builder.Add(component => component.ActiveView, MetricViewKind.Table));
 
        Assert.Empty(cut.FindComponents<PlotlyChart>());
        Assert.Single(cut.FindComponents<MetricTable>());
        if (cut.FindAll(".empty-content-cell") is [var emptyContentCell])
        {
            Assert.Equal("Loading...", emptyContentCell.TextContent.Trim());
        }
        cut.WaitForAssertion(() => Assert.Contains("1Value increased", cut.FindAll("[role='gridcell']").Select(cell => cell.TextContent.Trim())));
        Assert.Empty(activities);
 
        cut.SetParametersAndRender(builder => builder.Add(component => component.ActiveView, MetricViewKind.Graph));
 
        Assert.Single(cut.FindComponents<PlotlyChart>());
        Assert.Empty(cut.FindComponents<MetricTable>());
        Assert.Empty(activities);
 
        await telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
        {
            new ResourceMetrics
            {
                Resource = CreateResource(name: "TestApp"),
                ScopeMetrics =
                {
                    new ScopeMetrics
                    {
                        Scope = CreateScope(name: "test-meter"),
                        Metrics =
                        {
                            CreateSumMetric(
                                metricName: "test-instrument",
                                startTime: metricTime,
                                attributes: [new KeyValuePair<string, string>("http.method", "POST")])
                        }
                    }
                }
            }
        });
        activities.Clear();
 
        cut.SetParametersAndRender(builder => builder.Add(component => component.Duration, TimeSpan.FromMinutes(5)));
 
        Assert.Empty(activities);
 
        cut.SetParametersAndRender(builder => builder.Add(component => component.Duration, TimeSpan.FromMinutes(1)));
 
        cut.WaitForAssertion(() =>
        {
            var updatedFilter = Assert.Single(cut.Instance.DimensionFilters);
            Assert.NotSame(dimensionFilters, cut.Instance.DimensionFilters);
            Assert.Collection(
                updatedFilter.SelectedValues.Select(value => value.Value).Order(),
                value => Assert.Equal("GET", value),
                value => Assert.Equal("POST", value));
 
            var chart = cut.FindComponent<PlotlyChart>();
            var dimensions = chart.Instance.InstrumentViewModel.MatchedDimensions!;
            Assert.Equal(2, dimensions.Count);
            Assert.All(dimensions, dimension => Assert.Equal(1, Assert.IsType<MetricValue<long>>(Assert.Single(dimension.Values)).Value));
            Assert.Contains(activities, activity => ((string?)activity.GetTagItem("db.query.text"))?.Contains("telemetry_metric_points", StringComparison.Ordinal) == true);
        });
    }
 
    [Fact]
    public async Task ChartContainer_InstrumentChanged_TableUpdates()
    {
        JSInterop.Mode = JSRuntimeMode.Loose;
        MetricsSetupHelpers.SetupMetricsPage(this);
        await FluentUISetupHelpers.ConfigureTelemetryRepository(this, readOnly: true, telemetryRepository => telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
        {
            new ResourceMetrics
            {
                Resource = CreateResource(name: "TestApp"),
                ScopeMetrics =
                {
                    new ScopeMetrics
                    {
                        Scope = CreateScope(name: "test-meter"),
                        Metrics =
                        {
                            CreateSumMetric(metricName: "instrument-1", startTime: s_testTime.AddMinutes(1), value: 0),
                            CreateSumMetric(metricName: "instrument-1", startTime: s_testTime.AddMinutes(2), value: 1),
                            CreateSumMetric(metricName: "instrument-2", startTime: s_testTime.AddMinutes(1), value: 0),
                            CreateSumMetric(metricName: "instrument-2", startTime: s_testTime.AddMinutes(2), value: 2)
                        }
                    }
                }
            }
        }));
 
        var telemetryRepository = Services.GetRequiredService<SqliteTelemetryRepository>();
        var resource = telemetryRepository.GetResources().Single();
        var cut = RenderComponent<ChartContainer>(builder =>
        {
            builder.Add(component => component.ResourceKey, resource.ResourceKey);
            builder.Add(component => component.MeterName, "test-meter");
            builder.Add(component => component.InstrumentName, "instrument-1");
            builder.Add(component => component.Duration, TimeSpan.FromMinutes(5));
            builder.Add(component => component.ActiveView, MetricViewKind.Table);
            builder.Add(component => component.OnViewChangedAsync, _ => Task.CompletedTask);
            builder.Add(component => component.Resources, [resource]);
        });
 
        cut.WaitForState(() => cut.FindComponents<MetricTable>().Count == 1);
        var table = cut.FindComponent<MetricTable>();
        table.Render();
        table.WaitForAssertion(() => Assert.Contains("1Value increased", table.FindAll("[role='gridcell']").Select(cell => cell.TextContent.Trim())));
 
        cut.SetParametersAndRender(builder => builder.Add(component => component.InstrumentName, "instrument-2"));
 
        cut.WaitForAssertion(() =>
        {
            var updatedTable = cut.FindComponent<MetricTable>();
            Assert.Equal("instrument-2", updatedTable.Instance.InstrumentViewModel.Instrument?.Name);
            Assert.NotEmpty(updatedTable.Instance.InstrumentViewModel.MatchedDimensions!);
        });
        cut.WaitForAssertion(() => Assert.Contains("2Value increased", cut.FindAll("[role='gridcell']").Select(cell => cell.TextContent.Trim())));
    }
 
    [Fact]
    public async Task ChartContainer_TickUpdate_PreservesComponentCultureAndStopsOnDispose()
    {
        JSInterop.Mode = JSRuntimeMode.Loose;
        MetricsSetupHelpers.SetupMetricsPage(this);
        var timeProvider = new SignalingFakeTimeProvider();
        Services.AddSingleton<TimeProvider>(timeProvider);
 
        var telemetryRepository = Services.GetRequiredService<SqliteTelemetryRepository>();
        var metric = CreateSumMetric(metricName: "test-instrument", startTime: DateTime.UtcNow);
        // End in the current SQLite query window while spanning the fixed browser time used by the rendered chart.
        metric.Sum.DataPoints.Single().StartTimeUnixNano = 0;
        await telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
        {
            new ResourceMetrics
            {
                Resource = CreateResource(name: "TestApp"),
                ScopeMetrics =
                {
                    new ScopeMetrics
                    {
                        Scope = CreateScope(name: "test-meter"),
                        Metrics =
                        {
                            metric
                        }
                    }
                }
            }
        });
        var resource = telemetryRepository.GetResources().Single();
 
        var activitySource = Services.GetRequiredService<DashboardActivitySource>();
        var tickActivityCount = 0;
        var activityStarted = new TaskCompletionSource<Activity>(TaskCreationOptions.RunContinuationsAsynchronously);
        var activityStopped = new TaskCompletionSource<Activity>(TaskCreationOptions.RunContinuationsAsynchronously);
        using var listener = ActivityListenerHelper.Create(
            activitySource.ActivitySource,
            onActivityStarted: activity =>
            {
                if (activity.OperationName == "Update metric chart data from tick")
                {
                    Interlocked.Increment(ref tickActivityCount);
                    activityStarted.TrySetResult(activity);
                }
            },
            onActivityStopped: activity =>
            {
                if (activity.OperationName == "Update metric chart data from tick")
                {
                    activityStopped.TrySetResult(activity);
                }
            });
 
        var componentCulture = (CultureInfo)CultureInfo.CurrentCulture.Clone();
        componentCulture.DateTimeFormat.AMDesignator = "am";
        var originalCulture = CultureInfo.CurrentCulture;
 
        try
        {
            CultureInfo.CurrentCulture = componentCulture;
            var cut = RenderComponent<ChartContainer>(builder =>
            {
                builder.Add(component => component.ResourceKey, resource.ResourceKey);
                builder.Add(component => component.MeterName, "test-meter");
                builder.Add(component => component.InstrumentName, "test-instrument");
                builder.Add(component => component.Duration, TimeSpan.FromSeconds(1));
                builder.Add(component => component.ActiveView, MetricViewKind.Table);
                builder.Add(component => component.OnViewChangedAsync, _ => Task.CompletedTask);
                builder.Add(component => component.Resources, [resource]);
            });
 
            cut.WaitForAssertion(() => Assert.Single(cut.FindComponents<MetricTable>()));
            await timeProvider.TimerCreated.Task.DefaultTimeout();
            timeProvider.Advance(TimeSpan.FromSeconds(1));
 
            var started = await activityStarted.Task.WaitAsync(DefaultWaitTimeout);
            var activity = await activityStopped.Task.WaitAsync(DefaultWaitTimeout);
 
            Assert.Same(started, activity);
            Assert.Equal(ActivityKind.Internal, activity.Kind);
            Assert.Null(activity.ParentId);
            cut.WaitForAssertion(() =>
            {
                Assert.Equal("12:59:57 am", cut.Find("[role='gridcell']").TextContent.Trim(), ignoreWhiteSpaceDifferences: true);
            });
 
            await cut.Instance.DisposeAsync().DefaultTimeout();
            var activityCountAfterDispose = Volatile.Read(ref tickActivityCount);
            timeProvider.Advance(TimeSpan.FromSeconds(10));
 
            Assert.Equal(activityCountAfterDispose, Volatile.Read(ref tickActivityCount));
        }
        finally
        {
            CultureInfo.CurrentCulture = originalCulture;
        }
    }
 
    [Fact]
    public async Task InitialLoad_SingleResource_RedirectToResource()
    {
        // Arrange
        MetricsSetupHelpers.SetupMetricsPage(this);
 
        var navigationManager = Services.GetRequiredService<NavigationManager>();
 
        var targetLocationTcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
        using var changeHandler = navigationManager.RegisterLocationChangingHandler(c =>
        {
            targetLocationTcs.SetResult(c.TargetLocation);
            return ValueTask.CompletedTask;
        });
 
        var telemetryRepository = Services.GetRequiredService<SqliteTelemetryRepository>();
        await telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
        {
            new ResourceMetrics
            {
                Resource = CreateResource(name: "TestApp"),
                ScopeMetrics =
                {
                    new ScopeMetrics
                    {
                        Scope = CreateScope(name: "test-meter"),
                        Metrics =
                        {
                            CreateSumMetric(metricName: "test-instrument", startTime: s_testTime.AddMinutes(1))
                        }
                    }
                }
            }
        });
 
        // Act
        var cut = RenderComponent<Metrics>(builder =>
        {
            builder.AddCascadingValue(new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
        });
 
        // Assert
        Assert.NotNull(targetLocationTcs);
        Assert.Equal("/metrics/resource/TestApp?duration=5", await targetLocationTcs.Task.DefaultTimeout());
    }
 
    [Fact]
    public async Task InitialLoad_HasSessionState_RedirectUsingState()
    {
        // Arrange
        var testSessionStorage = new TestSessionStorage
        {
            OnGetAsync = key =>
            {
                if (key == BrowserStorageKeys.MetricsPageState)
                {
                    var state = new MetricsPageState
                    {
                        ResourceName = "TestApp2",
                        MeterName = "test-meter",
                        InstrumentName = "test-instrument",
                        DurationMinutes = 720,
                        ViewKind = MetricViewKind.Table.ToString()
                    };
                    return (true, state);
                }
                else
                {
                    throw new InvalidOperationException("Unexpected key: " + key);
                }
            }
        };
 
        MetricsSetupHelpers.SetupMetricsPage(this, sessionStorage: testSessionStorage);
 
        var navigationManager = Services.GetRequiredService<NavigationManager>();
        navigationManager.NavigateTo(DashboardUrls.MetricsUrl());
 
        Uri? loadRedirect = null;
        navigationManager.LocationChanged += (s, a) =>
        {
            loadRedirect = new Uri(a.Location);
        };
 
        var telemetryRepository = Services.GetRequiredService<SqliteTelemetryRepository>();
        await telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
        {
            new ResourceMetrics
            {
                Resource = CreateResource(name: "TestApp"),
                ScopeMetrics =
                {
                    new ScopeMetrics
                    {
                        Scope = CreateScope(name: "test-meter"),
                        Metrics =
                        {
                            CreateSumMetric(metricName: "test-instrument", startTime: s_testTime.AddMinutes(1))
                        }
                    }
                }
            },
            new ResourceMetrics
            {
                Resource = CreateResource(name: "TestApp2"),
                ScopeMetrics =
                {
                    new ScopeMetrics
                    {
                        Scope = CreateScope(name: "test-meter"),
                        Metrics =
                        {
                            CreateSumMetric(metricName: "test-instrument", startTime: s_testTime.AddMinutes(1))
                        }
                    }
                }
            }
        });
 
        // Act
        var cut = RenderComponent<Metrics>(builder =>
        {
            builder.AddCascadingValue(new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
        });
 
        // Assert
        Assert.NotNull(loadRedirect);
        Assert.Equal("/metrics/resource/TestApp2", loadRedirect.AbsolutePath);
 
        var query = HttpUtility.ParseQueryString(loadRedirect.Query);
        Assert.Equal("test-meter", query["meter"]);
        Assert.Equal("test-instrument", query["instrument"]);
        Assert.Equal("720", query["duration"]);
        Assert.Equal(MetricViewKind.Table.ToString(), query["view"]);
    }
 
    [Fact]
    public async Task PauseIncomingData_DisplaysPauseWarningInPageFooter()
    {
        MetricsSetupHelpers.SetupMetricsPage(this);
 
        var telemetryRepository = Services.GetRequiredService<SqliteTelemetryRepository>();
        await telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
        {
            new ResourceMetrics
            {
                Resource = CreateResource(name: "TestApp"),
                ScopeMetrics =
                {
                    new ScopeMetrics
                    {
                        Scope = CreateScope(name: "test-meter"),
                        Metrics =
                        {
                            CreateSumMetric(metricName: "test-instrument", startTime: s_testTime.AddMinutes(1))
                        }
                    }
                }
            }
        });
 
        var navigationManager = Services.GetRequiredService<NavigationManager>();
        navigationManager.NavigateTo(DashboardUrls.MetricsUrl(resource: "TestApp", meter: "test-meter", instrument: "test-instrument"));
 
        var viewport = new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false);
        var cut = RenderComponent<Metrics>(builder =>
        {
            builder.Add(m => m.ResourceName, "TestApp");
            builder.AddCascadingValue(viewport);
        });
 
        cut.WaitForState(() => cut.Instance.PageViewModel.SelectedInstrument is not null);
 
        var pauseManager = Services.GetRequiredService<PauseManager>();
        var timeProvider = Services.GetRequiredService<BrowserTimeProvider>();
        var loc = Services.GetRequiredService<IStringLocalizer<Resources.Metrics>>();
 
        cut.FindComponent<PauseIncomingDataSwitch>().WaitForElement("fluent-button").Click();
 
        cut.WaitForAssertion(() =>
        {
            Assert.True(pauseManager.AreMetricsPaused(out var pausedAt));
            var pauseWarning = cut.FindComponent<PauseWarning>();
            Assert.Equal(
                string.Format(
                    CultureInfo.CurrentCulture,
                    loc[nameof(Resources.Metrics.PauseInProgressText)],
                    FormatHelpers.FormatTimeWithOptionalDate(timeProvider, pausedAt.Value)),
                pauseWarning.Instance.PauseText);
            Assert.Single(cut.Find("footer").QuerySelectorAll(".block-warning"));
        });
 
        cut.FindComponent<PauseIncomingDataSwitch>().WaitForElement("fluent-button").Click();
 
        cut.WaitForAssertion(() =>
        {
            Assert.False(pauseManager.AreMetricsPaused(out _));
            Assert.False(cut.HasComponent<PauseWarning>());
        });
    }
 
    [Fact]
    public async Task MetricsTree_MetricsAdded_TreeUpdated()
    {
        // Arrange
        MetricsSetupHelpers.SetupMetricsPage(this);
 
        var telemetryRepository = Services.GetRequiredService<SqliteTelemetryRepository>();
        await telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
        {
            new ResourceMetrics
            {
                Resource = CreateResource(name: "TestApp"),
                ScopeMetrics =
                {
                    new ScopeMetrics
                    {
                        Scope = CreateScope(name: "test-meter1"),
                        Metrics =
                        {
                            CreateSumMetric(metricName: "test-instrument1-1", startTime: s_testTime.AddMinutes(1))
                        }
                    }
                }
            }
        });
 
        // Act 1
        // Initial page load
        var cut = RenderComponent<Metrics>(builder =>
        {
            builder.AddCascadingValue(new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
            builder.Add(m => m.ResourceName, "TestApp");
        });
 
        // Assert 2
        cut.WaitForState(() => cut.Instance.PageViewModel.Instruments?.Count == 1);
 
        cut.WaitForAssertion(() =>
        {
            // Assert in wait to make sure rendering has caught up to data update.
            var tree1 = cut.FindComponent<FluentTreeView>();
            var items1 = tree1.FindComponents<FluentTreeItem>();
 
            foreach (var instrument in cut.Instance.PageViewModel.Instruments!)
            {
                Assert.Single(items1, i => i.Instance.Data as OtlpInstrumentSummary == instrument);
                Assert.Single(items1, i => i.Instance.Data as string == instrument.Parent.Name);
            }
        });
 
        // Act 2
        // New instruments added
        await telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
        {
            new ResourceMetrics
            {
                Resource = CreateResource(name: "TestApp"),
                ScopeMetrics =
                {
                    new ScopeMetrics
                    {
                        Scope = CreateScope(name: "test-meter1"),
                        Metrics =
                        {
                            CreateSumMetric(metricName: "test-instrument1-2", startTime: s_testTime.AddMinutes(1))
                        }
                    },
                    new ScopeMetrics
                    {
                        Scope = CreateScope(name: "test-meter2"),
                        Metrics =
                        {
                            CreateSumMetric(metricName: "test-instrument2-1", startTime: s_testTime.AddMinutes(1))
                        }
                    }
                }
            }
        });
 
        // Assert 2
        cut.WaitForState(() => cut.Instance.PageViewModel.Instruments?.Count == 3);
 
        cut.WaitForAssertion(() =>
        {
            // Assert in wait to make sure rendering has caught up to data update.
            var tree2 = cut.FindComponent<FluentTreeView>();
            var items2 = tree2.FindComponents<FluentTreeItem>();
 
            foreach (var instrument in cut.Instance.PageViewModel.Instruments!)
            {
                Assert.Single(items2, i => i.Instance.Data as OtlpInstrumentSummary == instrument);
                Assert.Single(items2, i => i.Instance.Data as string == instrument.Parent.Name);
            }
        });
    }
 
    [Fact]
    public async Task ReadOnly_ChartEndsAtLatestMetricTime()
    {
        MetricsSetupHelpers.SetupMetricsPage(this);
 
        await FluentUISetupHelpers.ConfigureTelemetryRepository(this, readOnly: true, telemetryRepository => telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
        {
            new ResourceMetrics
            {
                Resource = CreateResource(name: "TestApp"),
                ScopeMetrics =
                {
                    new ScopeMetrics
                    {
                        Scope = CreateScope(name: "test-meter"),
                        Metrics =
                        {
                            CreateSumMetric(metricName: "test-instrument", startTime: s_testTime.AddMinutes(1)),
                            CreateSumMetric(metricName: "test-instrument", startTime: s_testTime.AddMinutes(2))
                        }
                    }
                }
            }
        }));
        Services.GetRequiredService<PauseManager>().SetMetricsPaused(true);
        Services.GetRequiredService<NavigationManager>().NavigateTo(
            DashboardUrls.MetricsUrl(resource: "TestApp", meter: "test-meter", instrument: "test-instrument", duration: 5, view: MetricViewKind.Graph.ToString()));
 
        var cut = RenderComponent<Metrics>(builder =>
        {
            builder.Add(m => m.ResourceName, "TestApp");
            builder.AddCascadingValue(new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
        });
 
        cut.WaitForAssertion(() => Assert.Single(cut.FindComponents<PlotlyChart>()));
        var chart = cut.FindComponent<PlotlyChart>().Instance;
        var expectedEndTime = new DateTimeOffset(s_testTime.AddMinutes(2));
        Assert.Equal(expectedEndTime, chart.DataEndTime);
        cut.WaitForAssertion(() =>
        {
            var initializeChart = Assert.Single(JSInterop.Invocations, invocation => invocation.Identifier == "initializeChart");
            Assert.Equal(chart.TimeProvider.ToLocal(expectedEndTime), Assert.IsType<DateTime>(initializeChart.Arguments[3]));
        });
    }
 
    private async Task ChangeResourceAndAssertInstrument(string app1InstrumentName, string app2InstrumentName, string? expectedMeterNameAfterChange, string? expectedInstrumentNameAfterChange)
    {
        // Arrange
        MetricsSetupHelpers.SetupMetricsPage(this);
 
        var navigationManager = Services.GetRequiredService<NavigationManager>();
        navigationManager.NavigateTo(DashboardUrls.MetricsUrl(resource: "TestApp", meter: "test-meter", instrument: app1InstrumentName, duration: 720, view: MetricViewKind.Table.ToString()));
 
        var telemetryRepository = Services.GetRequiredService<SqliteTelemetryRepository>();
        await telemetryRepository.AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
        {
            new ResourceMetrics
            {
                Resource = CreateResource(name: "TestApp"),
                ScopeMetrics =
                {
                    new ScopeMetrics
                    {
                        Scope = CreateScope(name: "test-meter"),
                        Metrics =
                        {
                            CreateSumMetric(metricName: app1InstrumentName, startTime: s_testTime.AddMinutes(1))
                        }
                    }
                }
            },
            new ResourceMetrics
            {
                Resource = CreateResource(name: "TestApp2"),
                ScopeMetrics =
                {
                    new ScopeMetrics
                    {
                        Scope = CreateScope(name: "test-meter"),
                        Metrics =
                        {
                            CreateSumMetric(metricName: app2InstrumentName, startTime: s_testTime.AddMinutes(1))
                        }
                    }
                }
            }
        });
 
        // Act 1
        var cut = RenderComponent<Metrics>(builder =>
        {
            builder.Add(m => m.ResourceName, "TestApp");
            builder.AddCascadingValue(new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
        });
 
        navigationManager.LocationChanged += (sender, e) =>
        {
            var expectedUrl = DashboardUrls.MetricsUrl(resource: "TestApp2", meter: expectedMeterNameAfterChange, instrument: expectedInstrumentNameAfterChange, duration: 720, view: "Table");
            Assert.EndsWith(expectedUrl, e.Location);
 
            cut.SetParametersAndRender(builder =>
            {
                builder.Add(m => m.ResourceName, "TestApp2");
            });
        };
 
        var viewModel = cut.Instance.PageViewModel;
 
        // Assert 1
        Assert.Equal("test-meter", viewModel.SelectedMeter);
        Assert.Equal(app1InstrumentName, viewModel.SelectedInstrument!.Name);
 
        // Act 2
        var resourceSelect = cut.FindComponent<ResourceSelect>();
        var innerSelect = resourceSelect.Find("fluent-select");
        innerSelect.Change("TestApp2");
 
        cut.WaitForAssertion(() => Assert.Equal("TestApp2", viewModel.SelectedResource.Name));
 
        Assert.Equal(expectedInstrumentNameAfterChange, viewModel.SelectedInstrument?.Name);
        Assert.Equal(expectedMeterNameAfterChange, viewModel.SelectedMeter);
 
        Assert.Equal(MetricViewKind.Table, viewModel.SelectedViewKind);
        Assert.Equal(TimeSpan.FromMinutes(720), viewModel.SelectedDuration.Id);
    }
 
    private sealed class SignalingFakeTimeProvider : FakeTimeProvider
    {
        public TaskCompletionSource TimerCreated { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
 
        public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
        {
            var timer = base.CreateTimer(callback, state, dueTime, period);
            TimerCreated.TrySetResult();
            return timer;
        }
    }
}