diff --git a/src/Aspire.Dashboard/Components/Controls/Chart/MetricTable.razor b/src/Aspire.Dashboard/Components/Controls/Chart/MetricTable.razor index ac621677f47..30f6b303892 100644 --- a/src/Aspire.Dashboard/Components/Controls/Chart/MetricTable.razor +++ b/src/Aspire.Dashboard/Components/Controls/Chart/MetricTable.razor @@ -21,8 +21,6 @@
@* ItemKey is to preserve row focus by associating rows with their associated time *@ + @if (context is HistogramMetricView histogramMetric) { var percentileData = histogramMetric.Percentiles[percentile]; @@ -51,13 +49,13 @@ } } - + } } else if (_metrics.Values.All(value => value is MetricValueView)) { - + @{ var metricValueView = context as MetricValueView; } @@ -78,11 +76,11 @@ } } - + } @if (_exemplars.Count > 0) { - + @if (context.Exemplars.Count > 0) { @* min-width ensures a consistent button width up to 999 metrics *@ @@ -95,7 +93,7 @@ { 0 } - + } diff --git a/src/Aspire.Dashboard/Components/Dialogs/ExemplarsDialog.razor b/src/Aspire.Dashboard/Components/Dialogs/ExemplarsDialog.razor index 8f6a3762888..fd1ed52a441 100644 --- a/src/Aspire.Dashboard/Components/Dialogs/ExemplarsDialog.razor +++ b/src/Aspire.Dashboard/Components/Dialogs/ExemplarsDialog.razor @@ -14,27 +14,25 @@ @inject IStringLocalizer ControlsStringsLoc
- - + @GetTitle(context) - - + + @FormatHelpers.FormatTimeWithOptionalDate(TimeProvider, TimeProvider.ToLocal(context.Start), MillisecondsDisplay.Truncated) - - + + @FormatMetricValue(context.Value) - - + + View - +  @Loc[nameof(ControlsStrings.MetricTableNoMetricsFound)] diff --git a/src/Aspire.Dashboard/Components/Pages/Metrics.razor b/src/Aspire.Dashboard/Components/Pages/Metrics.razor index 649a49e15cf..f9dc4508a84 100644 --- a/src/Aspire.Dashboard/Components/Pages/Metrics.razor +++ b/src/Aspire.Dashboard/Components/Pages/Metrics.razor @@ -91,7 +91,7 @@ TGridItem="OtlpInstrumentSummary"> - + @context.Name diff --git a/src/Aspire.Dashboard/Components/Pages/Metrics.razor.cs b/src/Aspire.Dashboard/Components/Pages/Metrics.razor.cs index 8abd1aafb88..bb94c0ec403 100644 --- a/src/Aspire.Dashboard/Components/Pages/Metrics.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/Metrics.razor.cs @@ -43,7 +43,7 @@ public partial class Metrics : IDisposable, IPageWithSessionAndUrlState { for (let entry of entries) { - Plotly.Plots.resize(entry.target); + // Don't resize if not visible. + var display = window.getComputedStyle(entry.target).display; + var isHidden = !display || display === "none"; + if (!isHidden) { + Plotly.Plots.resize(entry.target); + } } }); plot.then(plotyDiv => { diff --git a/tests/Aspire.Dashboard.Components.Tests/Pages/MetricsTests.cs b/tests/Aspire.Dashboard.Components.Tests/Pages/MetricsTests.cs index 75d3787baba..8206136db4a 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Pages/MetricsTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Pages/MetricsTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Web; using Aspire.Dashboard.Components.Controls; using Aspire.Dashboard.Components.Pages; using Aspire.Dashboard.Components.Resize; @@ -14,6 +15,7 @@ using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Proto.Metrics.V1; using Xunit; +using static Aspire.Dashboard.Components.Pages.Metrics; using static Aspire.Tests.Shared.Telemetry.TelemetryTestHelpers; namespace Aspire.Dashboard.Components.Tests.Pages; @@ -41,13 +43,87 @@ public void ChangeResource_MeterAndInstrumentNotOnNewResources_InstrumentCleared expectedInstrumentNameAfterChange: null); } + [Fact] + public void InitialLoad_HasSessionState_RedirectUsingState() + { + // Arrange + var testSessionStorage = new TestSessionStorage + { + OnGetAsync = key => + { + if (key == BrowserStorageKeys.MetricsPageState) + { + var state = new MetricsPageState + { + ApplicationName = "TestApp", + 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.NavigateTo(DashboardUrls.MetricsUrl()); + + Uri? loadRedirect = null; + navigationManager.LocationChanged += (s, a) => + { + loadRedirect = new Uri(a.Location); + }; + + var telemetryRepository = Services.GetRequiredService(); + telemetryRepository.AddMetrics(new AddContext(), new RepeatedField + { + 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(builder => + { + builder.AddCascadingValue(new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false)); + }); + + // Assert + Assert.NotNull(loadRedirect); + Assert.Equal("/metrics/resource/TestApp", 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"]); + } + private void ChangeResourceAndAssertInstrument(string app1InstrumentName, string app2InstrumentName, string? expectedInstrumentNameAfterChange) { // Arrange MetricsSetupHelpers.SetupMetricsPage(this); var navigationManager = Services.GetRequiredService(); - navigationManager.NavigateTo(DashboardUrls.MetricsUrl(resource: "TestApp", meter: "test-meter", instrument: app1InstrumentName)); + navigationManager.NavigateTo(DashboardUrls.MetricsUrl(resource: "TestApp", meter: "test-meter", instrument: app1InstrumentName, duration: 720, view: MetricViewKind.Table.ToString())); var telemetryRepository = Services.GetRequiredService(); telemetryRepository.AddMetrics(new AddContext(), new RepeatedField @@ -110,5 +186,8 @@ private void ChangeResourceAndAssertInstrument(string app1InstrumentName, string // Meter is cleared if instrument is cleared. Assert.Equal("test-meter", viewModel.SelectedMeter!.MeterName); } + + Assert.Equal(MetricViewKind.Table, viewModel.SelectedViewKind); + Assert.Equal(TimeSpan.FromMinutes(720), viewModel.SelectedDuration.Id); } } diff --git a/tests/Aspire.Dashboard.Components.Tests/Shared/MetricsSetupHelpers.cs b/tests/Aspire.Dashboard.Components.Tests/Shared/MetricsSetupHelpers.cs index dc9421bda71..3d59aa1065d 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Shared/MetricsSetupHelpers.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Shared/MetricsSetupHelpers.cs @@ -47,7 +47,7 @@ internal static void SetupPlotlyChart(TestContext context) context.Services.AddSingleton(); } - internal static void SetupMetricsPage(TestContext context) + internal static void SetupMetricsPage(TestContext context, ISessionStorage? sessionStorage = null) { var version = typeof(FluentMain).Assembly.GetName().Version!; @@ -78,7 +78,7 @@ internal static void SetupMetricsPage(TestContext context) context.Services.AddSingleton(); context.Services.AddSingleton(); context.Services.AddSingleton(); - context.Services.AddSingleton(); + context.Services.AddSingleton(sessionStorage ?? new TestSessionStorage()); context.Services.AddSingleton(); context.Services.AddSingleton(); context.Services.AddSingleton(); diff --git a/tests/Aspire.Dashboard.Components.Tests/Shared/TestSessionStorage.cs b/tests/Aspire.Dashboard.Components.Tests/Shared/TestSessionStorage.cs index 7b31cc7845d..9f01691d77f 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Shared/TestSessionStorage.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Shared/TestSessionStorage.cs @@ -7,13 +7,27 @@ namespace Aspire.Dashboard.Components.Tests.Shared; public sealed class TestSessionStorage : ISessionStorage { + public Func? OnGetAsync { get; set; } + public Action? OnSetAsync { get; set; } + public Task> GetAsync(string key) { + if (OnGetAsync is { } callback) + { + var (success, value) = callback(key); + return Task.FromResult(new StorageResult(Success: success, Value: (T)(value ?? default(T))!)); + } + return Task.FromResult>(new StorageResult(Success: false, Value: default)); } public Task SetAsync(string key, T value) { + if (OnSetAsync is { } callback) + { + callback(key, value); + } + return Task.CompletedTask; } }