From 033a9ff42110567d67edca09fd21179f2b17b0cd Mon Sep 17 00:00:00 2001 From: Tim Mulholland Date: Fri, 5 Apr 2024 13:54:47 -0700 Subject: [PATCH 01/11] Add User Profile Menu for OpenID Connect login --- .../Components/Controls/UserProfile.razor | 33 +++++++++ .../Components/Controls/UserProfile.razor.cs | 72 +++++++++++++++++++ .../Components/Controls/UserProfile.razor.css | 60 ++++++++++++++++ .../Components/Layout/MainLayout.razor | 1 + .../Components/Layout/MainLayout.razor.css | 9 +-- .../Components/_Imports.razor | 1 + .../Configuration/DashboardOptions.cs | 50 +++++++++++++ .../Configuration/ValidateDashboardOptions.cs | 5 ++ .../DashboardWebApplication.cs | 4 ++ .../Extensions/ClaimsIdentityExtensions.cs | 32 +++++++++ .../Extensions/StringExtensions.cs | 43 ++++++----- src/Aspire.Dashboard/README.md | 8 ++- .../Resources/Login.Designer.cs | 9 +++ src/Aspire.Dashboard/Resources/Login.resx | 4 ++ .../Resources/xlf/Login.cs.xlf | 5 ++ .../Resources/xlf/Login.de.xlf | 5 ++ .../Resources/xlf/Login.es.xlf | 5 ++ .../Resources/xlf/Login.fr.xlf | 5 ++ .../Resources/xlf/Login.it.xlf | 5 ++ .../Resources/xlf/Login.ja.xlf | 5 ++ .../Resources/xlf/Login.ko.xlf | 5 ++ .../Resources/xlf/Login.pl.xlf | 5 ++ .../Resources/xlf/Login.pt-BR.xlf | 5 ++ .../Resources/xlf/Login.ru.xlf | 5 ++ .../Resources/xlf/Login.tr.xlf | 5 ++ .../Resources/xlf/Login.zh-Hans.xlf | 5 ++ .../Resources/xlf/Login.zh-Hant.xlf | 5 ++ .../StringExtensionsTests.cs | 34 +++++---- 28 files changed, 391 insertions(+), 39 deletions(-) create mode 100644 src/Aspire.Dashboard/Components/Controls/UserProfile.razor create mode 100644 src/Aspire.Dashboard/Components/Controls/UserProfile.razor.cs create mode 100644 src/Aspire.Dashboard/Components/Controls/UserProfile.razor.css create mode 100644 src/Aspire.Dashboard/Extensions/ClaimsIdentityExtensions.cs diff --git a/src/Aspire.Dashboard/Components/Controls/UserProfile.razor b/src/Aspire.Dashboard/Components/Controls/UserProfile.razor new file mode 100644 index 0000000000..6251e54e30 --- /dev/null +++ b/src/Aspire.Dashboard/Components/Controls/UserProfile.razor @@ -0,0 +1,33 @@ + + + @if (_showUserProfileMenu) + { +
+ + + + OpenID Connect + +
+ + Sign out + +
+
+ + + +
@_name
+
@_username
+
+
+
+
+
+ } +
+
diff --git a/src/Aspire.Dashboard/Components/Controls/UserProfile.razor.cs b/src/Aspire.Dashboard/Components/Controls/UserProfile.razor.cs new file mode 100644 index 0000000000..5c302c9920 --- /dev/null +++ b/src/Aspire.Dashboard/Components/Controls/UserProfile.razor.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Security.Claims; +using Aspire.Dashboard.Configuration; +using Aspire.Dashboard.Extensions; +using Aspire.Dashboard.Resources; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; + +namespace Aspire.Dashboard.Components.Controls; + +public partial class UserProfile : ComponentBase +{ + [Inject] + public required IOptionsMonitor DashboardOptions { get; set; } + + [CascadingParameter] + public required Task AuthenticationState { get; set; } + + [Inject] + public required IStringLocalizer Loc { get; set; } + + [Inject] + public required ILogger Logger { get; set; } + + [Parameter] + public string ButtonSize { get; set; } = "24px"; + + [Parameter] + public string ImageSize { get; set; } = "52px"; + + private bool _showUserProfileMenu; + private string? _name; + private string? _username; + private string? _initials; + + protected override async Task OnParametersSetAsync() + { + if (DashboardOptions.CurrentValue.Frontend.AuthMode == FrontendAuthMode.OpenIdConnect) + { + var authState = await AuthenticationState; + + var claimsIdentity = authState.User.Identity as ClaimsIdentity; + + if (claimsIdentity?.IsAuthenticated == true) + { + _showUserProfileMenu = true; + _name = claimsIdentity.FindFirst(DashboardOptions.CurrentValue.OpenIdConnect.GetNameClaimTypes()); + if (string.IsNullOrWhiteSpace(_name)) + { + // Make sure there's always a name, even if that name is a placeholder + _name = Loc[nameof(Login.AuthorizedUser)]; + } + + _username = claimsIdentity.FindFirst(DashboardOptions.CurrentValue.OpenIdConnect.GetUsernameClaimTypes()); + _initials = _name.GetInitials(); + } + else + { + // If we don't have an authenticated user, don't show the user profile menu. This shouldn't happen. + _showUserProfileMenu = false; + _name = null; + _username = null; + _initials = null; + Logger.LogError("Dashboard:Frontend:AuthMode is configured for OpenIDConnect, but there is no authenticated user."); + } + } + } +} diff --git a/src/Aspire.Dashboard/Components/Controls/UserProfile.razor.css b/src/Aspire.Dashboard/Components/Controls/UserProfile.razor.css new file mode 100644 index 0000000000..e61ff5a8ae --- /dev/null +++ b/src/Aspire.Dashboard/Components/Controls/UserProfile.razor.css @@ -0,0 +1,60 @@ + +/* + Workaround for issue in fluent-anchored-region when the content + is near the top-right corner of the screen. Addressed in + https://github.com/microsoft/fluentui-blazor/pull/1795 which + we can use instead when it is available +*/ +.profile-menu-container ::deep .fluent-profile-menu fluent-anchored-region { + transform: unset !important; + left: unset; + right: -4px; +} + +::deep fluent-button[appearance=stealth]:not(:hover):not(:active)::part(control) { + background-color: var(--neutral-layer-floating); +} + +::deep fluent-button[appearance=stealth]:hover::part(control) { + background-color: var(--neutral-fill-secondary-hover); +} + +::deep .fluent-profile-menu > .fluent-persona { + margin: 0 4px; +} + +::deep .fluent-profile-menu > .fluent-persona > .initials { + font-size: var(--type-ramp-minus-1-font-size); +} + +::deep .full-name, +::deep .user-id { + color: var(--neutral-foreground-rest); + max-width: 200px; + overflow-x: hidden; + text-overflow: ellipsis; +} + +::deep .full-name { + font-size: var(--type-ramp-base-font-size); +} + +::deep .user-id { + font-size: var(--type-ramp-minus-1-font-size); + font-weight: normal; + max-width: 200px; +} + +::deep .inner-persona-container { + height: 100%; +} + +::deep .fluent-persona.inner-persona { + margin-right: 40px; + align-items: normal; +} + +/* The form takes up space and throws off the alignment if we don't change its display */ +::deep .sign-out-form { + display: flex; +} diff --git a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor index ee36d176bf..30f6a5873e 100644 --- a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor +++ b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor @@ -30,6 +30,7 @@ Title="@Loc[nameof(Layout.MainLayoutLaunchSettings)]" aria-label="@Loc[nameof(Layout.MainLayoutLaunchSettings)]"> +
diff --git a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.css b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.css index 7e0dabeeba..a30d64650f 100644 --- a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.css +++ b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.css @@ -50,6 +50,7 @@ ::deep.layout > header { grid-area: head; + /*overflow-x: clip;*/ } ::deep.layout > .nav-menu-container { @@ -74,9 +75,9 @@ margin-left: auto; } -::deep.layout > header fluent-button[appearance=stealth]:not(:hover)::part(control), -::deep.layout > header fluent-anchor[appearance=stealth]:not(:hover)::part(control), -::deep.layout > header fluent-anchor[appearance=stealth].logo::part(control), +::deep.layout > header > .header-gutters > fluent-button[appearance=stealth]:not(:hover)::part(control), +::deep.layout > header > .header-gutters > fluent-anchor[appearance=stealth]:not(:hover)::part(control), +::deep.layout > header > .header-gutters > fluent-anchor[appearance=stealth].logo::part(control), ::deep.layout > .aspire-icon fluent-anchor[appearance=stealth].logo::part(control) { background-color: var(--neutral-layer-4); } @@ -86,7 +87,7 @@ margin-bottom: 0; } -::deep.layout > header fluent-anchor { +::deep.layout > header > .header-gutters > fluent-anchor { font-size: var(--type-ramp-plus-2-font-size); } diff --git a/src/Aspire.Dashboard/Components/_Imports.razor b/src/Aspire.Dashboard/Components/_Imports.razor index 4940736b97..f39980ae12 100644 --- a/src/Aspire.Dashboard/Components/_Imports.razor +++ b/src/Aspire.Dashboard/Components/_Imports.razor @@ -3,6 +3,7 @@ @using Aspire.Dashboard.Authentication @using Microsoft.AspNetCore.Authentication.OpenIdConnect @using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web diff --git a/src/Aspire.Dashboard/Configuration/DashboardOptions.cs b/src/Aspire.Dashboard/Configuration/DashboardOptions.cs index 6b78af2d75..834f39b9a7 100644 --- a/src/Aspire.Dashboard/Configuration/DashboardOptions.cs +++ b/src/Aspire.Dashboard/Configuration/DashboardOptions.cs @@ -16,6 +16,7 @@ public sealed class DashboardOptions public FrontendOptions Frontend { get; set; } = new FrontendOptions(); public ResourceServiceClientOptions ResourceServiceClient { get; set; } = new ResourceServiceClientOptions(); public TelemetryLimitOptions TelemetryLimits { get; set; } = new TelemetryLimitOptions(); + public OpenIdConnectOptions OpenIdConnect { get; set; } = new OpenIdConnectOptions(); } // Don't set values after validating/parsing options. @@ -163,3 +164,52 @@ public sealed class TelemetryLimitOptions public int MaxAttributeLength { get; set; } = int.MaxValue; public int MaxSpanEventCount { get; set; } = int.MaxValue; } + +public sealed class OpenIdConnectOptions +{ + private string[]? _nameClaimTypes; + private string[]? _usernameClaimTypes; + + public string NameClaimType { get; set; } = "name"; + public string UsernameClaimType { get; set; } = "preferred_username"; + + public string[] GetNameClaimTypes() + { + Debug.Assert(_nameClaimTypes is not null, "Should have been parsed during validation."); + return _nameClaimTypes; + } + + public string[] GetUsernameClaimTypes() + { + Debug.Assert(_usernameClaimTypes is not null, "Should have been parsed during validation."); + return _usernameClaimTypes; + } + + internal bool TryParseOptions([NotNullWhen(false)] out IEnumerable? errorMessages) + { + List? messages = null; + if (string.IsNullOrWhiteSpace(NameClaimType)) + { + messages ??= []; + messages.Add("OpenID Connect claim type for name not configured. Specify a Dashboard:OpenIdConnect:NameClaimType value."); + } + else + { + _nameClaimTypes = NameClaimType.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.RemoveEmptyEntries); + } + + if (string.IsNullOrWhiteSpace(UsernameClaimType)) + { + messages ??= []; + messages.Add("OpenID Connect claim type for username not configured. Specify a Dashboard:OpenIdConnect:UsernameClaimType value."); + } + else + { + _usernameClaimTypes = UsernameClaimType.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.RemoveEmptyEntries); + } + + errorMessages = messages; + + return messages is null; + } +} diff --git a/src/Aspire.Dashboard/Configuration/ValidateDashboardOptions.cs b/src/Aspire.Dashboard/Configuration/ValidateDashboardOptions.cs index 961d8395d8..e9c719c0ed 100644 --- a/src/Aspire.Dashboard/Configuration/ValidateDashboardOptions.cs +++ b/src/Aspire.Dashboard/Configuration/ValidateDashboardOptions.cs @@ -100,6 +100,11 @@ public ValidateOptionsResult Validate(string? name, DashboardOptions options) } } + if (!options.OpenIdConnect.TryParseOptions(out var messages)) + { + errorMessages.AddRange(messages); + } + return errorMessages.Count > 0 ? ValidateOptionsResult.Fail(errorMessages) : ValidateOptionsResult.Success; diff --git a/src/Aspire.Dashboard/DashboardWebApplication.cs b/src/Aspire.Dashboard/DashboardWebApplication.cs index 3edde2f1d1..4c38aec901 100644 --- a/src/Aspire.Dashboard/DashboardWebApplication.cs +++ b/src/Aspire.Dashboard/DashboardWebApplication.cs @@ -259,6 +259,10 @@ await Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.Si }); #endif } + else if (dashboardOptions.Frontend.AuthMode == FrontendAuthMode.OpenIdConnect) + { + _app.MapPost("/authentication/logout", () => TypedResults.SignOut(authenticationSchemes: [CookieAuthenticationDefaults.AuthenticationScheme, "OpenIdConnect"])); + } } /// diff --git a/src/Aspire.Dashboard/Extensions/ClaimsIdentityExtensions.cs b/src/Aspire.Dashboard/Extensions/ClaimsIdentityExtensions.cs new file mode 100644 index 0000000000..a20bf22dbc --- /dev/null +++ b/src/Aspire.Dashboard/Extensions/ClaimsIdentityExtensions.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Security.Claims; + +namespace Aspire.Dashboard.Extensions; + +public static class ClaimsIdentityExtensions +{ + /// + /// Searches the claims in the for each of the claim types in + /// in the order presented and returns the first one that it finds. + /// + public static string? FindFirst(this ClaimsIdentity identity, string[] claimTypes) + { + if (identity is null) + { + return null; + } + + foreach (var claimType in claimTypes) + { + var claim = identity.FindFirst(claimType); + if (claim is not null) + { + return claim.Value; + } + } + + return null; + } +} diff --git a/src/Aspire.Dashboard/Extensions/StringExtensions.cs b/src/Aspire.Dashboard/Extensions/StringExtensions.cs index 41289b069e..3514114182 100644 --- a/src/Aspire.Dashboard/Extensions/StringExtensions.cs +++ b/src/Aspire.Dashboard/Extensions/StringExtensions.cs @@ -7,24 +7,6 @@ namespace Aspire.Dashboard.Extensions; internal static class StringExtensions { - /// - /// Shortens a string by replacing the middle with an ellipsis. - /// - /// The string to shorten - /// The max length of the result - public static string TrimMiddle(this string text, int maxLength) - { - if (text.Length <= maxLength) - { - return text; - } - - var firstPart = (maxLength - 1) / 2; - var lastPart = firstPart + ((maxLength - 1) % 2); - - return $"{text[..firstPart]}…{text[^lastPart..]}"; - } - public static string SanitizeHtmlId(this string input) { var sanitizedBuilder = new StringBuilder(capacity: input.Length); @@ -49,4 +31,29 @@ static bool IsValidHtmlIdCharacter(char c) return char.IsLetterOrDigit(c) || c == '_' || c == '-'; } } + + /// + /// Returns the two initial letters of the first and last words in the specified . + /// If only one word is present, a single initial is returned. If is null, empty or + /// white space only, is returned. + /// + public static string? GetInitials(this string name, string? defaultValue = default) + { + if (string.IsNullOrWhiteSpace(name)) + { + return defaultValue; + } + + var initials = name!.Split(" ", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .Select(s => s[0].ToString()) + .ToList(); + + if (initials.Count > 1) + { + // If the name contained two or more words, return the initials from the first and last + return initials[0].ToUpperInvariant() + initials[^1].ToUpperInvariant(); + } + + return initials[0].ToUpperInvariant(); + } } diff --git a/src/Aspire.Dashboard/README.md b/src/Aspire.Dashboard/README.md index e1d01dede8..4b7540a99a 100644 --- a/src/Aspire.Dashboard/README.md +++ b/src/Aspire.Dashboard/README.md @@ -41,10 +41,12 @@ Example JSON configuration file: The dashboard's frontend supports OpenID Connect (OIDC). Set `Dashboard:Frontend:AuthMode` to `OpenIdConnect`, then add the following configuration: -- `Authentication:Schemes:OpenIdConnect:Authority` — URL to the identity provider (IdP) -- `Authentication:Schemes:OpenIdConnect:ClientId` — Identity of the relying party (RP) -- `Authentication:Schemes:OpenIdConnect:ClientSecret`— A secret that only the real RP would know +- `Authentication:Schemes:OpenIdConnect:Authority` URL to the identity provider (IdP) +- `Authentication:Schemes:OpenIdConnect:ClientId` Identity of the relying party (RP) +- `Authentication:Schemes:OpenIdConnect:ClientSecret` A secret that only the real RP would know - Other properties of [`OpenIdConnectOptions`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.builder.openidconnectoptions) specified in configuration container `Authentication:Schemes:OpenIdConnect:*` +- `Dashboard:OpenIdConnect:NameClaimType` specifies the claim type(s) that should be used to display the authenticated user's full name. Can be a single claim type or a comma-delimited list of claim types. Defaults to `name`. +- `Dashboard:OpenIdConnect:UsernameClaimType` specifies the claim type(s) that should be used to display the authenticated user's username. Can be a single claim type or a comma-delimited list of claim types. Defaults to `preferred_username`. It may also be run unsecured. Set `Dashboard:Frontend:AuthMode` to `Unsecured`. The frontend endpoint will allow anonymous access. This setting is used during local development, but is not recommended if you attempt to host the dashboard in other settings. diff --git a/src/Aspire.Dashboard/Resources/Login.Designer.cs b/src/Aspire.Dashboard/Resources/Login.Designer.cs index 194cf38752..b2c956bca3 100644 --- a/src/Aspire.Dashboard/Resources/Login.Designer.cs +++ b/src/Aspire.Dashboard/Resources/Login.Designer.cs @@ -60,6 +60,15 @@ internal Login() { } } + /// + /// Looks up a localized string similar to Authorized User. + /// + public static string AuthorizedUser { + get { + return ResourceManager.GetString("AuthorizedUser", resourceCulture); + } + } + /// /// Looks up a localized string similar to {0} dashboard. /// diff --git a/src/Aspire.Dashboard/Resources/Login.resx b/src/Aspire.Dashboard/Resources/Login.resx index 529c2e9270..e37c5af563 100644 --- a/src/Aspire.Dashboard/Resources/Login.resx +++ b/src/Aspire.Dashboard/Resources/Login.resx @@ -117,6 +117,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Authorized User + Placeholder text for the name of the authorized user when no name claim exists + {0} dashboard {0} is an application name diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.cs.xlf index d01a3992ec..2950e1587d 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.cs.xlf @@ -2,6 +2,11 @@ + + Authorized User + Authorized User + Placeholder text for the name of the authorized user when no name claim exists + {0} dashboard {0} dashboard diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.de.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.de.xlf index 9d1ad51175..677154ac3a 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.de.xlf @@ -2,6 +2,11 @@ + + Authorized User + Authorized User + Placeholder text for the name of the authorized user when no name claim exists + {0} dashboard {0} dashboard diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.es.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.es.xlf index d1ce0e325e..c763f8727f 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.es.xlf @@ -2,6 +2,11 @@ + + Authorized User + Authorized User + Placeholder text for the name of the authorized user when no name claim exists + {0} dashboard {0} dashboard diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.fr.xlf index 98e20da8b3..5b4a924646 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.fr.xlf @@ -2,6 +2,11 @@ + + Authorized User + Authorized User + Placeholder text for the name of the authorized user when no name claim exists + {0} dashboard {0} dashboard diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.it.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.it.xlf index 742154835e..6cd1c7e59b 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.it.xlf @@ -2,6 +2,11 @@ + + Authorized User + Authorized User + Placeholder text for the name of the authorized user when no name claim exists + {0} dashboard {0} dashboard diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.ja.xlf index 5199f6482c..cd29f091e5 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.ja.xlf @@ -2,6 +2,11 @@ + + Authorized User + Authorized User + Placeholder text for the name of the authorized user when no name claim exists + {0} dashboard {0} dashboard diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.ko.xlf index 090f7ea606..5abdf0f23f 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.ko.xlf @@ -2,6 +2,11 @@ + + Authorized User + Authorized User + Placeholder text for the name of the authorized user when no name claim exists + {0} dashboard {0} dashboard diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.pl.xlf index 5e8c63ca0f..af8f686521 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.pl.xlf @@ -2,6 +2,11 @@ + + Authorized User + Authorized User + Placeholder text for the name of the authorized user when no name claim exists + {0} dashboard {0} dashboard diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.pt-BR.xlf index 21ce131cb3..959bbb55dc 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.pt-BR.xlf @@ -2,6 +2,11 @@ + + Authorized User + Authorized User + Placeholder text for the name of the authorized user when no name claim exists + {0} dashboard {0} dashboard diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.ru.xlf index 8c8c300f1d..462b173f58 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.ru.xlf @@ -2,6 +2,11 @@ + + Authorized User + Authorized User + Placeholder text for the name of the authorized user when no name claim exists + {0} dashboard {0} dashboard diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.tr.xlf index a5bdb9f800..9025ca3398 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.tr.xlf @@ -2,6 +2,11 @@ + + Authorized User + Authorized User + Placeholder text for the name of the authorized user when no name claim exists + {0} dashboard {0} dashboard diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hans.xlf index 374cb05194..0ca128d0fd 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hans.xlf @@ -2,6 +2,11 @@ + + Authorized User + Authorized User + Placeholder text for the name of the authorized user when no name claim exists + {0} dashboard {0} dashboard diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hant.xlf index ac3cf3162c..77225fab9c 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hant.xlf @@ -2,6 +2,11 @@ + + Authorized User + Authorized User + Placeholder text for the name of the authorized user when no name claim exists + {0} dashboard {0} dashboard diff --git a/tests/Aspire.Dashboard.Tests/StringExtensionsTests.cs b/tests/Aspire.Dashboard.Tests/StringExtensionsTests.cs index 8e35757b5e..cb29556174 100644 --- a/tests/Aspire.Dashboard.Tests/StringExtensionsTests.cs +++ b/tests/Aspire.Dashboard.Tests/StringExtensionsTests.cs @@ -9,21 +9,27 @@ namespace Aspire.Dashboard.Tests; public class StringExtensionsTests { [Theory] - [InlineData("/usr", 5, "/usr")] - [InlineData("~/src/repos/AspireStarterProject/MyApp/MyApp.csproj", 5, "~/…oj")] - [InlineData("~/src/repos/AspireStarterProject/MyApp/MyApp.csproj", 25, "~/src/repos/…MyApp.csproj")] - [InlineData("~/src/repos/AspireStarterProject/MyApp/MyApp.csproj", 26, "~/src/repos/…/MyApp.csproj")] - [InlineData("/home/username/src/repos/AspireStarterProject/MyApp/MyApp.csproj", 5, "/h…oj")] - [InlineData("/home/username/src/repos/AspireStarterProject/MyApp/MyApp.csproj", 38, "/home/username/src…/MyApp/MyApp.csproj")] - [InlineData("/home/username/src/repos/AspireStarterProject/MyApp/MyApp.csproj", 39, "/home/username/src/…/MyApp/MyApp.csproj")] - [InlineData("c:\\", 5, "c:\\")] - [InlineData("c:\\src\\repos\\AspireStarterProject\\MyApp\\MyApp.csproj", 5, "c:…oj")] - [InlineData("c:\\src\\repos\\AspireStarterProject\\MyApp\\MyApp.csproj", 32, "c:\\src\\repos\\As…App\\MyApp.csproj")] - [InlineData("c:\\src\\repos\\AspireStarterProject\\MyApp\\MyApp.csproj", 33, "c:\\src\\repos\\Asp…App\\MyApp.csproj")] - [InlineData("c:\\src\\repos\\AspireStarterProject\\MyApp\\MyApp.csproj", 48, "c:\\src\\repos\\AspireStar…oject\\MyApp\\MyApp.csproj")] - public void TrimMiddle(string path, int targetLength, string expectedResult) + [InlineData("", "DefaultValue", "DefaultValue")] + [InlineData("SingleNameOnly", null, "S")] + [InlineData("singleNameOnly", null, "S")] + [InlineData("Two Names", null, "TN")] + [InlineData("two Names", null, "TN")] + [InlineData("Two names", null, "TN")] + [InlineData("two names", null, "TN")] + [InlineData("With Three Names", null, "WN")] + [InlineData("with Three Names", null, "WN")] + [InlineData("With Three names", null, "WN")] + [InlineData("with Three names", null, "WN")] + [InlineData("With Hyphenated-Name", null, "WH")] + [InlineData("with Hyphenated-Name", null, "WH")] + [InlineData("With hyphenated-Name", null, "WH")] + [InlineData("With Hyphenated-name", null, "WH")] + [InlineData("with hyphenated-Name", null, "WH")] + [InlineData("with Hyphenated-name", null, "WH")] + [InlineData("with hyphenated-name", null, "WH")] + public void GetInitials(string name, string? defaultValue, string expectedResult) { - var actual = StringExtensions.TrimMiddle(path, targetLength); + var actual = StringExtensions.GetInitials(name, defaultValue); Assert.Equal(expectedResult, actual); } From d6baf32360515a7268d5ef11b378061fbbf5a2f5 Mon Sep 17 00:00:00 2001 From: Tim Mulholland Date: Sun, 7 Apr 2024 21:28:41 -0700 Subject: [PATCH 02/11] CR feedback --- .../Components/Controls/UserProfile.razor | 2 +- .../Components/Layout/MainLayout.razor.css | 1 - .../Extensions/ClaimsIdentityExtensions.cs | 2 +- src/Aspire.Dashboard/Resources/Login.Designer.cs | 9 +++++++++ src/Aspire.Dashboard/Resources/Login.resx | 3 +++ src/Aspire.Dashboard/Resources/xlf/Login.cs.xlf | 5 +++++ src/Aspire.Dashboard/Resources/xlf/Login.de.xlf | 5 +++++ src/Aspire.Dashboard/Resources/xlf/Login.es.xlf | 5 +++++ src/Aspire.Dashboard/Resources/xlf/Login.fr.xlf | 5 +++++ src/Aspire.Dashboard/Resources/xlf/Login.it.xlf | 5 +++++ src/Aspire.Dashboard/Resources/xlf/Login.ja.xlf | 5 +++++ src/Aspire.Dashboard/Resources/xlf/Login.ko.xlf | 5 +++++ src/Aspire.Dashboard/Resources/xlf/Login.pl.xlf | 5 +++++ src/Aspire.Dashboard/Resources/xlf/Login.pt-BR.xlf | 5 +++++ src/Aspire.Dashboard/Resources/xlf/Login.ru.xlf | 5 +++++ src/Aspire.Dashboard/Resources/xlf/Login.tr.xlf | 5 +++++ src/Aspire.Dashboard/Resources/xlf/Login.zh-Hans.xlf | 5 +++++ src/Aspire.Dashboard/Resources/xlf/Login.zh-Hant.xlf | 5 +++++ 18 files changed, 79 insertions(+), 3 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Controls/UserProfile.razor b/src/Aspire.Dashboard/Components/Controls/UserProfile.razor index 6251e54e30..7a2c1530bb 100644 --- a/src/Aspire.Dashboard/Components/Controls/UserProfile.razor +++ b/src/Aspire.Dashboard/Components/Controls/UserProfile.razor @@ -10,7 +10,7 @@ FooterLink=""> - OpenID Connect + @Loc[nameof(Resources.Login.SignedInAs)]
diff --git a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.css b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.css index a30d64650f..0cc0d10f4d 100644 --- a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.css +++ b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.css @@ -50,7 +50,6 @@ ::deep.layout > header { grid-area: head; - /*overflow-x: clip;*/ } ::deep.layout > .nav-menu-container { diff --git a/src/Aspire.Dashboard/Extensions/ClaimsIdentityExtensions.cs b/src/Aspire.Dashboard/Extensions/ClaimsIdentityExtensions.cs index a20bf22dbc..a1deca0f3c 100644 --- a/src/Aspire.Dashboard/Extensions/ClaimsIdentityExtensions.cs +++ b/src/Aspire.Dashboard/Extensions/ClaimsIdentityExtensions.cs @@ -5,7 +5,7 @@ namespace Aspire.Dashboard.Extensions; -public static class ClaimsIdentityExtensions +internal static class ClaimsIdentityExtensions { /// /// Searches the claims in the for each of the claim types in diff --git a/src/Aspire.Dashboard/Resources/Login.Designer.cs b/src/Aspire.Dashboard/Resources/Login.Designer.cs index b2c956bca3..fb48ab9efe 100644 --- a/src/Aspire.Dashboard/Resources/Login.Designer.cs +++ b/src/Aspire.Dashboard/Resources/Login.Designer.cs @@ -132,6 +132,15 @@ public static string PageTitle { } } + /// + /// Looks up a localized string similar to Signed in as:. + /// + public static string SignedInAs { + get { + return ResourceManager.GetString("SignedInAs", resourceCulture); + } + } + /// /// Looks up a localized string similar to Enter token to log in.... /// diff --git a/src/Aspire.Dashboard/Resources/Login.resx b/src/Aspire.Dashboard/Resources/Login.resx index e37c5af563..77beea868f 100644 --- a/src/Aspire.Dashboard/Resources/Login.resx +++ b/src/Aspire.Dashboard/Resources/Login.resx @@ -144,6 +144,9 @@ {0} login {0} is an application name + + Signed in as: + Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.cs.xlf index 2950e1587d..c3eda1c2d4 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.cs.xlf @@ -42,6 +42,11 @@ {0} login {0} is an application name + + Signed in as: + Signed in as: + + Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.de.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.de.xlf index 677154ac3a..2b527859a3 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.de.xlf @@ -42,6 +42,11 @@ {0} login {0} is an application name + + Signed in as: + Signed in as: + + Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.es.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.es.xlf index c763f8727f..c071e79a0f 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.es.xlf @@ -42,6 +42,11 @@ {0} login {0} is an application name + + Signed in as: + Signed in as: + + Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.fr.xlf index 5b4a924646..7a66125b25 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.fr.xlf @@ -42,6 +42,11 @@ {0} login {0} is an application name + + Signed in as: + Signed in as: + + Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.it.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.it.xlf index 6cd1c7e59b..24bd7031f0 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.it.xlf @@ -42,6 +42,11 @@ {0} login {0} is an application name + + Signed in as: + Signed in as: + + Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.ja.xlf index cd29f091e5..d5e1ac8946 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.ja.xlf @@ -42,6 +42,11 @@ {0} login {0} is an application name + + Signed in as: + Signed in as: + + Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.ko.xlf index 5abdf0f23f..c851f139f0 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.ko.xlf @@ -42,6 +42,11 @@ {0} login {0} is an application name + + Signed in as: + Signed in as: + + Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.pl.xlf index af8f686521..cdc13b16e6 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.pl.xlf @@ -42,6 +42,11 @@ {0} login {0} is an application name + + Signed in as: + Signed in as: + + Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.pt-BR.xlf index 959bbb55dc..e010cce917 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.pt-BR.xlf @@ -42,6 +42,11 @@ {0} login {0} is an application name + + Signed in as: + Signed in as: + + Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.ru.xlf index 462b173f58..0c2f596863 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.ru.xlf @@ -42,6 +42,11 @@ {0} login {0} is an application name + + Signed in as: + Signed in as: + + Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.tr.xlf index 9025ca3398..ccd2563ebb 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.tr.xlf @@ -42,6 +42,11 @@ {0} login {0} is an application name + + Signed in as: + Signed in as: + + Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hans.xlf index 0ca128d0fd..dcdc9a3bc1 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hans.xlf @@ -42,6 +42,11 @@ {0} login {0} is an application name + + Signed in as: + Signed in as: + + Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hant.xlf index 77225fab9c..d6870fb766 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hant.xlf @@ -42,6 +42,11 @@ {0} login {0} is an application name + + Signed in as: + Signed in as: + + Enter token to log in... Enter token to log in... From 4a60f7ef75496c5e607fba83e2c5fc53d9aa01a2 Mon Sep 17 00:00:00 2001 From: Tim Mulholland Date: Mon, 8 Apr 2024 12:20:45 -0700 Subject: [PATCH 03/11] Signed in as -> Logged in as --- .../Components/Controls/UserProfile.razor | 2 +- .../Resources/Login.Designer.cs | 18 +++++++++--------- src/Aspire.Dashboard/Resources/Login.resx | 6 +++--- .../Resources/xlf/Login.cs.xlf | 10 +++++----- .../Resources/xlf/Login.de.xlf | 10 +++++----- .../Resources/xlf/Login.es.xlf | 10 +++++----- .../Resources/xlf/Login.fr.xlf | 10 +++++----- .../Resources/xlf/Login.it.xlf | 10 +++++----- .../Resources/xlf/Login.ja.xlf | 10 +++++----- .../Resources/xlf/Login.ko.xlf | 10 +++++----- .../Resources/xlf/Login.pl.xlf | 10 +++++----- .../Resources/xlf/Login.pt-BR.xlf | 10 +++++----- .../Resources/xlf/Login.ru.xlf | 10 +++++----- .../Resources/xlf/Login.tr.xlf | 10 +++++----- .../Resources/xlf/Login.zh-Hans.xlf | 10 +++++----- .../Resources/xlf/Login.zh-Hant.xlf | 10 +++++----- 16 files changed, 78 insertions(+), 78 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Controls/UserProfile.razor b/src/Aspire.Dashboard/Components/Controls/UserProfile.razor index 7a2c1530bb..4689efe062 100644 --- a/src/Aspire.Dashboard/Components/Controls/UserProfile.razor +++ b/src/Aspire.Dashboard/Components/Controls/UserProfile.razor @@ -10,7 +10,7 @@ FooterLink=""> - @Loc[nameof(Resources.Login.SignedInAs)] + @Loc[nameof(Resources.Login.LoggedInAs)] diff --git a/src/Aspire.Dashboard/Resources/Login.Designer.cs b/src/Aspire.Dashboard/Resources/Login.Designer.cs index fb48ab9efe..05a3acbe20 100644 --- a/src/Aspire.Dashboard/Resources/Login.Designer.cs +++ b/src/Aspire.Dashboard/Resources/Login.Designer.cs @@ -105,6 +105,15 @@ public static string InvalidTokenErrorMessage { } } + /// + /// Looks up a localized string similar to Logged in as:. + /// + public static string LoggedInAs { + get { + return ResourceManager.GetString("LoggedInAs", resourceCulture); + } + } + /// /// Looks up a localized string similar to Log in. /// @@ -132,15 +141,6 @@ public static string PageTitle { } } - /// - /// Looks up a localized string similar to Signed in as:. - /// - public static string SignedInAs { - get { - return ResourceManager.GetString("SignedInAs", resourceCulture); - } - } - /// /// Looks up a localized string similar to Enter token to log in.... /// diff --git a/src/Aspire.Dashboard/Resources/Login.resx b/src/Aspire.Dashboard/Resources/Login.resx index 77beea868f..bc17f59099 100644 --- a/src/Aspire.Dashboard/Resources/Login.resx +++ b/src/Aspire.Dashboard/Resources/Login.resx @@ -134,6 +134,9 @@ Invalid token. Please try again + + Logged in as: + Log in @@ -144,9 +147,6 @@ {0} login {0} is an application name - - Signed in as: - Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.cs.xlf index c3eda1c2d4..5480f45066 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.cs.xlf @@ -32,6 +32,11 @@ Log in + + Logged in as: + Logged in as: + + More info More info @@ -42,11 +47,6 @@ {0} login {0} is an application name - - Signed in as: - Signed in as: - - Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.de.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.de.xlf index 2b527859a3..b8099b1306 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.de.xlf @@ -32,6 +32,11 @@ Log in + + Logged in as: + Logged in as: + + More info More info @@ -42,11 +47,6 @@ {0} login {0} is an application name - - Signed in as: - Signed in as: - - Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.es.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.es.xlf index c071e79a0f..02a1e3ca0a 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.es.xlf @@ -32,6 +32,11 @@ Log in + + Logged in as: + Logged in as: + + More info More info @@ -42,11 +47,6 @@ {0} login {0} is an application name - - Signed in as: - Signed in as: - - Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.fr.xlf index 7a66125b25..99aaf125cd 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.fr.xlf @@ -32,6 +32,11 @@ Log in + + Logged in as: + Logged in as: + + More info More info @@ -42,11 +47,6 @@ {0} login {0} is an application name - - Signed in as: - Signed in as: - - Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.it.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.it.xlf index 24bd7031f0..2fa153aaf2 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.it.xlf @@ -32,6 +32,11 @@ Log in + + Logged in as: + Logged in as: + + More info More info @@ -42,11 +47,6 @@ {0} login {0} is an application name - - Signed in as: - Signed in as: - - Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.ja.xlf index d5e1ac8946..5b85cfbdc8 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.ja.xlf @@ -32,6 +32,11 @@ Log in + + Logged in as: + Logged in as: + + More info More info @@ -42,11 +47,6 @@ {0} login {0} is an application name - - Signed in as: - Signed in as: - - Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.ko.xlf index c851f139f0..3decc703a0 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.ko.xlf @@ -32,6 +32,11 @@ Log in + + Logged in as: + Logged in as: + + More info More info @@ -42,11 +47,6 @@ {0} login {0} is an application name - - Signed in as: - Signed in as: - - Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.pl.xlf index cdc13b16e6..31b96606c4 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.pl.xlf @@ -32,6 +32,11 @@ Log in + + Logged in as: + Logged in as: + + More info More info @@ -42,11 +47,6 @@ {0} login {0} is an application name - - Signed in as: - Signed in as: - - Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.pt-BR.xlf index e010cce917..4856d93777 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.pt-BR.xlf @@ -32,6 +32,11 @@ Log in + + Logged in as: + Logged in as: + + More info More info @@ -42,11 +47,6 @@ {0} login {0} is an application name - - Signed in as: - Signed in as: - - Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.ru.xlf index 0c2f596863..9594779ccf 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.ru.xlf @@ -32,6 +32,11 @@ Log in + + Logged in as: + Logged in as: + + More info More info @@ -42,11 +47,6 @@ {0} login {0} is an application name - - Signed in as: - Signed in as: - - Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.tr.xlf index ccd2563ebb..c0a8585eb6 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.tr.xlf @@ -32,6 +32,11 @@ Log in + + Logged in as: + Logged in as: + + More info More info @@ -42,11 +47,6 @@ {0} login {0} is an application name - - Signed in as: - Signed in as: - - Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hans.xlf index dcdc9a3bc1..d10a25e928 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hans.xlf @@ -32,6 +32,11 @@ Log in + + Logged in as: + Logged in as: + + More info More info @@ -42,11 +47,6 @@ {0} login {0} is an application name - - Signed in as: - Signed in as: - - Enter token to log in... Enter token to log in... diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hant.xlf index d6870fb766..02248344ff 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hant.xlf @@ -32,6 +32,11 @@ Log in + + Logged in as: + Logged in as: + + More info More info @@ -42,11 +47,6 @@ {0} login {0} is an application name - - Signed in as: - Signed in as: - - Enter token to log in... Enter token to log in... From 8141935073f1e28d8f92ef323008e3e78c0ea8ae Mon Sep 17 00:00:00 2001 From: Tim Mulholland Date: Mon, 8 Apr 2024 12:28:05 -0700 Subject: [PATCH 04/11] Nest OpenIdConnect options under Frontend --- src/Aspire.Dashboard/Components/Controls/UserProfile.razor.cs | 4 ++-- src/Aspire.Dashboard/Configuration/DashboardOptions.cs | 2 +- .../Configuration/ValidateDashboardOptions.cs | 2 +- src/Aspire.Dashboard/README.md | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Controls/UserProfile.razor.cs b/src/Aspire.Dashboard/Components/Controls/UserProfile.razor.cs index 5c302c9920..1a471cbf67 100644 --- a/src/Aspire.Dashboard/Components/Controls/UserProfile.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/UserProfile.razor.cs @@ -48,14 +48,14 @@ protected override async Task OnParametersSetAsync() if (claimsIdentity?.IsAuthenticated == true) { _showUserProfileMenu = true; - _name = claimsIdentity.FindFirst(DashboardOptions.CurrentValue.OpenIdConnect.GetNameClaimTypes()); + _name = claimsIdentity.FindFirst(DashboardOptions.CurrentValue.Frontend.OpenIdConnect.GetNameClaimTypes()); if (string.IsNullOrWhiteSpace(_name)) { // Make sure there's always a name, even if that name is a placeholder _name = Loc[nameof(Login.AuthorizedUser)]; } - _username = claimsIdentity.FindFirst(DashboardOptions.CurrentValue.OpenIdConnect.GetUsernameClaimTypes()); + _username = claimsIdentity.FindFirst(DashboardOptions.CurrentValue.Frontend.OpenIdConnect.GetUsernameClaimTypes()); _initials = _name.GetInitials(); } else diff --git a/src/Aspire.Dashboard/Configuration/DashboardOptions.cs b/src/Aspire.Dashboard/Configuration/DashboardOptions.cs index 834f39b9a7..5514e11454 100644 --- a/src/Aspire.Dashboard/Configuration/DashboardOptions.cs +++ b/src/Aspire.Dashboard/Configuration/DashboardOptions.cs @@ -16,7 +16,6 @@ public sealed class DashboardOptions public FrontendOptions Frontend { get; set; } = new FrontendOptions(); public ResourceServiceClientOptions ResourceServiceClient { get; set; } = new ResourceServiceClientOptions(); public TelemetryLimitOptions TelemetryLimits { get; set; } = new TelemetryLimitOptions(); - public OpenIdConnectOptions OpenIdConnect { get; set; } = new OpenIdConnectOptions(); } // Don't set values after validating/parsing options. @@ -115,6 +114,7 @@ public sealed class FrontendOptions public string? EndpointUrls { get; set; } public FrontendAuthMode? AuthMode { get; set; } public string? BrowserToken { get; set; } + public OpenIdConnectOptions OpenIdConnect { get; set; } = new OpenIdConnectOptions(); public byte[]? GetBrowserTokenBytes() => _browserTokenBytes; diff --git a/src/Aspire.Dashboard/Configuration/ValidateDashboardOptions.cs b/src/Aspire.Dashboard/Configuration/ValidateDashboardOptions.cs index e9c719c0ed..a5b7e3b643 100644 --- a/src/Aspire.Dashboard/Configuration/ValidateDashboardOptions.cs +++ b/src/Aspire.Dashboard/Configuration/ValidateDashboardOptions.cs @@ -100,7 +100,7 @@ public ValidateOptionsResult Validate(string? name, DashboardOptions options) } } - if (!options.OpenIdConnect.TryParseOptions(out var messages)) + if (!options.Frontend.OpenIdConnect.TryParseOptions(out var messages)) { errorMessages.AddRange(messages); } diff --git a/src/Aspire.Dashboard/README.md b/src/Aspire.Dashboard/README.md index 4b7540a99a..1b01a85ad8 100644 --- a/src/Aspire.Dashboard/README.md +++ b/src/Aspire.Dashboard/README.md @@ -45,8 +45,8 @@ The dashboard's frontend supports OpenID Connect (OIDC). Set `Dashboard:Frontend - `Authentication:Schemes:OpenIdConnect:ClientId` Identity of the relying party (RP) - `Authentication:Schemes:OpenIdConnect:ClientSecret` A secret that only the real RP would know - Other properties of [`OpenIdConnectOptions`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.builder.openidconnectoptions) specified in configuration container `Authentication:Schemes:OpenIdConnect:*` -- `Dashboard:OpenIdConnect:NameClaimType` specifies the claim type(s) that should be used to display the authenticated user's full name. Can be a single claim type or a comma-delimited list of claim types. Defaults to `name`. -- `Dashboard:OpenIdConnect:UsernameClaimType` specifies the claim type(s) that should be used to display the authenticated user's username. Can be a single claim type or a comma-delimited list of claim types. Defaults to `preferred_username`. +- `Dashboard:Frontend:OpenIdConnect:NameClaimType` specifies the claim type(s) that should be used to display the authenticated user's full name. Can be a single claim type or a comma-delimited list of claim types. Defaults to `name`. +- `Dashboard:Frontend:OpenIdConnect:UsernameClaimType` specifies the claim type(s) that should be used to display the authenticated user's username. Can be a single claim type or a comma-delimited list of claim types. Defaults to `preferred_username`. It may also be run unsecured. Set `Dashboard:Frontend:AuthMode` to `Unsecured`. The frontend endpoint will allow anonymous access. This setting is used during local development, but is not recommended if you attempt to host the dashboard in other settings. From 11da055c9bb7e491fe48c6533a0c6880fbc9f230 Mon Sep 17 00:00:00 2001 From: Tim Mulholland Date: Tue, 9 Apr 2024 23:26:15 -0700 Subject: [PATCH 05/11] Update src/Aspire.Dashboard/Configuration/DashboardOptions.cs Co-authored-by: Drew Noakes --- src/Aspire.Dashboard/Configuration/DashboardOptions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Aspire.Dashboard/Configuration/DashboardOptions.cs b/src/Aspire.Dashboard/Configuration/DashboardOptions.cs index 5514e11454..5202627048 100644 --- a/src/Aspire.Dashboard/Configuration/DashboardOptions.cs +++ b/src/Aspire.Dashboard/Configuration/DashboardOptions.cs @@ -165,6 +165,7 @@ public sealed class TelemetryLimitOptions public int MaxSpanEventCount { get; set; } = int.MaxValue; } +// Don't set values after validating/parsing options. public sealed class OpenIdConnectOptions { private string[]? _nameClaimTypes; From 080ba8a6ce59b5f84f8472a52a8cf76307dc0676 Mon Sep 17 00:00:00 2001 From: Tim Mulholland Date: Tue, 9 Apr 2024 23:36:02 -0700 Subject: [PATCH 06/11] Update src/Aspire.Dashboard/Configuration/DashboardOptions.cs Co-authored-by: Drew Noakes --- src/Aspire.Dashboard/Configuration/DashboardOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Dashboard/Configuration/DashboardOptions.cs b/src/Aspire.Dashboard/Configuration/DashboardOptions.cs index 5202627048..07393edb25 100644 --- a/src/Aspire.Dashboard/Configuration/DashboardOptions.cs +++ b/src/Aspire.Dashboard/Configuration/DashboardOptions.cs @@ -196,7 +196,7 @@ internal bool TryParseOptions([NotNullWhen(false)] out IEnumerable? erro } else { - _nameClaimTypes = NameClaimType.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.RemoveEmptyEntries); + _nameClaimTypes = NameClaimType.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); } if (string.IsNullOrWhiteSpace(UsernameClaimType)) From 4d884a020ff552f56e02a43401ab5633ab5aadd4 Mon Sep 17 00:00:00 2001 From: Tim Mulholland Date: Tue, 9 Apr 2024 23:36:32 -0700 Subject: [PATCH 07/11] Update src/Aspire.Dashboard/Configuration/DashboardOptions.cs Co-authored-by: Drew Noakes --- src/Aspire.Dashboard/Configuration/DashboardOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Dashboard/Configuration/DashboardOptions.cs b/src/Aspire.Dashboard/Configuration/DashboardOptions.cs index 07393edb25..71f9ae222e 100644 --- a/src/Aspire.Dashboard/Configuration/DashboardOptions.cs +++ b/src/Aspire.Dashboard/Configuration/DashboardOptions.cs @@ -206,7 +206,7 @@ internal bool TryParseOptions([NotNullWhen(false)] out IEnumerable? erro } else { - _usernameClaimTypes = UsernameClaimType.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.RemoveEmptyEntries); + _usernameClaimTypes = UsernameClaimType.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); } errorMessages = messages; From c5247948bbe20392d336f4b45dd68492fda1cecb Mon Sep 17 00:00:00 2001 From: Tim Mulholland Date: Tue, 9 Apr 2024 23:47:37 -0700 Subject: [PATCH 08/11] CR feedback --- .../Configuration/ValidateDashboardOptions.cs | 9 ++++----- src/Aspire.Dashboard/DashboardWebApplication.cs | 2 +- .../Extensions/ClaimsIdentityExtensions.cs | 5 ----- src/Aspire.Dashboard/Extensions/StringExtensions.cs | 8 +++++--- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/Aspire.Dashboard/Configuration/ValidateDashboardOptions.cs b/src/Aspire.Dashboard/Configuration/ValidateDashboardOptions.cs index a5b7e3b643..9321efd323 100644 --- a/src/Aspire.Dashboard/Configuration/ValidateDashboardOptions.cs +++ b/src/Aspire.Dashboard/Configuration/ValidateDashboardOptions.cs @@ -22,6 +22,10 @@ public ValidateOptionsResult Validate(string? name, DashboardOptions options) case FrontendAuthMode.Unsecured: break; case FrontendAuthMode.OpenIdConnect: + if (!options.Frontend.OpenIdConnect.TryParseOptions(out var messages)) + { + errorMessages.AddRange(messages); + } break; case FrontendAuthMode.BrowserToken: if (string.IsNullOrEmpty(options.Frontend.BrowserToken)) @@ -100,11 +104,6 @@ public ValidateOptionsResult Validate(string? name, DashboardOptions options) } } - if (!options.Frontend.OpenIdConnect.TryParseOptions(out var messages)) - { - errorMessages.AddRange(messages); - } - return errorMessages.Count > 0 ? ValidateOptionsResult.Fail(errorMessages) : ValidateOptionsResult.Success; diff --git a/src/Aspire.Dashboard/DashboardWebApplication.cs b/src/Aspire.Dashboard/DashboardWebApplication.cs index 4c38aec901..2a07215038 100644 --- a/src/Aspire.Dashboard/DashboardWebApplication.cs +++ b/src/Aspire.Dashboard/DashboardWebApplication.cs @@ -261,7 +261,7 @@ await Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.Si } else if (dashboardOptions.Frontend.AuthMode == FrontendAuthMode.OpenIdConnect) { - _app.MapPost("/authentication/logout", () => TypedResults.SignOut(authenticationSchemes: [CookieAuthenticationDefaults.AuthenticationScheme, "OpenIdConnect"])); + _app.MapPost("/authentication/logout", () => TypedResults.SignOut(authenticationSchemes: [CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme])); } } diff --git a/src/Aspire.Dashboard/Extensions/ClaimsIdentityExtensions.cs b/src/Aspire.Dashboard/Extensions/ClaimsIdentityExtensions.cs index a1deca0f3c..be91611c34 100644 --- a/src/Aspire.Dashboard/Extensions/ClaimsIdentityExtensions.cs +++ b/src/Aspire.Dashboard/Extensions/ClaimsIdentityExtensions.cs @@ -13,11 +13,6 @@ internal static class ClaimsIdentityExtensions /// public static string? FindFirst(this ClaimsIdentity identity, string[] claimTypes) { - if (identity is null) - { - return null; - } - foreach (var claimType in claimTypes) { var claim = identity.FindFirst(claimType); diff --git a/src/Aspire.Dashboard/Extensions/StringExtensions.cs b/src/Aspire.Dashboard/Extensions/StringExtensions.cs index 3514114182..97964a3481 100644 --- a/src/Aspire.Dashboard/Extensions/StringExtensions.cs +++ b/src/Aspire.Dashboard/Extensions/StringExtensions.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.Diagnostics.CodeAnalysis; using System.Text; namespace Aspire.Dashboard.Extensions; @@ -37,6 +38,7 @@ static bool IsValidHtmlIdCharacter(char c) /// If only one word is present, a single initial is returned. If is null, empty or /// white space only, is returned. ///
+ [return: NotNullIfNotNull(nameof(defaultValue))] public static string? GetInitials(this string name, string? defaultValue = default) { if (string.IsNullOrWhiteSpace(name)) @@ -44,9 +46,9 @@ static bool IsValidHtmlIdCharacter(char c) return defaultValue; } - var initials = name!.Split(" ", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) - .Select(s => s[0].ToString()) - .ToList(); + var initials = name.Split(" ", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .Select(s => s[0].ToString()) + .ToList(); if (initials.Count > 1) { From 8bb2f37cd3367d04709679279b8ce63a3e11627b Mon Sep 17 00:00:00 2001 From: Tim Mulholland Date: Wed, 10 Apr 2024 00:05:19 -0700 Subject: [PATCH 09/11] CR feedback --- .../Extensions/StringExtensions.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Aspire.Dashboard/Extensions/StringExtensions.cs b/src/Aspire.Dashboard/Extensions/StringExtensions.cs index 97964a3481..c201c1122e 100644 --- a/src/Aspire.Dashboard/Extensions/StringExtensions.cs +++ b/src/Aspire.Dashboard/Extensions/StringExtensions.cs @@ -41,21 +41,21 @@ static bool IsValidHtmlIdCharacter(char c) [return: NotNullIfNotNull(nameof(defaultValue))] public static string? GetInitials(this string name, string? defaultValue = default) { - if (string.IsNullOrWhiteSpace(name)) + var s = name.AsSpan().Trim(); + + if (s.Length == 0) { return defaultValue; } - var initials = name.Split(" ", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) - .Select(s => s[0].ToString()) - .ToList(); + var lastSpaceIndex = s.LastIndexOf(' '); - if (initials.Count > 1) + if (lastSpaceIndex == -1) { - // If the name contained two or more words, return the initials from the first and last - return initials[0].ToUpperInvariant() + initials[^1].ToUpperInvariant(); + return s[0].ToString().ToUpperInvariant(); } - return initials[0].ToUpperInvariant(); + // The name contained two or more words. Return the initials from the first and last. + return $"{char.ToUpperInvariant(s[0])}{char.ToUpperInvariant(s[lastSpaceIndex + 1])}"; } } From 98692a26d7726289a7b55cfe9092b6efbee159c7 Mon Sep 17 00:00:00 2001 From: Tim Mulholland Date: Wed, 10 Apr 2024 00:07:26 -0700 Subject: [PATCH 10/11] Update tests/Aspire.Dashboard.Tests/StringExtensionsTests.cs Co-authored-by: Drew Noakes --- tests/Aspire.Dashboard.Tests/StringExtensionsTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Aspire.Dashboard.Tests/StringExtensionsTests.cs b/tests/Aspire.Dashboard.Tests/StringExtensionsTests.cs index cb29556174..0ed63b3614 100644 --- a/tests/Aspire.Dashboard.Tests/StringExtensionsTests.cs +++ b/tests/Aspire.Dashboard.Tests/StringExtensionsTests.cs @@ -10,6 +10,8 @@ public class StringExtensionsTests { [Theory] [InlineData("", "DefaultValue", "DefaultValue")] + [InlineData(" ", "DefaultValue", "DefaultValue")] + [InlineData("\t", "DefaultValue", "DefaultValue")] [InlineData("SingleNameOnly", null, "S")] [InlineData("singleNameOnly", null, "S")] [InlineData("Two Names", null, "TN")] From bbdef3b909c8b268f0315cfc7062a31a2dd4ede1 Mon Sep 17 00:00:00 2001 From: Tim Mulholland Date: Wed, 10 Apr 2024 10:45:21 -0700 Subject: [PATCH 11/11] Update FluentUI Blazor reference --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c9daf59cb2..fe4f549a30 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -98,8 +98,8 @@ - - + +