Skip to content

Commit

Permalink
Persist dismissing the OTLP unsecured message bar (#5465)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK authored Aug 29, 2024
1 parent 1d33f4b commit 0587e48
Show file tree
Hide file tree
Showing 11 changed files with 274 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Globalization;
using Aspire.Dashboard.Components.Resize;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Utils;
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
using Microsoft.JSInterop;
Expand Down Expand Up @@ -293,13 +294,13 @@ static void GetPanelSizes(
private string GetSizeStorageKey()
{
var viewKey = ViewKey ?? NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
return $"Aspire_SplitterSize_{Orientation}_{viewKey}";
return BrowserStorageKeys.SplitterSizeKey(viewKey, Orientation);
}

private string GetOrientationStorageKey()
{
var viewKey = ViewKey ?? NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
return $"Aspire_SplitterOrientation_{viewKey}";
return BrowserStorageKeys.SplitterOrientationKey(viewKey);
}

public void Dispose()
Expand Down
14 changes: 7 additions & 7 deletions src/Aspire.Dashboard/Components/Controls/UserProfile.razor
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<AuthorizeView>
<Authorized>
@if (_showUserProfileMenu)
{
@if (_showUserProfileMenu)
{
<AuthorizeView>
<Authorized>
<div class="profile-menu-container">
<FluentProfileMenu Initials="@_initials"
EMail="@_username"
Expand All @@ -28,6 +28,6 @@
</ChildContent>
</FluentProfileMenu>
</div>
}
</Authorized>
</AuthorizeView>
</Authorized>
</AuthorizeView>
}
41 changes: 27 additions & 14 deletions src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ public partial class MainLayout : IGlobalKeydownListener, IAsyncDisposable
[Inject]
public required IOptionsMonitor<DashboardOptions> Options { get; init; }

[Inject]
public required ILocalStorage LocalStorage { get; init; }

[CascadingParameter]
public required ViewportInformation ViewportInformation { get; set; }

Expand Down Expand Up @@ -102,22 +105,32 @@ protected override async Task OnInitializedAsync()

if (Options.CurrentValue.Otlp.AuthMode == OtlpAuthMode.Unsecured)
{
// ShowMessageBarAsync must come after an await. Otherwise it will NRE.
// I think this order allows the message bar provider to be fully initialized.
await MessageService.ShowMessageBarAsync(options =>
var dismissedResult = await LocalStorage.GetUnprotectedAsync<bool>(BrowserStorageKeys.UnsecuredTelemetryMessageDismissedKey);
var skipMessage = dismissedResult.Success && dismissedResult.Value;

if (!skipMessage)
{
options.Title = Loc[nameof(Resources.Layout.MessageTelemetryTitle)];
options.Body = Loc[nameof(Resources.Layout.MessageTelemetryBody)];
options.Link = new()
// ShowMessageBarAsync must come after an await. Otherwise it will NRE.
// I think this order allows the message bar provider to be fully initialized.
await MessageService.ShowMessageBarAsync(options =>
{
Text = Loc[nameof(Resources.Layout.MessageTelemetryLink)],
Href = "https://aka.ms/dotnet/aspire/telemetry-unsecured",
Target = "_blank"
};
options.Intent = MessageIntent.Warning;
options.Section = MessageBarSection;
options.AllowDismiss = true;
});
options.Title = Loc[nameof(Resources.Layout.MessageTelemetryTitle)];
options.Body = Loc[nameof(Resources.Layout.MessageTelemetryBody)];
options.Link = new()
{
Text = Loc[nameof(Resources.Layout.MessageTelemetryLink)],
Href = "https://aka.ms/dotnet/aspire/telemetry-unsecured",
Target = "_blank"
};
options.Intent = MessageIntent.Warning;
options.Section = MessageBarSection;
options.AllowDismiss = true;
options.OnClose = async m =>
{
await LocalStorage.SetUnprotectedAsync(BrowserStorageKeys.UnsecuredTelemetryMessageDismissedKey, true);
};
});
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public sealed partial class ConsoleLogs : ComponentBase, IAsyncDisposable, IPage
public ConsoleLogsViewModel PageViewModel { get; set; } = null!;

public string BasePath => DashboardUrls.ConsoleLogBasePath;
public string SessionStorageKey => "Aspire_ConsoleLogs_PageState";
public string SessionStorageKey => BrowserStorageKeys.ConsoleLogsPageState;

protected override async Task OnInitializedAsync()
{
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Pages/Metrics.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public partial class Metrics : IDisposable, IPageWithSessionAndUrlState<Metrics.
private Subscription? _metricsSubscription;

public string BasePath => DashboardUrls.MetricsBasePath;
public string SessionStorageKey => "Aspire_Metrics_PageState";
public string SessionStorageKey => BrowserStorageKeys.MetricsPageState;
public MetricsViewModel PageViewModel { get; set; } = null!;

[Parameter]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public partial class StructuredLogs : IPageWithSessionAndUrlState<StructuredLogs
private GridColumnManager _manager = null!;

public string BasePath => DashboardUrls.StructuredLogsBasePath;
public string SessionStorageKey => "Aspire_StructuredLogs_PageState";
public string SessionStorageKey => BrowserStorageKeys.StructuredLogsPageState;
public StructuredLogsPageViewModel PageViewModel { get; set; } = null!;

[Inject]
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Pages/Traces.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public partial class Traces : IPageWithSessionAndUrlState<TracesPageViewModel, T
private AspirePageContentLayout? _contentLayout;
private GridColumnManager _manager = null!;

public string SessionStorageKey => "Aspire_Traces_PageState";
public string SessionStorageKey => BrowserStorageKeys.TracesPageState;
public string BasePath => DashboardUrls.TracesBasePath;
public TracesPageViewModel PageViewModel { get; set; } = null!;

Expand Down
26 changes: 26 additions & 0 deletions src/Aspire.Dashboard/Utils/BrowserStorageKeys.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.FluentUI.AspNetCore.Components;

namespace Aspire.Dashboard.Utils;

internal static class BrowserStorageKeys
{
public const string UnsecuredTelemetryMessageDismissedKey = "Aspire_Telemetry_UnsecuredMessageDismissed";

public const string TracesPageState = "Aspire_PageState_Traces";
public const string StructuredLogsPageState = "Aspire_PageState_StructuredLogs";
public const string MetricsPageState = "Aspire_PageState_Metrics";
public const string ConsoleLogsPageState = "Aspire_PageState_ConsoleLogs";

public static string SplitterOrientationKey(string viewKey)
{
return $"Aspire_SplitterOrientation_{viewKey}";
}

public static string SplitterSizeKey(string viewKey, Orientation orientation)
{
return $"Aspire_SplitterSize_{orientation}_{viewKey}";
}
}
162 changes: 162 additions & 0 deletions tests/Aspire.Dashboard.Components.Tests/Layout/MainLayoutTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// 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.Layout;
using Aspire.Dashboard.Components.Resize;
using Aspire.Dashboard.Components.Tests.Shared;
using Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Model.BrowserStorage;
using Aspire.Dashboard.Utils;
using Bunit;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.FluentUI.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components.Components.Tooltip;
using Xunit;

namespace Aspire.Dashboard.Components.Tests.Layout;

[UseCulture("en-US")]
public partial class MainLayoutTests : TestContext
{
[Fact]
public async Task OnInitialize_UnsecuredOtlp_NotDismissed_DisplayMessageBar()
{
// Arrange
var testLocalStorage = new TestLocalStorage();
var messageService = new MessageService();

SetupMainLayoutServices(localStorage: testLocalStorage, messageService: messageService);

Message? message = null;
var messageShownTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
messageService.OnMessageItemsUpdatedAsync += () =>
{
message = messageService.AllMessages.Single();
messageShownTcs.TrySetResult();
return Task.CompletedTask;
};

testLocalStorage.OnGetUnprotectedAsync = key =>
{
if (key == BrowserStorageKeys.UnsecuredTelemetryMessageDismissedKey)
{
return (false, false);
}
else
{
throw new InvalidOperationException("Unexpected key.");
}
};

var dismissedSettingSetTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
testLocalStorage.OnSetUnprotectedAsync = (key, value) =>
{
if (key == BrowserStorageKeys.UnsecuredTelemetryMessageDismissedKey)
{
dismissedSettingSetTcs.TrySetResult((bool)value!);
}
else
{
throw new InvalidOperationException("Unexpected key.");
}
};

// Act
var cut = RenderComponent<MainLayout>(builder =>
{
builder.Add(p => p.ViewportInformation, new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
});

// Assert
await messageShownTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));

Assert.NotNull(message);

message.Close();

Assert.True(await dismissedSettingSetTcs.Task.WaitAsync(TimeSpan.FromSeconds(5)));
}

[Fact]
public async Task OnInitialize_UnsecuredOtlp_Dismissed_NoMessageBar()
{
// Arrange
var testLocalStorage = new TestLocalStorage();
var messageService = new MessageService();

SetupMainLayoutServices(localStorage: testLocalStorage, messageService: messageService);

var messageShownTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
messageService.OnMessageItemsUpdatedAsync += () =>
{
messageShownTcs.TrySetResult();
return Task.CompletedTask;
};

testLocalStorage.OnGetUnprotectedAsync = key =>
{
if (key == BrowserStorageKeys.UnsecuredTelemetryMessageDismissedKey)
{
return (true, true);
}
else
{
throw new InvalidOperationException("Unexpected key.");
}
};

// Act
var cut = RenderComponent<MainLayout>(builder =>
{
builder.Add(p => p.ViewportInformation, new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
});

// Assert
var timeoutTask = Task.Delay(100);
var completedTask = await Task.WhenAny(messageShownTcs.Task, timeoutTask).WaitAsync(TimeSpan.FromSeconds(5));

// It's hard to test something not happening.
// In this case of checking for a message, apply a small display and then double check that no message was displayed.
Assert.True(completedTask != messageShownTcs.Task, "No message bar should be displayed.");
Assert.Empty(messageService.AllMessages);
}

private void SetupMainLayoutServices(TestLocalStorage? localStorage = null, MessageService? messageService = null)
{
Services.AddLocalization();
Services.AddOptions();
Services.AddSingleton<ThemeManager>();
Services.AddSingleton<IDialogService, DialogService>();
Services.AddSingleton<IDashboardClient, TestDashboardClient>();
Services.AddSingleton<ILocalStorage>(localStorage ?? new TestLocalStorage());
Services.AddSingleton<IEffectiveThemeResolver, TestEffectiveThemeResolver>();
Services.AddSingleton<ShortcutManager>();
Services.AddSingleton<BrowserTimeProvider, TestTimeProvider>();
Services.AddSingleton<IMessageService>(messageService ?? new MessageService());
Services.AddSingleton<LibraryConfiguration>();
Services.AddSingleton<ITooltipService, TooltipService>();
Services.AddSingleton<IToastService, ToastService>();
Services.AddSingleton<GlobalState>();
Services.Configure<DashboardOptions>(o => o.Otlp.AuthMode = OtlpAuthMode.Unsecured);

var version = typeof(FluentMain).Assembly.GetName().Version!;

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

var anchorModule = JSInterop.SetupModule(GetFluentFile("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Anchor/FluentAnchor.razor.js", version));

var themeModule = JSInterop.SetupModule("/js/app-theme.js");

JSInterop.SetupModule("window.registerGlobalKeydownListener", _ => true);
JSInterop.SetupModule("window.registerOpenTextVisualizerOnClick", _ => true);

JSInterop.Setup<string>("window.getBrowserTimeZone").SetResult("abc");
}

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,33 @@
// 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;

namespace Aspire.Dashboard.Components.Tests.Shared;

public class TestDashboardClient : IDashboardClient
{
public bool IsEnabled { get; }
public Task WhenConnected { get; } = Task.CompletedTask;
public string ApplicationName { get; } = "TestApp";

public ValueTask DisposeAsync()
{
throw new NotImplementedException();
}

public Task<ResourceCommandResponseViewModel> ExecuteResourceCommandAsync(string resourceName, string resourceType, CommandViewModel command, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}

public IAsyncEnumerable<IReadOnlyList<ResourceLogLine>>? SubscribeConsoleLogs(string resourceName, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}

public Task<ResourceViewModelSubscription> SubscribeResourcesAsync(CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
12 changes: 12 additions & 0 deletions tests/Aspire.Dashboard.Components.Tests/Shared/TestLocalStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,21 @@ namespace Aspire.Dashboard.Components.Tests.Shared;

public sealed class TestLocalStorage : ILocalStorage
{
public Func<string, (bool Success, object? Value)>? OnGetUnprotectedAsync { get; set; }
public Action<string, object?>? OnSetUnprotectedAsync { get; set; }

public Task<StorageResult<T>> GetAsync<T>(string key)
{
return Task.FromResult(new StorageResult<T>(Success: false, Value: default));
}

public Task<StorageResult<T>> GetUnprotectedAsync<T>(string key)
{
if (OnGetUnprotectedAsync is { } callback)
{
var (success, value) = callback(key);
return Task.FromResult(new StorageResult<T>(Success: success, Value: (T)(value ?? default(T))!));
}
return Task.FromResult(new StorageResult<T>(Success: false, Value: default));
}

Expand All @@ -24,6 +32,10 @@ public Task SetAsync<T>(string key, T value)

public Task SetUnprotectedAsync<T>(string key, T value)
{
if (OnSetUnprotectedAsync is { } callback)
{
callback(key, value);
}
return Task.CompletedTask;
}
}

0 comments on commit 0587e48

Please sign in to comment.