diff --git a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs index 4b18e713c67..6a6cc151010 100644 --- a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs +++ b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs @@ -105,7 +105,7 @@ protected override async Task OnInitializedAsync() TimeProvider.SetBrowserTimeZone(result.TimeZone); TelemetryContextProvider.SetBrowserUserAgent(result.UserAgent); - if (Options.CurrentValue.Otlp.AuthMode == OtlpAuthMode.Unsecured) + if (Options.CurrentValue.Otlp.AuthMode == OtlpAuthMode.Unsecured && !Options.CurrentValue.Otlp.SuppressUnsecuredTelemetryMessage) { var dismissedResult = await LocalStorage.GetUnprotectedAsync(BrowserStorageKeys.UnsecuredTelemetryMessageDismissedKey); var skipMessage = dismissedResult.Success && dismissedResult.Value; diff --git a/src/Aspire.Dashboard/Configuration/DashboardOptions.cs b/src/Aspire.Dashboard/Configuration/DashboardOptions.cs index 37e51f58535..363e667a372 100644 --- a/src/Aspire.Dashboard/Configuration/DashboardOptions.cs +++ b/src/Aspire.Dashboard/Configuration/DashboardOptions.cs @@ -85,6 +85,12 @@ public sealed class OtlpOptions public List AllowedCertificates { get; set; } = new(); + /// + /// Gets or sets a value indicating whether to suppress the unsecured telemetry message in the dashboard UI. + /// When true, the warning message about unsecured OTLP endpoints will not be displayed. + /// + public bool SuppressUnsecuredTelemetryMessage { get; set; } + public BindingAddress? GetGrpcEndpointAddress() { return _parsedGrpcEndpointAddress; diff --git a/src/Shared/DashboardConfigNames.cs b/src/Shared/DashboardConfigNames.cs index 3eb3a1076a8..86bc1eb6e56 100644 --- a/src/Shared/DashboardConfigNames.cs +++ b/src/Shared/DashboardConfigNames.cs @@ -18,6 +18,7 @@ internal static class DashboardConfigNames public static readonly ConfigName DashboardOtlpAuthModeName = new("Dashboard:Otlp:AuthMode", "DASHBOARD__OTLP__AUTHMODE"); public static readonly ConfigName DashboardOtlpPrimaryApiKeyName = new("Dashboard:Otlp:PrimaryApiKey", "DASHBOARD__OTLP__PRIMARYAPIKEY"); public static readonly ConfigName DashboardOtlpSecondaryApiKeyName = new("Dashboard:Otlp:SecondaryApiKey", "DASHBOARD__OTLP__SECONDARYAPIKEY"); + public static readonly ConfigName DashboardOtlpSuppressUnsecuredTelemetryMessageName = new("Dashboard:Otlp:SuppressUnsecuredTelemetryMessage", "DASHBOARD__OTLP__SUPPRESSUNSECUREDTELEMETRYMESSAGE"); public static readonly ConfigName DashboardOtlpCorsAllowedOriginsKeyName = new("Dashboard:Otlp:Cors:AllowedOrigins", "DASHBOARD__OTLP__CORS__ALLOWEDORIGINS"); public static readonly ConfigName DashboardOtlpCorsAllowedHeadersKeyName = new("Dashboard:Otlp:Cors:AllowedHeaders", "DASHBOARD__OTLP__CORS__ALLOWEDHEADERS"); public static readonly ConfigName DashboardOtlpAllowedCertificatesName = new("Dashboard:Otlp:AllowedCertificates", "DASHBOARD__OTLP__ALLOWEDCERTIFICATES"); diff --git a/tests/Aspire.Dashboard.Components.Tests/Layout/MainLayoutTests.cs b/tests/Aspire.Dashboard.Components.Tests/Layout/MainLayoutTests.cs index afe1bf75b4d..6c3b2f662cc 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Layout/MainLayoutTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Layout/MainLayoutTests.cs @@ -12,6 +12,7 @@ using Aspire.Dashboard.Tests; using Aspire.Dashboard.Utils; using Bunit; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; using Microsoft.FluentUI.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components.Components.Tooltip; @@ -72,13 +73,13 @@ public async Task OnInitialize_UnsecuredOtlp_NotDismissed_DisplayMessageBar() }); // Assert - await messageShownTcs.Task.WaitAsync(TimeSpan.FromSeconds(5)); + await messageShownTcs.Task.DefaultTimeout(); Assert.NotNull(message); message.Close(); - Assert.True(await dismissedSettingSetTcs.Task.WaitAsync(TimeSpan.FromSeconds(5))); + Assert.True(await dismissedSettingSetTcs.Task.DefaultTimeout()); } [Fact] @@ -117,7 +118,7 @@ public async Task OnInitialize_UnsecuredOtlp_Dismissed_NoMessageBar() // Assert var timeoutTask = Task.Delay(100); - var completedTask = await Task.WhenAny(messageShownTcs.Task, timeoutTask).WaitAsync(TimeSpan.FromSeconds(5)); + var completedTask = await Task.WhenAny(messageShownTcs.Task, timeoutTask).DefaultTimeout(); // 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. @@ -125,7 +126,61 @@ public async Task OnInitialize_UnsecuredOtlp_Dismissed_NoMessageBar() Assert.Empty(messageService.AllMessages); } - private void SetupMainLayoutServices(TestLocalStorage? localStorage = null, MessageService? messageService = null) + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task OnInitialize_UnsecuredOtlp_SuppressConfigured_NoMessageBar(bool suppressUnsecuredMessage) + { + // Arrange + var testLocalStorage = new TestLocalStorage(); + var messageService = new MessageService(); + + SetupMainLayoutServices(localStorage: testLocalStorage, messageService: messageService, suppressUnsecuredMessage: suppressUnsecuredMessage); + + var messageShownTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + messageService.OnMessageItemsUpdatedAsync += () => + { + messageShownTcs.TrySetResult(); + return Task.CompletedTask; + }; + + testLocalStorage.OnGetUnprotectedAsync = key => + { + if (key == BrowserStorageKeys.UnsecuredTelemetryMessageDismissedKey) + { + return (false, false); // Message not dismissed, but should be suppressed by config if suppressUnsecuredMessage is true + } + else + { + throw new InvalidOperationException("Unexpected key."); + } + }; + + // Act + var cut = RenderComponent(builder => + { + builder.Add(p => p.ViewportInformation, new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false)); + }); + + // Assert + if (suppressUnsecuredMessage) + { + var timeoutTask = Task.Delay(100); + var completedTask = await Task.WhenAny(messageShownTcs.Task, timeoutTask).DefaultTimeout(); + + // When suppressed, no message should be displayed + Assert.True(completedTask != messageShownTcs.Task, "No message bar should be displayed when suppressed by configuration."); + Assert.Empty(messageService.AllMessages); + } + else + { + // When not suppressed, message should be displayed since it wasn't dismissed + await messageShownTcs.Task.DefaultTimeout(); + Assert.NotEmpty(messageService.AllMessages); + } + } + + private void SetupMainLayoutServices(TestLocalStorage? localStorage = null, MessageService? messageService = null, bool suppressUnsecuredMessage = false) { Services.AddLocalization(); Services.AddOptions(); @@ -144,7 +199,11 @@ private void SetupMainLayoutServices(TestLocalStorage? localStorage = null, Mess Services.AddSingleton(); Services.AddSingleton(); Services.AddSingleton(); - Services.Configure(o => o.Otlp.AuthMode = OtlpAuthMode.Unsecured); + Services.Configure(o => + { + o.Otlp.AuthMode = OtlpAuthMode.Unsecured; + o.Otlp.SuppressUnsecuredTelemetryMessage = suppressUnsecuredMessage; + }); var version = typeof(FluentMain).Assembly.GetName().Version!;