Skip to content

Commit 0587e48

Browse files
authored
Persist dismissing the OTLP unsecured message bar (#5465)
1 parent 1d33f4b commit 0587e48

File tree

11 files changed

+274
-27
lines changed

11 files changed

+274
-27
lines changed

src/Aspire.Dashboard/Components/Controls/SummaryDetailsView.razor.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Globalization;
55
using Aspire.Dashboard.Components.Resize;
66
using Aspire.Dashboard.Model;
7+
using Aspire.Dashboard.Utils;
78
using Microsoft.AspNetCore.Components;
89
using Microsoft.FluentUI.AspNetCore.Components;
910
using Microsoft.JSInterop;
@@ -293,13 +294,13 @@ static void GetPanelSizes(
293294
private string GetSizeStorageKey()
294295
{
295296
var viewKey = ViewKey ?? NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
296-
return $"Aspire_SplitterSize_{Orientation}_{viewKey}";
297+
return BrowserStorageKeys.SplitterSizeKey(viewKey, Orientation);
297298
}
298299

299300
private string GetOrientationStorageKey()
300301
{
301302
var viewKey = ViewKey ?? NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
302-
return $"Aspire_SplitterOrientation_{viewKey}";
303+
return BrowserStorageKeys.SplitterOrientationKey(viewKey);
303304
}
304305

305306
public void Dispose()

src/Aspire.Dashboard/Components/Controls/UserProfile.razor

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
<AuthorizeView>
2-
<Authorized>
3-
@if (_showUserProfileMenu)
4-
{
1+
@if (_showUserProfileMenu)
2+
{
3+
<AuthorizeView>
4+
<Authorized>
55
<div class="profile-menu-container">
66
<FluentProfileMenu Initials="@_initials"
77
EMail="@_username"
@@ -28,6 +28,6 @@
2828
</ChildContent>
2929
</FluentProfileMenu>
3030
</div>
31-
}
32-
</Authorized>
33-
</AuthorizeView>
31+
</Authorized>
32+
</AuthorizeView>
33+
}

src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ public partial class MainLayout : IGlobalKeydownListener, IAsyncDisposable
6464
[Inject]
6565
public required IOptionsMonitor<DashboardOptions> Options { get; init; }
6666

67+
[Inject]
68+
public required ILocalStorage LocalStorage { get; init; }
69+
6770
[CascadingParameter]
6871
public required ViewportInformation ViewportInformation { get; set; }
6972

@@ -102,22 +105,32 @@ protected override async Task OnInitializedAsync()
102105

103106
if (Options.CurrentValue.Otlp.AuthMode == OtlpAuthMode.Unsecured)
104107
{
105-
// ShowMessageBarAsync must come after an await. Otherwise it will NRE.
106-
// I think this order allows the message bar provider to be fully initialized.
107-
await MessageService.ShowMessageBarAsync(options =>
108+
var dismissedResult = await LocalStorage.GetUnprotectedAsync<bool>(BrowserStorageKeys.UnsecuredTelemetryMessageDismissedKey);
109+
var skipMessage = dismissedResult.Success && dismissedResult.Value;
110+
111+
if (!skipMessage)
108112
{
109-
options.Title = Loc[nameof(Resources.Layout.MessageTelemetryTitle)];
110-
options.Body = Loc[nameof(Resources.Layout.MessageTelemetryBody)];
111-
options.Link = new()
113+
// ShowMessageBarAsync must come after an await. Otherwise it will NRE.
114+
// I think this order allows the message bar provider to be fully initialized.
115+
await MessageService.ShowMessageBarAsync(options =>
112116
{
113-
Text = Loc[nameof(Resources.Layout.MessageTelemetryLink)],
114-
Href = "https://aka.ms/dotnet/aspire/telemetry-unsecured",
115-
Target = "_blank"
116-
};
117-
options.Intent = MessageIntent.Warning;
118-
options.Section = MessageBarSection;
119-
options.AllowDismiss = true;
120-
});
117+
options.Title = Loc[nameof(Resources.Layout.MessageTelemetryTitle)];
118+
options.Body = Loc[nameof(Resources.Layout.MessageTelemetryBody)];
119+
options.Link = new()
120+
{
121+
Text = Loc[nameof(Resources.Layout.MessageTelemetryLink)],
122+
Href = "https://aka.ms/dotnet/aspire/telemetry-unsecured",
123+
Target = "_blank"
124+
};
125+
options.Intent = MessageIntent.Warning;
126+
options.Section = MessageBarSection;
127+
options.AllowDismiss = true;
128+
options.OnClose = async m =>
129+
{
130+
await LocalStorage.SetUnprotectedAsync(BrowserStorageKeys.UnsecuredTelemetryMessageDismissedKey, true);
131+
};
132+
});
133+
}
121134
}
122135
}
123136

