Skip to content

Commit

Permalink
Preserve selected instrument when switching resources (#5083)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK committed Aug 2, 2024
1 parent 1499990 commit ceb5d5a
Show file tree
Hide file tree
Showing 20 changed files with 274 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
OptionDisabled="@(c => c!.Id?.Type is Otlp.Model.OtlpApplicationType.ResourceGrouping)"
SelectedOption="SelectedResource"
SelectedOptionChanged="SelectedResourceChanged"
ValueChanged="ValuedChanged"
AriaLabel="@AriaLabel"
Class="resource-list"
Height="@GetPopupHeight()">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ public partial class ResourceSelect

private FluentSelect<SelectViewModel<ResourceTypeDetails>>? _resourceSelectComponent;

private static void ValuedChanged(string value)
{
// Do nothing. Required for bunit change to trigger SelectedOptionChanged.
}

/// <summary>
/// Workaround for issue in fluent-select web component where the display value of the
/// selected item doesn't update automatically when the item changes.
Expand Down
35 changes: 29 additions & 6 deletions src/Aspire.Dashboard/Components/Pages/Metrics.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,6 @@ public partial class Metrics : IDisposable, IPageWithSessionAndUrlState<Metrics.
[Inject]
public required TelemetryRepository TelemetryRepository { get; init; }

[Inject]
public required TracesViewModel TracesViewModel { get; init; }

[Inject]
public required ILogger<Metrics> Logger { get; init; }

Expand Down Expand Up @@ -102,7 +99,6 @@ protected override Task OnInitializedAsync()
protected override async Task OnParametersSetAsync()
{
await this.InitializeViewModelAsync();
TracesViewModel.ApplicationKey = PageViewModel.SelectedApplication.Id?.GetApplicationKey();
UpdateSubscription();
}

Expand Down Expand Up @@ -154,11 +150,38 @@ private void UpdateApplications()

private Task HandleSelectedApplicationChangedAsync()
{
PageViewModel.SelectedMeter = null;
PageViewModel.SelectedInstrument = null;
// The new resource might not have the currently selected meter/instrument.
// Check whether the new resource has the current values or not, and clear if they're not available.
if (PageViewModel.SelectedMeter != null ||
PageViewModel.SelectedInstrument != null)
{
var selectedInstance = PageViewModel.SelectedApplication.Id?.GetApplicationKey();
var instruments = selectedInstance != null ? TelemetryRepository.GetInstrumentsSummary(selectedInstance.Value) : null;

if (instruments == null || ShouldClearSelectedMetrics(instruments))
{
PageViewModel.SelectedMeter = null;
PageViewModel.SelectedInstrument = null;
}
}

return this.AfterViewModelChangedAsync(_contentLayout, isChangeInToolbar: true);
}

private bool ShouldClearSelectedMetrics(List<OtlpInstrument> instruments)
{
if (PageViewModel.SelectedMeter != null && !instruments.Any(i => i.Parent.MeterName == PageViewModel.SelectedMeter.MeterName))
{
return true;
}
if (PageViewModel.SelectedInstrument != null && !instruments.Any(i => i.Name == PageViewModel.SelectedInstrument.Name))
{
return true;
}

return false;
}

private Task HandleSelectedDurationChangedAsync()
{
return this.AfterViewModelChangedAsync(_contentLayout, isChangeInToolbar: true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@
<PackageReference Include="bUnit" />
</ItemGroup>

<ItemGroup>
<Compile Include="$(TestsSharedDir)Telemetry\*.cs" LinkBase="shared/Telemetry" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
// 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.Tests.Shared;
using Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Model.MetricValues;
using Aspire.Dashboard.Otlp.Storage;
using Bunit;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.FluentUI.AspNetCore.Components;
using OpenTelemetry.Proto.Common.V1;
using OpenTelemetry.Proto.Metrics.V1;
using Xunit;
Expand All @@ -24,13 +22,7 @@ public class PlotlyChartTests : TestContext
public void Render_NoInstrument_NoPlotlyInvocations()
{
// Arrange
JSInterop.SetupModule("/js/app-metrics.js");

Services.AddLocalization();
Services.AddSingleton<IInstrumentUnitResolver, TestInstrumentUnitResolver>();
Services.AddSingleton<BrowserTimeProvider, TestTimeProvider>();
Services.AddSingleton<TelemetryRepository>();
Services.AddSingleton<IDialogService, DialogService>();
MetricsSetupHelpers.SetupPlotlyChart(this);

var model = new InstrumentViewModel();

Expand All @@ -55,14 +47,7 @@ public void Render_NoInstrument_NoPlotlyInvocations()
public async Task Render_HasInstrument_InitializeChartInvocation()
{
// Arrange
var module = JSInterop.SetupModule("/js/app-metrics.js");
module.SetupVoid("initializeChart", _ => true);

Services.AddLocalization();
Services.AddSingleton<IInstrumentUnitResolver, TestInstrumentUnitResolver>();
Services.AddSingleton<BrowserTimeProvider, TestTimeProvider>();
Services.AddSingleton<TelemetryRepository>();
Services.AddSingleton<IDialogService, DialogService>();
MetricsSetupHelpers.SetupPlotlyChart(this);

var options = new TelemetryLimitOptions();
var instrument = new OtlpInstrument
Expand Down
113 changes: 113 additions & 0 deletions tests/Aspire.Dashboard.Components.Tests/Pages/MetricsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Dashboard.Components.Controls;
using Aspire.Dashboard.Components.Pages;
using Aspire.Dashboard.Components.Resize;
using Aspire.Dashboard.Components.Tests.Shared;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Dashboard.Utils;
using Bunit;
using Google.Protobuf.Collections;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Proto.Metrics.V1;
using Xunit;
using static Aspire.Tests.Shared.Telemetry.TelemetryTestHelpers;

namespace Aspire.Dashboard.Components.Tests.Pages;

[UseCulture("en-US")]
public partial class MetricsTests : TestContext
{
private static readonly DateTime s_testTime = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

[Fact]
public void ChangeResource_MeterAndInstrumentOnNewResource_InstrumentSet()
{
ChangeResourceAndAssertInstrument(
app1InstrumentName: "test1",
app2InstrumentName: "test1",
expectedInstrumentNameAfterChange: "test1");
}

[Fact]
public void ChangeResource_MeterAndInstrumentNotOnNewResources_InstrumentCleared()
{
ChangeResourceAndAssertInstrument(
app1InstrumentName: "test1",
app2InstrumentName: "test2",
expectedInstrumentNameAfterChange: null);
}

private void ChangeResourceAndAssertInstrument(string app1InstrumentName, string app2InstrumentName, string? expectedInstrumentNameAfterChange)
{
// Arrange
MetricsSetupHelpers.SetupMetricsPage(this);

var navigationManager = Services.GetRequiredService<NavigationManager>();
navigationManager.NavigateTo(DashboardUrls.MetricsUrl(resource: "TestApp", meter: "test-meter", instrument: app1InstrumentName));

var telemetryRepository = Services.GetRequiredService<TelemetryRepository>();
telemetryRepository.AddMetrics(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.ApplicationName, "TestApp");
builder.AddCascadingValue(new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false));
});

var viewModel = cut.Instance.PageViewModel;

// Assert 1
Assert.Equal("test-meter", viewModel.SelectedMeter!.MeterName);
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.SelectedApplication.Name), TestConstants.WaitTimeout);

Assert.Equal(expectedInstrumentNameAfterChange, viewModel.SelectedInstrument?.Name);
if (expectedInstrumentNameAfterChange != null)
{
// Meter is cleared if instrument is cleared.
Assert.Equal("test-meter", viewModel.SelectedMeter!.MeterName);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Aspire.Dashboard.Components.Pages;
using Aspire.Dashboard.Components.Resize;
using Aspire.Dashboard.Components.Tests.Shared;
using Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Model.BrowserStorage;
Expand All @@ -17,7 +18,7 @@
using Microsoft.FluentUI.AspNetCore.Components;
using Xunit;

namespace Aspire.Dashboard.Components.Tests.Controls;
namespace Aspire.Dashboard.Components.Tests.Pages;

[UseCulture("en-US")]
public partial class StructuredLogsTests : TestContext
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Aspire.Dashboard.Components.Resize;
using Aspire.Dashboard.Components.Tests.Controls;
using Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Model.BrowserStorage;
using Aspire.Dashboard.Otlp.Storage;
using Bunit;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.FluentUI.AspNetCore.Components;

namespace Aspire.Dashboard.Components.Tests.Shared;

internal static class MetricsSetupHelpers
{
public static void SetupChartContainer(TestContext context)
{
_ = context.JSInterop.SetupModule("/Components/Controls/Chart/MetricTable.razor.js");

var tabModule = context.JSInterop.SetupModule("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Tabs/FluentTab.razor.js?v=4.9.3.24205");
tabModule.SetupVoid("TabEditable_Changed", _ => true);

var overflowModule = context.JSInterop.SetupModule("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Overflow/FluentOverflow.razor.js?v=4.9.3.24205");
overflowModule.SetupVoid("fluentOverflowInitialize", _ => true);

SetupPlotlyChart(context);
}

internal static void SetupPlotlyChart(TestContext context)
{
var module = context.JSInterop.SetupModule("/js/app-metrics.js");
module.SetupVoid("initializeChart", _ => true);
module.SetupVoid("updateChart", _ => true);

context.Services.AddLocalization();
context.Services.AddSingleton<IInstrumentUnitResolver, TestInstrumentUnitResolver>();
context.Services.AddSingleton<BrowserTimeProvider, TestTimeProvider>();
context.Services.AddSingleton<TelemetryRepository>();
context.Services.AddSingleton<IDialogService, DialogService>();
}

internal static void SetupMetricsPage(TestContext context)
{
var version = typeof(FluentMain).Assembly.GetName().Version!;

var dividerModule = context.JSInterop.SetupModule(GetFluentFile("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Divider/FluentDivider.razor.js", version));
dividerModule.SetupVoid("setDividerAriaOrientation");

var inputLabelModule = context.JSInterop.SetupModule(GetFluentFile("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Label/FluentInputLabel.razor.js", version));
inputLabelModule.SetupVoid("setInputAriaLabel", _ => true);

var dataGridModule = context.JSInterop.SetupModule(GetFluentFile("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/DataGrid/FluentDataGrid.razor.js", version));
var dataGridRef = dataGridModule.SetupModule("init", _ => true);
dataGridRef.SetupVoid("stop");

var listModule = context.JSInterop.SetupModule(GetFluentFile("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/List/ListComponentBase.razor.js", version));

var searchModule = context.JSInterop.SetupModule(GetFluentFile("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Search/FluentSearch.razor.js", version));
searchModule.SetupVoid("addAriaHidden", _ => true);

MetricsSetupHelpers.SetupChartContainer(context);

context.Services.AddLocalization();
context.Services.AddSingleton<TelemetryRepository>();
context.Services.AddSingleton<IMessageService, MessageService>();
context.Services.AddSingleton<IOptions<DashboardOptions>>(Options.Create(new DashboardOptions()));
context.Services.AddSingleton<DimensionManager>();
context.Services.AddSingleton<IDialogService, DialogService>();
context.Services.AddSingleton<BrowserTimeProvider, TestTimeProvider>();
context.Services.AddSingleton<ISessionStorage, TestSessionStorage>();
context.Services.AddSingleton<ILocalStorage, TestLocalStorage>();
context.Services.AddSingleton<ShortcutManager>();
context.Services.AddSingleton<LibraryConfiguration>();
context.Services.AddSingleton<IKeyCodeService, KeyCodeService>();
context.Services.AddSingleton<ThemeManager>();
}

private static string GetFluentFile(string filePath, Version version)
{
return $"{filePath}?v={version}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Dashboard.Components.Tests.Shared;

internal static class TestConstants
{
public static readonly TimeSpan WaitTimeout = TimeSpan.FromSeconds(5);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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.Model;
using Aspire.Dashboard.Otlp.Model;

namespace Aspire.Dashboard.Components.Tests.Controls;
namespace Aspire.Dashboard.Components.Tests.Shared;

public sealed class TestInstrumentUnitResolver : IInstrumentUnitResolver
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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.Model.BrowserStorage;

namespace Aspire.Dashboard.Components.Tests.Controls;
namespace Aspire.Dashboard.Components.Tests.Shared;

public sealed class TestLocalStorage : ILocalStorage
{
Expand Down
Loading

0 comments on commit ceb5d5a

Please sign in to comment.