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;
}
}