src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public sealed partial class ConsoleLogs : ComponentBase, IAsyncDisposable, IPage
5757
public ConsoleLogsViewModel PageViewModel { get; set; } = null!;
5858

5959
public string BasePath => DashboardUrls.ConsoleLogBasePath;
60-
public string SessionStorageKey => "Aspire_ConsoleLogs_PageState";
60+
public string SessionStorageKey => BrowserStorageKeys.ConsoleLogsPageState;
6161

6262
protected override async Task OnInitializedAsync()
6363
{

src/Aspire.Dashboard/Components/Pages/Metrics.razor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public partial class Metrics : IDisposable, IPageWithSessionAndUrlState<Metrics.
2727
private Subscription? _metricsSubscription;
2828

2929
public string BasePath => DashboardUrls.MetricsBasePath;
30-
public string SessionStorageKey => "Aspire_Metrics_PageState";
30+
public string SessionStorageKey => BrowserStorageKeys.MetricsPageState;
3131
public MetricsViewModel PageViewModel { get; set; } = null!;
3232

3333
[Parameter]

src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public partial class StructuredLogs : IPageWithSessionAndUrlState<StructuredLogs
4444
private GridColumnManager _manager = null!;
4545

4646
public string BasePath => DashboardUrls.StructuredLogsBasePath;
47-
public string SessionStorageKey => "Aspire_StructuredLogs_PageState";
47+
public string SessionStorageKey => BrowserStorageKeys.StructuredLogsPageState;
4848
public StructuredLogsPageViewModel PageViewModel { get; set; } = null!;
4949

5050
[Inject]

src/Aspire.Dashboard/Components/Pages/Traces.razor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public partial class Traces : IPageWithSessionAndUrlState<TracesPageViewModel, T
4040
private AspirePageContentLayout? _contentLayout;
4141
private GridColumnManager _manager = null!;
4242

43-
public string SessionStorageKey => "Aspire_Traces_PageState";
43+
public string SessionStorageKey => BrowserStorageKeys.TracesPageState;
4444
public string BasePath => DashboardUrls.TracesBasePath;
4545
public TracesPageViewModel PageViewModel { get; set; } = null!;
4646

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.FluentUI.AspNetCore.Components;
5+
6+
namespace Aspire.Dashboard.Utils;
7+
8+
internal static class BrowserStorageKeys
9+
{
10+
public const string UnsecuredTelemetryMessageDismissedKey = "Aspire_Telemetry_UnsecuredMessageDismissed";
11+
12+
public const string TracesPageState = "Aspire_PageState_Traces";
13+
public const string StructuredLogsPageState = "Aspire_PageState_StructuredLogs";
14+
public const string MetricsPageState = "Aspire_PageState_Metrics";
15+
public const string ConsoleLogsPageState = "Aspire_PageState_ConsoleLogs";
16+
17+
public static string SplitterOrientationKey(string viewKey)
18+
{
19+
return $"Aspire_SplitterOrientation_{viewKey}";
20+
}
21+
22+
public static string SplitterSizeKey(string viewKey, Orientation orientation)
23+
{
24+
return $"Aspire_SplitterSize_{orientation}_{viewKey}";
25+
}
26+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Dashboard.Components.Layout;
5+
using Aspire.Dashboard.Components.Resize;
6+
using Aspire.Dashboard.Components.Tests.Shared;
7+
using Aspire.Dashboard.Configuration;
8+
using Aspire.Dashboard.Model;
9+
using Aspire.Dashboard.Model.BrowserStorage;
10+
using Aspire.Dashboard.Utils;
11+
using Bunit;
12+
using Microsoft.Extensions.DependencyInjection;
13+
using Microsoft.FluentUI.AspNetCore.Components;
14+
using Microsoft.FluentUI.AspNetCore.Components.Components.Tooltip;
15+
using Xunit;
16+
17+
namespace Aspire.Dashboard.Components.Tests.Layout;
18+
19+
[UseCulture("en-US")]
20+
public partial class MainLayoutTests : TestContext
21+
{
22+
[Fact]
23+
public async Task OnInitialize_UnsecuredOtlp_NotDismissed_DisplayMessageBar()
24+
{
25+
// Arrange
26+
var testLocalStorage = new TestLocalStorage();
27+
var messageService = new MessageService();
28+
29+
SetupMainLayoutServices(localStorage: testLocalStorage, messageService: messageService);
30+
31+
Message? message = null;
32+
var messageShownTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
33+
messageService.OnMessageItemsUpdatedAsync += () =>
34+
{
35+
message = messageService.AllMessages.Single();
36+
messageShownTcs.TrySetResult();
37+
return Task.CompletedTask;
38+
};
39+
40+
testLocalStorage.OnGetUnprotectedAsync = key =>
41+
{
42+
if (key == BrowserStorageKeys.UnsecuredTelemetryMessageDismissedKey)
43+
{
44+
return (false, false);
45+
}
46+
else
47+
{
48+
throw new InvalidOperationException("Unexpected key.");
49+
}
50+
};
51+
52+
var dismissedSettingSetTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
53+
testLocalStorage.OnSetUnprotectedAsync = (key, value) =>
54+
{
55+
if (key == BrowserStorageKeys.UnsecuredTelemetryMessageDismissedKey)
56+
{
57+
dismissedSettingSetTcs.TrySetResult((bool)value!);
58+
}
59+
else
60+
{
61+
throw new InvalidOperationException("Unexpected key.");
62+
}
63+
};
64+
65+
// Act
66+
var cut = RenderComponent<MainLayout>(builder =>
67+
{
68+
builder.Add(p => p.ViewportInformation, new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
69+
});
70+
71+
// Assert
72+
await messageShownTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
73+
74+
Assert.NotNull(message);
75+
76+
message.Close();
77+
78+
Assert.True(await dismissedSettingSetTcs.Task.WaitAsync(TimeSpan.FromSeconds(5)));
79+
}
80+
81+
[Fact]
82+
public async Task OnInitialize_UnsecuredOtlp_Dismissed_NoMessageBar()
83+
{
84+
// Arrange
85+
var testLocalStorage = new TestLocalStorage();
86+
var messageService = new MessageService();
87+
88+
SetupMainLayoutServices(localStorage: testLocalStorage, messageService: messageService);
89+
90+
var messageShownTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
91+
messageService.OnMessageItemsUpdatedAsync += () =>
92+
{
93+
messageShownTcs.TrySetResult();
94+
return Task.CompletedTask;
95+
};
96+
97+
testLocalStorage.OnGetUnprotectedAsync = key =>
98+
{
99+
if (key == BrowserStorageKeys.UnsecuredTelemetryMessageDismissedKey)
100+
{
101+
return (true, true);
102+
}
103+
else
104+
{
105+
throw new InvalidOperationException("Unexpected key.");
106+
}
107+
};
108+
109+
// Act
110+
var cut = RenderComponent<MainLayout>(builder =>
111+
{
112+
builder.Add(p => p.ViewportInformation, new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false));
113+
});
114+
115+
// Assert
116+
var timeoutTask = Task.Delay(100);
117+
var completedTask = await Task.WhenAny(messageShownTcs.Task, timeoutTask).WaitAsync(TimeSpan.FromSeconds(5));
118+
119+
// It's hard to test something not happening.
120+
// In this case of checking for a message, apply a small display and then double check that no message was displayed.
121+
Assert.True(completedTask != messageShownTcs.Task, "No message bar should be displayed.");
122+
Assert.Empty(messageService.AllMessages);
123+
}
124+
125+
private void SetupMainLayoutServices(TestLocalStorage? localStorage = null, MessageService? messageService = null)
126+
{
127+
Services.AddLocalization();
128+
Services.AddOptions();
129+
Services.AddSingleton<ThemeManager>();
130+
Services.AddSingleton<IDialogService, DialogService>();
131+
Services.AddSingleton<IDashboardClient, TestDashboardClient>();
132+
Services.AddSingleton<ILocalStorage>(localStorage ?? new TestLocalStorage());
133+
Services.AddSingleton<IEffectiveThemeResolver, TestEffectiveThemeResolver>();
134+
Services.AddSingleton<ShortcutManager>();
135+
Services.AddSingleton<BrowserTimeProvider, TestTimeProvider>();
136+
Services.AddSingleton<IMessageService>(messageService ?? new MessageService());
137+
Services.AddSingleton<LibraryConfiguration>();
138+
Services.AddSingleton<ITooltipService, TooltipService>();
139+
Services.AddSingleton<IToastService, ToastService>();
140+
Services.AddSingleton<GlobalState>();
141+
Services.Configure<DashboardOptions>(o => o.Otlp.AuthMode = OtlpAuthMode.Unsecured);
142+
143+
var version = typeof(FluentMain).Assembly.GetName().Version!;
144+
145+
var overflowModule = JSInterop.SetupModule(GetFluentFile("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Overflow/FluentOverflow.razor.js", version));
146+
overflowModule.SetupVoid("fluentOverflowInitialize", _ => true);
147+
148+
var anchorModule = JSInterop.SetupModule(GetFluentFile("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Anchor/FluentAnchor.razor.js", version));
149+
150+
var themeModule = JSInterop.SetupModule("/js/app-theme.js");
151+
152+
JSInterop.SetupModule("window.registerGlobalKeydownListener", _ => true);
153+
JSInterop.SetupModule("window.registerOpenTextVisualizerOnClick", _ => true);
154+
155+
JSInterop.Setup<string>("window.getBrowserTimeZone").SetResult("abc");
156+
}
157+
158+
private static string GetFluentFile(string filePath, Version version)
159+
{
160+
return $"{filePath}?v={version}";
161+
}
162+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Dashboard.Model;
5+
6+
namespace Aspire.Dashboard.Components.Tests.Shared;
7+
8+
public class TestDashboardClient : IDashboardClient
9+
{
10+
public bool IsEnabled { get; }
11+
public Task WhenConnected { get; } = Task.CompletedTask;
12+
public string ApplicationName { get; } = "TestApp";
13+
14+
public ValueTask DisposeAsync()
15+
{
16+
throw new NotImplementedException();
17+
}
18+
19+
public Task<ResourceCommandResponseViewModel> ExecuteResourceCommandAsync(string resourceName, string resourceType, CommandViewModel command, CancellationToken cancellationToken)
20+
{
21+
throw new NotImplementedException();
22+
}
23+
24+
public IAsyncEnumerable<IReadOnlyList<ResourceLogLine>>? SubscribeConsoleLogs(string resourceName, CancellationToken cancellationToken)
25+
{
26+
throw new NotImplementedException();
27+
}
28+
29+
public Task<ResourceViewModelSubscription> SubscribeResourcesAsync(CancellationToken cancellationToken)
30+
{
31+
throw new NotImplementedException();
32+
}
33+
}

0 commit comments

Comments
 (0)