+ }
+
+
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..1a471cbf67
--- /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.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.Frontend.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..0cc0d10f4d 100644
--- a/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.css
+++ b/src/Aspire.Dashboard/Components/Layout/MainLayout.razor.css
@@ -74,9 +74,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 +86,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..71f9ae222e 100644
--- a/src/Aspire.Dashboard/Configuration/DashboardOptions.cs
+++ b/src/Aspire.Dashboard/Configuration/DashboardOptions.cs
@@ -114,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;
@@ -163,3 +164,53 @@ public sealed class TelemetryLimitOptions
public int MaxAttributeLength { get; set; } = int.MaxValue;
public int MaxSpanEventCount { get; set; } = int.MaxValue;
}
+
+// Don't set values after validating/parsing options.
+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.TrimEntries);
+ }
+
+ 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.TrimEntries);
+ }
+
+ errorMessages = messages;
+
+ return messages is null;
+ }
+}
diff --git a/src/Aspire.Dashboard/Configuration/ValidateDashboardOptions.cs b/src/Aspire.Dashboard/Configuration/ValidateDashboardOptions.cs
index 961d8395d8..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))
diff --git a/src/Aspire.Dashboard/DashboardWebApplication.cs b/src/Aspire.Dashboard/DashboardWebApplication.cs
index 64c5975ec9..a6e37ca358 100644
--- a/src/Aspire.Dashboard/DashboardWebApplication.cs
+++ b/src/Aspire.Dashboard/DashboardWebApplication.cs
@@ -275,6 +275,10 @@ await Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.Si
});
#endif
}
+ else if (dashboardOptions.Frontend.AuthMode == FrontendAuthMode.OpenIdConnect)
+ {
+ _app.MapPost("/authentication/logout", () => TypedResults.SignOut(authenticationSchemes: [CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme]));
+ }
}
private ILogger GetLogger()
diff --git a/src/Aspire.Dashboard/Extensions/ClaimsIdentityExtensions.cs b/src/Aspire.Dashboard/Extensions/ClaimsIdentityExtensions.cs
new file mode 100644
index 0000000000..be91611c34
--- /dev/null
+++ b/src/Aspire.Dashboard/Extensions/ClaimsIdentityExtensions.cs
@@ -0,0 +1,27 @@
+// 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;
+
+internal 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)
+ {
+ 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..c201c1122e 100644
--- a/src/Aspire.Dashboard/Extensions/StringExtensions.cs
+++ b/src/Aspire.Dashboard/Extensions/StringExtensions.cs
@@ -1,30 +1,13 @@
// 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;
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 +32,30 @@ 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.
+ ///
+ [return: NotNullIfNotNull(nameof(defaultValue))]
+ public static string? GetInitials(this string name, string? defaultValue = default)
+ {
+ var s = name.AsSpan().Trim();
+
+ if (s.Length == 0)
+ {
+ return defaultValue;
+ }
+
+ var lastSpaceIndex = s.LastIndexOf(' ');
+
+ if (lastSpaceIndex == -1)
+ {
+ return s[0].ToString().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])}";
+ }
}
diff --git a/src/Aspire.Dashboard/README.md b/src/Aspire.Dashboard/README.md
index a01d3530e7..0daad4be3c 100644
--- a/src/Aspire.Dashboard/README.md
+++ b/src/Aspire.Dashboard/README.md
@@ -53,10 +53,12 @@ Set `Dashboard:Frontend:AuthMode` to `BrowserToken`. Browser token authenticatio
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: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`.
### OTLP authentication
diff --git a/src/Aspire.Dashboard/Resources/Login.Designer.cs b/src/Aspire.Dashboard/Resources/Login.Designer.cs
index ef69c5eca5..896c171886 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.
///
@@ -87,6 +96,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.
///
diff --git a/src/Aspire.Dashboard/Resources/Login.resx b/src/Aspire.Dashboard/Resources/Login.resx
index b3ab3ddfe2..03ff6345b9 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
@@ -127,6 +131,9 @@
Invalid token. Please try again
+
+ Logged in as:
+
Log in
diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.cs.xlf
index 48505e9076..a8b8db8843 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
+ Placeholder text for the name of the authorized user when no name claim exists
+ Řídicí panel aplikace {0}
@@ -22,6 +27,11 @@
Přihlásit se
+
+
+ Logged in as:
+
+ Další informace
diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.de.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.de.xlf
index a2fb0f1675..2a0754ed93 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
+ Placeholder text for the name of the authorized user when no name claim exists
+ {0}-Dashboard
@@ -22,6 +27,11 @@
Anmelden
+
+
+ Logged in as:
+
+ Weitere Informationen
diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.es.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.es.xlf
index 1c8ac2c31c..a49a64ef85 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
+ Placeholder text for the name of the authorized user when no name claim exists
+ Panel de {0}
@@ -22,6 +27,11 @@
Iniciar sesión
+
+
+ Logged in as:
+
+ Más información
diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.fr.xlf
index fa7c190feb..3440ccabf5 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
+ Placeholder text for the name of the authorized user when no name claim exists
+ Tableau de bord {0}
@@ -22,6 +27,11 @@
Connexion
+
+
+ Logged in as:
+
+ Obtenir plus d’informations
diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.it.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.it.xlf
index 38b2d62c6f..56c31b8216 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
+ Placeholder text for the name of the authorized user when no name claim exists
+ Dashboard {0}
@@ -22,6 +27,11 @@
Accedi
+
+
+ Logged in as:
+
+ Altre informazioni
diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.ja.xlf
index 98c7c5f4dd..15659d6e8c 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
+ Placeholder text for the name of the authorized user when no name claim exists
+ {0} ダッシュボード
@@ -22,6 +27,11 @@
ログイン
+
+
+ Logged in as:
+
+ 詳細情報
diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.ko.xlf
index 8c6bdf23b4..b1994a4ac8 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
+ Placeholder text for the name of the authorized user when no name claim exists
+ {0} 대시보드
@@ -22,6 +27,11 @@
로그인
+
+
+ Logged in as:
+
+ 자세한 정보
diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.pl.xlf
index 7e485d3cde..71a6431b48 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
+ Placeholder text for the name of the authorized user when no name claim exists
+ Pulpit nawigacyjny aplikacji {0}
@@ -22,6 +27,11 @@
Zaloguj się
+
+
+ Logged in as:
+
+ Więcej informacji
diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.pt-BR.xlf
index 4aeb8fb5b2..2a4308463e 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
+ Placeholder text for the name of the authorized user when no name claim exists
+ Painel {0}
@@ -22,6 +27,11 @@
Fazer logon
+
+
+ Logged in as:
+
+ Mais informações
diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.ru.xlf
index 7c84c623be..44734a2334 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
+ Placeholder text for the name of the authorized user when no name claim exists
+ Панель мониторинга {0}
@@ -22,6 +27,11 @@
Войти
+
+
+ Logged in as:
+
+ Дополнительные сведения
diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.tr.xlf
index e1f27ba255..41e4430227 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
+ Placeholder text for the name of the authorized user when no name claim exists
+ {0} panosu
@@ -22,6 +27,11 @@
Oturum aç
+
+
+ Logged in as:
+
+ Daha fazla bilgi
diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hans.xlf
index c338ad9491..9a53624404 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
+ Placeholder text for the name of the authorized user when no name claim exists
+ {0} 仪表板
@@ -22,6 +27,11 @@
登录
+
+
+ Logged in as:
+
+ 详细信息
diff --git a/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/Login.zh-Hant.xlf
index 3cb68b48d9..46842b52c4 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
+ Placeholder text for the name of the authorized user when no name claim exists
+ {0} 儀表板
@@ -22,6 +27,11 @@
登入
+
+
+ Logged in as:
+
+ 詳細資訊
diff --git a/tests/Aspire.Dashboard.Tests/StringExtensionsTests.cs b/tests/Aspire.Dashboard.Tests/StringExtensionsTests.cs
index 8e35757b5e..0ed63b3614 100644
--- a/tests/Aspire.Dashboard.Tests/StringExtensionsTests.cs
+++ b/tests/Aspire.Dashboard.Tests/StringExtensionsTests.cs
@@ -9,21 +9,29 @@ 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(" ", "DefaultValue", "DefaultValue")]
+ [InlineData("\t", "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);
}