From d510d16da590c529d92a5d9a2d3cc0125b6c9e84 Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Thu, 6 Feb 2025 15:13:04 -0500 Subject: [PATCH 01/10] Add display name and priority to Urls --- .../Model/ResourceEndpointHelpers.cs | 14 ++++++----- .../Model/ResourceViewModel.cs | 6 ++++- .../ResourceService/Partials.cs | 2 +- .../CustomResourceSnapshot.cs | 4 +++- .../ApplicationModel/EndpointAnnotation.cs | 16 ++++++++++++- .../ApplicationModel/EndpointReference.cs | 10 ++++++++ .../Dashboard/proto/resource_service.proto | 4 ++++ .../Dcp/ResourceSnapshotBuilder.cs | 6 ++--- .../ResourceBuilderExtensions.cs | 24 ++++++++++++------- 9 files changed, 65 insertions(+), 21 deletions(-) diff --git a/src/Aspire.Dashboard/Model/ResourceEndpointHelpers.cs b/src/Aspire.Dashboard/Model/ResourceEndpointHelpers.cs index e90fc788c5..0e0705c575 100644 --- a/src/Aspire.Dashboard/Model/ResourceEndpointHelpers.cs +++ b/src/Aspire.Dashboard/Model/ResourceEndpointHelpers.cs @@ -19,13 +19,13 @@ public static List GetEndpoints(ResourceViewModel resource, b { if ((includeInternalUrls && url.IsInternal) || !url.IsInternal) { - endpoints.Add(new DisplayedEndpoint + endpoints.Add(new DisplayedEndpoint(displayName: url.DisplayName, originalString: url.Url.OriginalString) { Name = url.Name, - Text = url.Url.OriginalString, Address = url.Url.Host, Port = url.Url.Port, - Url = url.Url.Scheme is "http" or "https" ? url.Url.OriginalString : null + Url = url.Url.Scheme is "http" or "https" ? url.Url.OriginalString : null, + Priority = url.Priority }); } } @@ -36,7 +36,8 @@ public static List GetEndpoints(ResourceViewModel resource, b // - other urls // - endpoint name var orderedEndpoints = endpoints - .OrderByDescending(e => e.Url?.StartsWith("https") == true) + .OrderByDescending(e => e.Priority ?? 0) + .ThenByDescending(e => e.Url?.StartsWith("https") == true) .ThenByDescending(e => e.Url != null) .ThenBy(e => e.Name, StringComparers.EndpointAnnotationName) .ToList(); @@ -46,13 +47,14 @@ public static List GetEndpoints(ResourceViewModel resource, b } [DebuggerDisplay("Name = {Name}, Text = {Text}, Address = {Address}:{Port}, Url = {Url}")] -public sealed class DisplayedEndpoint : IPropertyGridItem +public sealed class DisplayedEndpoint(string? displayName, string originalString) : IPropertyGridItem { public required string Name { get; set; } - public required string Text { get; set; } + public string Text { get; } = displayName ?? originalString; public string? Address { get; set; } public int? Port { get; set; } public string? Url { get; set; } + public int? Priority { get; set; } /// /// Don't display a plain string value here. The URL will be displayed as a hyperlink diff --git a/src/Aspire.Dashboard/Model/ResourceViewModel.cs b/src/Aspire.Dashboard/Model/ResourceViewModel.cs index 0ce9ddf136..8dcc50cc46 100644 --- a/src/Aspire.Dashboard/Model/ResourceViewModel.cs +++ b/src/Aspire.Dashboard/Model/ResourceViewModel.cs @@ -324,8 +324,10 @@ public sealed class UrlViewModel public string Name { get; } public Uri Url { get; } public bool IsInternal { get; } + public string? DisplayName { get; } + public int? Priority { get; } - public UrlViewModel(string name, Uri url, bool isInternal) + public UrlViewModel(string name, Uri url, bool isInternal, string? displayName = null, int? priority = null) { ArgumentException.ThrowIfNullOrWhiteSpace(name); ArgumentNullException.ThrowIfNull(url); @@ -333,6 +335,8 @@ public UrlViewModel(string name, Uri url, bool isInternal) Name = name; Url = url; IsInternal = isInternal; + DisplayName = displayName; + Priority = priority; } } diff --git a/src/Aspire.Dashboard/ResourceService/Partials.cs b/src/Aspire.Dashboard/ResourceService/Partials.cs index bf8593cfa0..fe04ed66a3 100644 --- a/src/Aspire.Dashboard/ResourceService/Partials.cs +++ b/src/Aspire.Dashboard/ResourceService/Partials.cs @@ -93,7 +93,7 @@ ImmutableArray GetUrls() return (from u in Urls let parsedUri = Uri.TryCreate(u.FullUrl, UriKind.Absolute, out var uri) ? uri : null where parsedUri != null - select new UrlViewModel(u.Name, parsedUri, u.IsInternal)) + select new UrlViewModel(u.Name, parsedUri, u.IsInternal, u.HasDisplayName ? u.DisplayName : null, u.HasPriority ? u.Priority : null)) .ToImmutableArray(); } diff --git a/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs b/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs index 6fe33caaa6..07ed7c60da 100644 --- a/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs +++ b/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs @@ -177,8 +177,10 @@ public sealed record EnvironmentVariableSnapshot(string Name, string? Value, boo /// Name of the url. /// The full uri. /// Determines if this url is internal. +/// The display name of the url. +/// The order of the url in UI. [DebuggerDisplay("{Url}", Name = "{Name}")] -public sealed record UrlSnapshot(string Name, string Url, bool IsInternal); +public sealed record UrlSnapshot(string Name, string Url, bool IsInternal, string? DisplayName = null, int? Priority = null); /// /// A snapshot of a volume, mounted to a container. diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs index 2879fde892..a91878a584 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs @@ -31,7 +31,9 @@ public sealed class EndpointAnnotation : IResourceAnnotation /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. /// Indicates that this endpoint should be exposed externally at publish time. /// Specifies if the endpoint will be proxied by DCP. Defaults to true. - public EndpointAnnotation(ProtocolType protocol, string? uriScheme = null, string? transport = null, [EndpointName] string? name = null, int? port = null, int? targetPort = null, bool? isExternal = null, bool isProxied = true) + /// Display name of the endpoint, to be displayed in the Aspire Dashboard. + /// Integer to control visual ordering of endpoints in the Aspire Dashboard. + public EndpointAnnotation(ProtocolType protocol, string? uriScheme = null, string? transport = null, [EndpointName] string? name = null, int? port = null, int? targetPort = null, bool? isExternal = null, bool isProxied = true, string? displayName = null, int? priority = null) { // If the URI scheme is null, we'll adopt either udp:// or tcp:// based on the // protocol. If the name is null, we'll use the URI scheme as the default. This @@ -51,6 +53,8 @@ public EndpointAnnotation(ProtocolType protocol, string? uriScheme = null, strin _targetPort = targetPort; IsExternal = isExternal ?? false; IsProxied = isProxied; + DisplayName = displayName; + Priority = priority; } /// @@ -131,6 +135,16 @@ public string Transport /// Defaults to true. public bool IsProxied { get; set; } = true; + /// + /// Display name of the endpoint, to be displayed in the Aspire Dashboard. + /// + public string? DisplayName { get; } + + /// + /// Integer to control visual ordering of endpoints in the Aspire Dashboard. + /// + public int? Priority { get; } + /// /// Gets or sets a value indicating whether the endpoint is from a launch profile. /// diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs index a0de3a8eb8..1235ac20ed 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs @@ -101,6 +101,16 @@ public EndpointReferenceExpression Property(EndpointProperty property) /// public string Url => AllocatedEndpoint.UriString; + /// + /// Gets the display name for this endpoint. + /// + public string? DisplayName => EndpointAnnotation.DisplayName; + + /// + /// Gets the visual priority for this endpoint. + /// + public int? Priority => EndpointAnnotation.Priority; + internal AllocatedEndpoint AllocatedEndpoint => GetAllocatedEndpoint() ?? throw new InvalidOperationException($"The endpoint `{EndpointName}` is not allocated for the resource `{Resource.Name}`."); diff --git a/src/Aspire.Hosting/Dashboard/proto/resource_service.proto b/src/Aspire.Hosting/Dashboard/proto/resource_service.proto index 2010afe742..0e978dbedf 100644 --- a/src/Aspire.Hosting/Dashboard/proto/resource_service.proto +++ b/src/Aspire.Hosting/Dashboard/proto/resource_service.proto @@ -131,6 +131,10 @@ message Url { // Determines if this url shows up in the details view only by default. // If true, the url will not be shown in the list of urls in the top level resources view. bool is_internal = 3; + // The priority of the url. Lower values are displayed first in the UI. The absence of a value is treated as lowest priority. + optional int32 priority = 4; + // The display name of the url, to appear in the UI. + optional string display_name = 5; } // Data about a volume mounted to a container. diff --git a/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs b/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs index adfa0b376d..d0aab6948e 100644 --- a/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs +++ b/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs @@ -200,21 +200,21 @@ string CombineUrls(string url, string launchUrl) { var url = CombineUrls(ep.Url, launchUrl); - urls.Add(new(Name: ep.EndpointName, Url: url, IsInternal: false)); + urls.Add(new(Name: ep.EndpointName, Url: url, IsInternal: false, DisplayName: ep.DisplayName, Priority: ep.Priority)); } } else { if (ep.IsAllocated) { - urls.Add(new(Name: ep.EndpointName, Url: ep.Url, IsInternal: false)); + urls.Add(new(Name: ep.EndpointName, Url: ep.Url, IsInternal: false, DisplayName: ep.DisplayName, Priority: ep.Priority)); } } if (ep.EndpointAnnotation.IsProxied) { var endpointString = $"{ep.Scheme}://{endpoint.Spec.Address}:{endpoint.Spec.Port}"; - urls.Add(new(Name: $"{ep.EndpointName} target port", Url: endpointString, IsInternal: true)); + urls.Add(new(Name: $"{ep.EndpointName} target port", Url: endpointString, IsInternal: true, DisplayName: ep.DisplayName, Priority: ep.Priority)); } } } diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 463518e558..fc95827697 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -533,10 +533,12 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build /// An optional name of the environment variable that will be used to inject the . If the target port is null one will be dynamically generated and assigned to the environment variable. /// Indicates that this endpoint should be exposed externally at publish time. /// Specifies if the endpoint will be proxied by DCP. Defaults to true. + /// An optional display name of the endpoint, to be displayed in the Aspire Dashboard. + /// An optional integer to control visual ordering of endpoints in the Aspire Dashboard, in descending order. /// The . /// Throws an exception if an endpoint with the same name already exists on the specified resource. [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "")] - public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, string? scheme = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true, bool? isExternal = null) where T : IResourceWithEndpoints + public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, string? scheme = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true, bool? isExternal = null, string? displayName = null, int? priority = null) where T : IResourceWithEndpoints { ArgumentNullException.ThrowIfNull(builder); @@ -547,7 +549,9 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build port: port, targetPort: targetPort, isExternal: isExternal, - isProxied: isProxied); + isProxied: isProxied, + displayName: displayName, + priority: priority); if (builder.Resource.Annotations.OfType().Any(sb => string.Equals(sb.Name, annotation.Name, StringComparisons.EndpointAnnotationName))) { @@ -581,13 +585,15 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build /// An optional name of the endpoint. Defaults to "http" if not specified. /// An optional name of the environment variable to inject. /// Specifies if the endpoint will be proxied by DCP. Defaults to true. + /// An optional display name of the endpoint, to be displayed in the Aspire Dashboard. + /// An optional integer to control visual ordering of endpoints in the Aspire Dashboard, in descending order. /// The . /// Throws an exception if an endpoint with the same name already exists on the specified resource. - public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true) where T : IResourceWithEndpoints + public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true, string? displayName = null, int? priority = null) where T : IResourceWithEndpoints { ArgumentNullException.ThrowIfNull(builder); - return builder.WithEndpoint(targetPort: targetPort, port: port, scheme: "http", name: name, env: env, isProxied: isProxied); + return builder.WithEndpoint(targetPort: targetPort, port: port, scheme: "http", name: name, env: env, isProxied: isProxied, displayName: displayName, priority: priority); } /// @@ -601,13 +607,15 @@ public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder b /// An optional name of the endpoint. Defaults to "https" if not specified. /// An optional name of the environment variable to inject. /// Specifies if the endpoint will be proxied by DCP. Defaults to true. + /// An optional display name of the endpoint, to be displayed in the Aspire Dashboard. + /// An optional integer to control visual ordering of endpoints in the Aspire Dashboard, in descending order. /// The . /// Throws an exception if an endpoint with the same name already exists on the specified resource. - public static IResourceBuilder WithHttpsEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true) where T : IResourceWithEndpoints + public static IResourceBuilder WithHttpsEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true, string? displayName = null, int? priority = null) where T : IResourceWithEndpoints { ArgumentNullException.ThrowIfNull(builder); - return builder.WithEndpoint(targetPort: targetPort, port: port, scheme: "https", name: name, env: env, isProxied: isProxied); + return builder.WithEndpoint(targetPort: targetPort, port: port, scheme: "https", name: name, env: env, isProxied: isProxied, displayName: displayName, priority: priority); } /// @@ -637,7 +645,7 @@ public static IResourceBuilder WithExternalHttpEndpoints(this IResourceBui } /// - /// Gets an by name from the resource. These endpoints are declared either using or by launch settings (for project resources). + /// Gets an by name from the resource. These endpoints are declared either using or by launch settings (for project resources). /// The can be used to resolve the address of the endpoint in . /// /// The resource type. @@ -1109,7 +1117,7 @@ public static IResourceBuilder WithRelationship( /// /// var builder = DistributedApplication.CreateBuilder(args); /// var backend = builder.AddProject<Projects.Backend>("backend"); - /// + /// /// var frontend = builder.AddProject<Projects.Manager>("frontend") /// .WithParentRelationship(backend.Resource); /// From ed426a4fd6f35348483e0d0b26fe0f6f699aafda Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Thu, 6 Feb 2025 15:30:22 -0500 Subject: [PATCH 02/10] Make sure to plumb new properties through gRPC --- src/Aspire.Hosting/Dashboard/proto/Partials.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Hosting/Dashboard/proto/Partials.cs b/src/Aspire.Hosting/Dashboard/proto/Partials.cs index 6b067c8a41..7e339970d7 100644 --- a/src/Aspire.Hosting/Dashboard/proto/Partials.cs +++ b/src/Aspire.Hosting/Dashboard/proto/Partials.cs @@ -40,9 +40,20 @@ public static Resource FromSnapshot(ResourceSnapshot snapshot) resource.Environment.Add(new EnvironmentVariable { Name = env.Name, Value = env.Value ?? "", IsFromSpec = env.IsFromSpec }); } - foreach (var url in snapshot.Urls) + foreach (var urlSnapshot in snapshot.Urls) { - resource.Urls.Add(new Url { Name = url.Name, FullUrl = url.Url, IsInternal = url.IsInternal }); + var url = new Url { Name = urlSnapshot.Name, FullUrl = urlSnapshot.Url, IsInternal = urlSnapshot.IsInternal }; + if (urlSnapshot.DisplayName is not null) + { + url.DisplayName = urlSnapshot.DisplayName; + } + + if (urlSnapshot.Priority is not null) + { + url.Priority = urlSnapshot.Priority.Value; + } + + resource.Urls.Add(url); } foreach (var relationship in snapshot.Relationships) From d5a5876593bf1838ce59b6c7f32b4442eefee9d4 Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Thu, 6 Feb 2025 16:04:02 -0500 Subject: [PATCH 03/10] Add third display name column for endpoints when there is a display name --- .../Components/Controls/ResourceDetails.razor | 52 ++++++++++++++++--- .../Model/ResourceEndpointHelpers.cs | 8 +-- .../Resources/ControlsStrings.Designer.cs | 9 ++++ .../Resources/ControlsStrings.resx | 5 +- .../Resources/xlf/ControlsStrings.cs.xlf | 5 ++ .../Resources/xlf/ControlsStrings.de.xlf | 5 ++ .../Resources/xlf/ControlsStrings.es.xlf | 5 ++ .../Resources/xlf/ControlsStrings.fr.xlf | 5 ++ .../Resources/xlf/ControlsStrings.it.xlf | 5 ++ .../Resources/xlf/ControlsStrings.ja.xlf | 5 ++ .../Resources/xlf/ControlsStrings.ko.xlf | 5 ++ .../Resources/xlf/ControlsStrings.pl.xlf | 5 ++ .../Resources/xlf/ControlsStrings.pt-BR.xlf | 5 ++ .../Resources/xlf/ControlsStrings.ru.xlf | 5 ++ .../Resources/xlf/ControlsStrings.tr.xlf | 5 ++ .../Resources/xlf/ControlsStrings.zh-Hans.xlf | 5 ++ .../Resources/xlf/ControlsStrings.zh-Hant.xlf | 5 ++ 17 files changed, 127 insertions(+), 12 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor b/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor index 57e4401fc7..3426b06d0a 100644 --- a/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor +++ b/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor @@ -63,14 +63,50 @@ @FilteredEndpoints.Count() - + + @{ + var hasAnyEndpointDisplayNames = FilteredEndpoints.Any(e => e.DisplayName != null); + } + + + + + + @if (hasAnyEndpointDisplayNames) + { + + @if (context.DisplayName is not null) + { + + } + + } + + + + @if (Resource.IsContainer()) { diff --git a/src/Aspire.Dashboard/Model/ResourceEndpointHelpers.cs b/src/Aspire.Dashboard/Model/ResourceEndpointHelpers.cs index 0e0705c575..b03408473f 100644 --- a/src/Aspire.Dashboard/Model/ResourceEndpointHelpers.cs +++ b/src/Aspire.Dashboard/Model/ResourceEndpointHelpers.cs @@ -19,7 +19,7 @@ public static List GetEndpoints(ResourceViewModel resource, b { if ((includeInternalUrls && url.IsInternal) || !url.IsInternal) { - endpoints.Add(new DisplayedEndpoint(displayName: url.DisplayName, originalString: url.Url.OriginalString) + endpoints.Add(new DisplayedEndpoint(displayName: url.DisplayName, originalUrlString: url.Url.OriginalString) { Name = url.Name, Address = url.Url.Host, @@ -47,14 +47,16 @@ public static List GetEndpoints(ResourceViewModel resource, b } [DebuggerDisplay("Name = {Name}, Text = {Text}, Address = {Address}:{Port}, Url = {Url}")] -public sealed class DisplayedEndpoint(string? displayName, string originalString) : IPropertyGridItem +public sealed class DisplayedEndpoint(string? displayName, string originalUrlString) : IPropertyGridItem { public required string Name { get; set; } - public string Text { get; } = displayName ?? originalString; + public string Text { get; } = displayName ?? originalUrlString; public string? Address { get; set; } public int? Port { get; set; } public string? Url { get; set; } public int? Priority { get; set; } + public string? DisplayName => displayName; + public string OriginalUrlString { get; set; } = originalUrlString; /// /// Don't display a plain string value here. The URL will be displayed as a hyperlink diff --git a/src/Aspire.Dashboard/Resources/ControlsStrings.Designer.cs b/src/Aspire.Dashboard/Resources/ControlsStrings.Designer.cs index e6641c6439..b750af9943 100644 --- a/src/Aspire.Dashboard/Resources/ControlsStrings.Designer.cs +++ b/src/Aspire.Dashboard/Resources/ControlsStrings.Designer.cs @@ -230,6 +230,15 @@ public static string DetailsColumnHeader { } } + /// + /// Looks up a localized string similar to Display name. + /// + public static string DisplayNameColumnHeader { + get { + return ResourceManager.GetString("DisplayNameColumnHeader", resourceCulture); + } + } + /// /// Looks up a localized string similar to Duration. /// diff --git a/src/Aspire.Dashboard/Resources/ControlsStrings.resx b/src/Aspire.Dashboard/Resources/ControlsStrings.resx index 2ab34386f7..01daba50e2 100644 --- a/src/Aspire.Dashboard/Resources/ControlsStrings.resx +++ b/src/Aspire.Dashboard/Resources/ControlsStrings.resx @@ -181,6 +181,9 @@ Name + + Display name + Value @@ -437,4 +440,4 @@ Remove for resource - \ No newline at end of file + diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.cs.xlf index d374ef04f8..b55a571e42 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.cs.xlf @@ -97,6 +97,11 @@ Podrobnosti + + Display name + Display name + + Duration Doba trvání diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.de.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.de.xlf index de11b75f4a..980a7e19e7 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.de.xlf @@ -97,6 +97,11 @@ Details + + Display name + Display name + + Duration Dauer diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.es.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.es.xlf index 361186ba3a..1085d9bc4a 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.es.xlf @@ -97,6 +97,11 @@ Detalles + + Display name + Display name + + Duration Duración diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.fr.xlf index 744fa0c273..001ecc49ce 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.fr.xlf @@ -97,6 +97,11 @@ Détails + + Display name + Display name + + Duration Durée diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.it.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.it.xlf index e25d40bbed..169575e40b 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.it.xlf @@ -97,6 +97,11 @@ Dettagli + + Display name + Display name + + Duration Durata diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ja.xlf index e9d0b0df37..973e42f377 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ja.xlf @@ -97,6 +97,11 @@ 詳細 + + Display name + Display name + + Duration 継続時間 diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ko.xlf index 3f666fd51d..d78fd0c66a 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ko.xlf @@ -97,6 +97,11 @@ 세부 정보 + + Display name + Display name + + Duration 기간 diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pl.xlf index 9555a49b59..15f54e96ec 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pl.xlf @@ -97,6 +97,11 @@ Szczegóły + + Display name + Display name + + Duration Czas trwania diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pt-BR.xlf index c54dd80b96..46ad7c85a9 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.pt-BR.xlf @@ -97,6 +97,11 @@ Detalhes + + Display name + Display name + + Duration Duração diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ru.xlf index 5eacb42082..3a6e51d312 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.ru.xlf @@ -97,6 +97,11 @@ Сведения + + Display name + Display name + + Duration Длительность diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.tr.xlf index 9d24d82c4d..db9d812b7d 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.tr.xlf @@ -97,6 +97,11 @@ Ayrıntılar + + Display name + Display name + + Duration Süre diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hans.xlf index 837147645e..e28aea1e0f 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hans.xlf @@ -97,6 +97,11 @@ 详细信息 + + Display name + Display name + + Duration 持续时间 diff --git a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hant.xlf index 127860d847..a90f548fcf 100644 --- a/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/ControlsStrings.zh-Hant.xlf @@ -97,6 +97,11 @@ 詳細資料 + + Display name + Display name + + Duration 持續時間 From 8cc202c4a285de4095f3fa081f171e40d2a36a96 Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Thu, 6 Feb 2025 16:05:19 -0500 Subject: [PATCH 04/10] add Data Explorer display name to cosmos db, make properties settable --- src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs | 3 ++- src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs index 1763c88f93..72f71c53fa 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs @@ -288,10 +288,11 @@ public static IResourceBuilder WithDataExplorer(t endpoint.UriScheme = "http"; endpoint.TargetPort = 1234; endpoint.Port = port; + endpoint.DisplayName = "Data Explorer"; }); } - /// + /// /// Configures the resource to use access key authentication with Azure Cosmos DB. /// /// The Azure Cosmos DB resource builder. diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs index a91878a584..1eebbdd8b9 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs @@ -138,12 +138,12 @@ public string Transport /// /// Display name of the endpoint, to be displayed in the Aspire Dashboard. /// - public string? DisplayName { get; } + public string? DisplayName { get; set; } /// /// Integer to control visual ordering of endpoints in the Aspire Dashboard. /// - public int? Priority { get; } + public int? Priority { get; set; } /// /// Gets or sets a value indicating whether the endpoint is from a launch profile. From cf8f35881a35e847ac5f68b20426c464532ea4c6 Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Thu, 6 Feb 2025 16:10:31 -0500 Subject: [PATCH 05/10] clean up DisplayedEndpoint type --- .../Model/ResourceEndpointHelpers.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Aspire.Dashboard/Model/ResourceEndpointHelpers.cs b/src/Aspire.Dashboard/Model/ResourceEndpointHelpers.cs index b03408473f..773a4dc73c 100644 --- a/src/Aspire.Dashboard/Model/ResourceEndpointHelpers.cs +++ b/src/Aspire.Dashboard/Model/ResourceEndpointHelpers.cs @@ -19,13 +19,15 @@ public static List GetEndpoints(ResourceViewModel resource, b { if ((includeInternalUrls && url.IsInternal) || !url.IsInternal) { - endpoints.Add(new DisplayedEndpoint(displayName: url.DisplayName, originalUrlString: url.Url.OriginalString) + endpoints.Add(new DisplayedEndpoint { Name = url.Name, Address = url.Url.Host, Port = url.Url.Port, Url = url.Url.Scheme is "http" or "https" ? url.Url.OriginalString : null, - Priority = url.Priority + Priority = url.Priority, + OriginalUrlString = url.Url.OriginalString, + Text = url.DisplayName ?? url.Url.OriginalString }); } } @@ -47,16 +49,16 @@ public static List GetEndpoints(ResourceViewModel resource, b } [DebuggerDisplay("Name = {Name}, Text = {Text}, Address = {Address}:{Port}, Url = {Url}")] -public sealed class DisplayedEndpoint(string? displayName, string originalUrlString) : IPropertyGridItem +public sealed class DisplayedEndpoint : IPropertyGridItem { public required string Name { get; set; } - public string Text { get; } = displayName ?? originalUrlString; + public required string Text { get; set; } public string? Address { get; set; } public int? Port { get; set; } public string? Url { get; set; } public int? Priority { get; set; } - public string? DisplayName => displayName; - public string OriginalUrlString { get; set; } = originalUrlString; + public string? DisplayName { get; set; } + public required string OriginalUrlString { get; set; } /// /// Don't display a plain string value here. The URL will be displayed as a hyperlink From 5d46e88920d1588460104289f8b423073312ca31 Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Fri, 7 Feb 2025 08:35:10 -0500 Subject: [PATCH 06/10] Use display properties object instead on EndpointAnnotation --- .../Model/ResourceEndpointHelpers.cs | 11 +++++---- .../Model/ResourceViewModel.cs | 10 ++++---- .../ResourceService/Partials.cs | 2 +- .../AzureCosmosDBExtensions.cs | 3 ++- .../CustomResourceSnapshot.cs | 12 +++++++--- .../ApplicationModel/EndpointAnnotation.cs | 18 +++++--------- .../EndpointDisplayProperties.cs | 21 ++++++++++++++++ .../ApplicationModel/EndpointReference.cs | 9 ++----- .../Dashboard/proto/Partials.cs | 10 ++++---- .../Dashboard/proto/resource_service.proto | 13 ++++++---- .../Dcp/ResourceSnapshotBuilder.cs | 6 ++--- .../ResourceBuilderExtensions.cs | 24 ++++++++----------- 12 files changed, 80 insertions(+), 59 deletions(-) create mode 100644 src/Aspire.Hosting/ApplicationModel/EndpointDisplayProperties.cs diff --git a/src/Aspire.Dashboard/Model/ResourceEndpointHelpers.cs b/src/Aspire.Dashboard/Model/ResourceEndpointHelpers.cs index 773a4dc73c..796ae106ce 100644 --- a/src/Aspire.Dashboard/Model/ResourceEndpointHelpers.cs +++ b/src/Aspire.Dashboard/Model/ResourceEndpointHelpers.cs @@ -25,9 +25,10 @@ public static List GetEndpoints(ResourceViewModel resource, b Address = url.Url.Host, Port = url.Url.Port, Url = url.Url.Scheme is "http" or "https" ? url.Url.OriginalString : null, - Priority = url.Priority, + SortOrder = url.DisplayProperties?.SortOrder, + DisplayName = url.DisplayProperties?.DisplayName, OriginalUrlString = url.Url.OriginalString, - Text = url.DisplayName ?? url.Url.OriginalString + Text = url.DisplayProperties?.DisplayName ?? url.Url.OriginalString }); } } @@ -38,7 +39,7 @@ public static List GetEndpoints(ResourceViewModel resource, b // - other urls // - endpoint name var orderedEndpoints = endpoints - .OrderByDescending(e => e.Priority ?? 0) + .OrderByDescending(e => e.SortOrder ?? 0) .ThenByDescending(e => e.Url?.StartsWith("https") == true) .ThenByDescending(e => e.Url != null) .ThenBy(e => e.Name, StringComparers.EndpointAnnotationName) @@ -48,7 +49,7 @@ public static List GetEndpoints(ResourceViewModel resource, b } } -[DebuggerDisplay("Name = {Name}, Text = {Text}, Address = {Address}:{Port}, Url = {Url}")] +[DebuggerDisplay("Name = {Name}, Text = {Text}, Address = {Address}:{Port}, Url = {Url}, DisplayName = {DisplayName}, OriginalUrlString = {OriginalUrlString}, SortOrder = {SortOrder}")] public sealed class DisplayedEndpoint : IPropertyGridItem { public required string Name { get; set; } @@ -56,7 +57,7 @@ public sealed class DisplayedEndpoint : IPropertyGridItem public string? Address { get; set; } public int? Port { get; set; } public string? Url { get; set; } - public int? Priority { get; set; } + public int? SortOrder { get; set; } public string? DisplayName { get; set; } public required string OriginalUrlString { get; set; } diff --git a/src/Aspire.Dashboard/Model/ResourceViewModel.cs b/src/Aspire.Dashboard/Model/ResourceViewModel.cs index 8dcc50cc46..7408d6a6cb 100644 --- a/src/Aspire.Dashboard/Model/ResourceViewModel.cs +++ b/src/Aspire.Dashboard/Model/ResourceViewModel.cs @@ -324,10 +324,9 @@ public sealed class UrlViewModel public string Name { get; } public Uri Url { get; } public bool IsInternal { get; } - public string? DisplayName { get; } - public int? Priority { get; } + public UrlDisplayPropertiesViewModel? DisplayProperties { get; } - public UrlViewModel(string name, Uri url, bool isInternal, string? displayName = null, int? priority = null) + public UrlViewModel(string name, Uri url, bool isInternal, UrlDisplayPropertiesViewModel? displayProperties = null) { ArgumentException.ThrowIfNullOrWhiteSpace(name); ArgumentNullException.ThrowIfNull(url); @@ -335,11 +334,12 @@ public UrlViewModel(string name, Uri url, bool isInternal, string? displayName = Name = name; Url = url; IsInternal = isInternal; - DisplayName = displayName; - Priority = priority; + DisplayProperties = displayProperties; } } +public record UrlDisplayPropertiesViewModel(string? DisplayName, int? SortOrder); + public sealed record class VolumeViewModel(int index, string Source, string Target, string MountType, bool IsReadOnly) : IPropertyGridItem { string IPropertyGridItem.Name => Source; diff --git a/src/Aspire.Dashboard/ResourceService/Partials.cs b/src/Aspire.Dashboard/ResourceService/Partials.cs index fe04ed66a3..3ff7b8f9f2 100644 --- a/src/Aspire.Dashboard/ResourceService/Partials.cs +++ b/src/Aspire.Dashboard/ResourceService/Partials.cs @@ -93,7 +93,7 @@ ImmutableArray GetUrls() return (from u in Urls let parsedUri = Uri.TryCreate(u.FullUrl, UriKind.Absolute, out var uri) ? uri : null where parsedUri != null - select new UrlViewModel(u.Name, parsedUri, u.IsInternal, u.HasDisplayName ? u.DisplayName : null, u.HasPriority ? u.Priority : null)) + select new UrlViewModel(u.Name, parsedUri, u.IsInternal, u.DisplayProperties is not null ? new UrlDisplayPropertiesViewModel(u.DisplayProperties.HasDisplayName ? u.DisplayProperties.DisplayName : null, u.DisplayProperties.HasSortOrder ? u.DisplayProperties.SortOrder : null) : null)) .ToImmutableArray(); } diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs index 189c8898c0..35b74529dc 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs @@ -14,6 +14,7 @@ using Azure.Provisioning.KeyVault; using Microsoft.Azure.Cosmos; using Microsoft.Extensions.DependencyInjection; +using EndpointDisplayProperties = Aspire.Hosting.ApplicationModel.EndpointDisplayProperties; namespace Aspire.Hosting; @@ -288,7 +289,7 @@ public static IResourceBuilder WithDataExplorer(t endpoint.UriScheme = "http"; endpoint.TargetPort = 1234; endpoint.Port = port; - endpoint.DisplayName = "Data Explorer"; + endpoint.DisplayProperties = new EndpointDisplayProperties { DisplayName = "Data Explorer" }; }); } diff --git a/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs b/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs index 07ed7c60da..a0ee7661d7 100644 --- a/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs +++ b/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs @@ -177,10 +177,16 @@ public sealed record EnvironmentVariableSnapshot(string Name, string? Value, boo /// Name of the url. /// The full uri. /// Determines if this url is internal. -/// The display name of the url. -/// The order of the url in UI. +/// The UI display properties for the url. [DebuggerDisplay("{Url}", Name = "{Name}")] -public sealed record UrlSnapshot(string Name, string Url, bool IsInternal, string? DisplayName = null, int? Priority = null); +public sealed record UrlSnapshot(string Name, string Url, bool IsInternal, UrlDisplayPropertiesSnapshot? DisplayProperties = null); + +/// +/// A snapshot of the display properties for a url. +/// +/// The display name of the url. +/// The order of the url in UI. Higher numbers are displayed first in the UI. +public sealed record UrlDisplayPropertiesSnapshot(string? DisplayName = null, int? SortOrder = null); /// /// A snapshot of a volume, mounted to a container. diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs index 1eebbdd8b9..d07a3680ef 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs @@ -31,9 +31,8 @@ public sealed class EndpointAnnotation : IResourceAnnotation /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. /// Indicates that this endpoint should be exposed externally at publish time. /// Specifies if the endpoint will be proxied by DCP. Defaults to true. - /// Display name of the endpoint, to be displayed in the Aspire Dashboard. - /// Integer to control visual ordering of endpoints in the Aspire Dashboard. - public EndpointAnnotation(ProtocolType protocol, string? uriScheme = null, string? transport = null, [EndpointName] string? name = null, int? port = null, int? targetPort = null, bool? isExternal = null, bool isProxied = true, string? displayName = null, int? priority = null) + /// Function to configure Aspire Dashboard display properties for this endpoint. + public EndpointAnnotation(ProtocolType protocol, string? uriScheme = null, string? transport = null, [EndpointName] string? name = null, int? port = null, int? targetPort = null, bool? isExternal = null, bool isProxied = true, Action? displayProperties = null) { // If the URI scheme is null, we'll adopt either udp:// or tcp:// based on the // protocol. If the name is null, we'll use the URI scheme as the default. This @@ -53,8 +52,8 @@ public EndpointAnnotation(ProtocolType protocol, string? uriScheme = null, strin _targetPort = targetPort; IsExternal = isExternal ?? false; IsProxied = isProxied; - DisplayName = displayName; - Priority = priority; + DisplayProperties = new EndpointDisplayProperties(); + displayProperties?.Invoke(DisplayProperties); } /// @@ -136,14 +135,9 @@ public string Transport public bool IsProxied { get; set; } = true; /// - /// Display name of the endpoint, to be displayed in the Aspire Dashboard. + /// Display properties of the endpoint to be displayed in UI. /// - public string? DisplayName { get; set; } - - /// - /// Integer to control visual ordering of endpoints in the Aspire Dashboard. - /// - public int? Priority { get; set; } + public EndpointDisplayProperties DisplayProperties { get; set; } /// /// Gets or sets a value indicating whether the endpoint is from a launch profile. diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointDisplayProperties.cs b/src/Aspire.Hosting/ApplicationModel/EndpointDisplayProperties.cs new file mode 100644 index 0000000000..2f01f19a94 --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/EndpointDisplayProperties.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Display properties of the endpoint to be displayed in UI. +/// +public sealed class EndpointDisplayProperties +{ + /// + /// Display name of the endpoint, to be displayed in the Aspire Dashboard. + /// + public string? DisplayName { get; set; } + + /// + /// Integer to control visual ordering of endpoints in the Aspire Dashboard. Higher values are displayed first. + /// Ties are broken by protocol type first (https before others), then by endpoint name. + /// + public int? SortOrder { get; set; } +} diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs index 1235ac20ed..c6d99ee181 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs @@ -102,14 +102,9 @@ public EndpointReferenceExpression Property(EndpointProperty property) public string Url => AllocatedEndpoint.UriString; /// - /// Gets the display name for this endpoint. + /// Gets the display properties for this endpoint. /// - public string? DisplayName => EndpointAnnotation.DisplayName; - - /// - /// Gets the visual priority for this endpoint. - /// - public int? Priority => EndpointAnnotation.Priority; + public EndpointDisplayProperties DisplayProperties => EndpointAnnotation.DisplayProperties; internal AllocatedEndpoint AllocatedEndpoint => GetAllocatedEndpoint() diff --git a/src/Aspire.Hosting/Dashboard/proto/Partials.cs b/src/Aspire.Hosting/Dashboard/proto/Partials.cs index 7e339970d7..a697c34529 100644 --- a/src/Aspire.Hosting/Dashboard/proto/Partials.cs +++ b/src/Aspire.Hosting/Dashboard/proto/Partials.cs @@ -43,16 +43,18 @@ public static Resource FromSnapshot(ResourceSnapshot snapshot) foreach (var urlSnapshot in snapshot.Urls) { var url = new Url { Name = urlSnapshot.Name, FullUrl = urlSnapshot.Url, IsInternal = urlSnapshot.IsInternal }; - if (urlSnapshot.DisplayName is not null) + var displayProperties = new EndpointDisplayProperties(); + if (urlSnapshot.DisplayProperties?.DisplayName is not null) { - url.DisplayName = urlSnapshot.DisplayName; + displayProperties.DisplayName = urlSnapshot.DisplayProperties.DisplayName; } - if (urlSnapshot.Priority is not null) + if (urlSnapshot.DisplayProperties?.SortOrder is not null) { - url.Priority = urlSnapshot.Priority.Value; + displayProperties.SortOrder = urlSnapshot.DisplayProperties.SortOrder.Value; } + url.DisplayProperties = displayProperties; resource.Urls.Add(url); } diff --git a/src/Aspire.Hosting/Dashboard/proto/resource_service.proto b/src/Aspire.Hosting/Dashboard/proto/resource_service.proto index 0e978dbedf..243e72e5db 100644 --- a/src/Aspire.Hosting/Dashboard/proto/resource_service.proto +++ b/src/Aspire.Hosting/Dashboard/proto/resource_service.proto @@ -131,10 +131,15 @@ message Url { // Determines if this url shows up in the details view only by default. // If true, the url will not be shown in the list of urls in the top level resources view. bool is_internal = 3; - // The priority of the url. Lower values are displayed first in the UI. The absence of a value is treated as lowest priority. - optional int32 priority = 4; - // The display name of the url, to appear in the UI. - optional string display_name = 5; + // Display properties of the Url + optional EndpointDisplayProperties display_properties = 4; +} + +message EndpointDisplayProperties { + // The sort order of the url. Lower values are displayed first in the UI. The absence of a value is treated as lowest order. + optional int32 sort_order = 1; + // The display name of the url, to appear in the UI. + optional string display_name = 2; } // Data about a volume mounted to a container. diff --git a/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs b/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs index 6062b1ad92..ea79286e4d 100644 --- a/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs +++ b/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs @@ -236,21 +236,21 @@ string CombineUrls(string url, string launchUrl) { var url = CombineUrls(ep.Url, launchUrl); - urls.Add(new(Name: ep.EndpointName, Url: url, IsInternal: false, DisplayName: ep.DisplayName, Priority: ep.Priority)); + urls.Add(new(Name: ep.EndpointName, Url: url, IsInternal: false, DisplayProperties: new(ep.DisplayProperties.DisplayName, ep.DisplayProperties.SortOrder))); } } else { if (ep.IsAllocated) { - urls.Add(new(Name: ep.EndpointName, Url: ep.Url, IsInternal: false, DisplayName: ep.DisplayName, Priority: ep.Priority)); + urls.Add(new(Name: ep.EndpointName, Url: ep.Url, IsInternal: false, DisplayProperties: new(ep.DisplayProperties.DisplayName, ep.DisplayProperties.SortOrder))); } } if (ep.EndpointAnnotation.IsProxied) { var endpointString = $"{ep.Scheme}://{endpoint.Spec.Address}:{endpoint.Spec.Port}"; - urls.Add(new(Name: $"{ep.EndpointName} target port", Url: endpointString, IsInternal: true, DisplayName: ep.DisplayName, Priority: ep.Priority)); + urls.Add(new(Name: $"{ep.EndpointName} target port", Url: endpointString, IsInternal: true, DisplayProperties: new(ep.DisplayProperties.DisplayName, ep.DisplayProperties.SortOrder))); } } } diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index e886a0fffd..64f72ddf9b 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -533,12 +533,11 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build /// An optional name of the environment variable that will be used to inject the . If the target port is null one will be dynamically generated and assigned to the environment variable. /// Indicates that this endpoint should be exposed externally at publish time. /// Specifies if the endpoint will be proxied by DCP. Defaults to true. - /// An optional display name of the endpoint, to be displayed in the Aspire Dashboard. - /// An optional integer to control visual ordering of endpoints in the Aspire Dashboard, in descending order. + /// Function to configure Aspire Dashboard display properties for this endpoint. /// The . /// Throws an exception if an endpoint with the same name already exists on the specified resource. [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "")] - public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, string? scheme = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true, bool? isExternal = null, string? displayName = null, int? priority = null) where T : IResourceWithEndpoints + public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, string? scheme = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true, bool? isExternal = null, Action? displayProperties = null) where T : IResourceWithEndpoints { ArgumentNullException.ThrowIfNull(builder); @@ -550,8 +549,7 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build targetPort: targetPort, isExternal: isExternal, isProxied: isProxied, - displayName: displayName, - priority: priority); + displayProperties: displayProperties); if (builder.Resource.Annotations.OfType().Any(sb => string.Equals(sb.Name, annotation.Name, StringComparisons.EndpointAnnotationName))) { @@ -585,15 +583,14 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build /// An optional name of the endpoint. Defaults to "http" if not specified. /// An optional name of the environment variable to inject. /// Specifies if the endpoint will be proxied by DCP. Defaults to true. - /// An optional display name of the endpoint, to be displayed in the Aspire Dashboard. - /// An optional integer to control visual ordering of endpoints in the Aspire Dashboard, in descending order. + /// Function to configure Aspire Dashboard display properties for this endpoint. /// The . /// Throws an exception if an endpoint with the same name already exists on the specified resource. - public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true, string? displayName = null, int? priority = null) where T : IResourceWithEndpoints + public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true, Action? displayProperties = null) where T : IResourceWithEndpoints { ArgumentNullException.ThrowIfNull(builder); - return builder.WithEndpoint(targetPort: targetPort, port: port, scheme: "http", name: name, env: env, isProxied: isProxied, displayName: displayName, priority: priority); + return builder.WithEndpoint(targetPort: targetPort, port: port, scheme: "http", name: name, env: env, isProxied: isProxied, displayProperties: displayProperties); } /// @@ -607,15 +604,14 @@ public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder b /// An optional name of the endpoint. Defaults to "https" if not specified. /// An optional name of the environment variable to inject. /// Specifies if the endpoint will be proxied by DCP. Defaults to true. - /// An optional display name of the endpoint, to be displayed in the Aspire Dashboard. - /// An optional integer to control visual ordering of endpoints in the Aspire Dashboard, in descending order. + /// Function to configure Aspire Dashboard display properties for this endpoint. /// The . /// Throws an exception if an endpoint with the same name already exists on the specified resource. - public static IResourceBuilder WithHttpsEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true, string? displayName = null, int? priority = null) where T : IResourceWithEndpoints + public static IResourceBuilder WithHttpsEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true, Action? displayProperties = null) where T : IResourceWithEndpoints { ArgumentNullException.ThrowIfNull(builder); - return builder.WithEndpoint(targetPort: targetPort, port: port, scheme: "https", name: name, env: env, isProxied: isProxied, displayName: displayName, priority: priority); + return builder.WithEndpoint(targetPort: targetPort, port: port, scheme: "https", name: name, env: env, isProxied: isProxied, displayProperties: displayProperties); } /// @@ -645,7 +641,7 @@ public static IResourceBuilder WithExternalHttpEndpoints(this IResourceBui } /// - /// Gets an by name from the resource. These endpoints are declared either using or by launch settings (for project resources). + /// Gets an by name from the resource. These endpoints are declared either using or by launch settings (for project resources). /// The can be used to resolve the address of the endpoint in . /// /// The resource type. From a9ac084a5f9b0306df408b7c11a4b265ef9a20e1 Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Fri, 7 Feb 2025 16:37:47 -0500 Subject: [PATCH 07/10] Add unit test, remove breaking api changes --- .../AzureCosmosDBExtensions.cs | 1 - .../ApplicationModel/EndpointAnnotation.cs | 4 +- .../Dashboard/proto/Partials.cs | 2 +- .../Dashboard/proto/resource_service.proto | 4 +- .../ResourceBuilderExtensions.cs | 18 ++++----- .../Model/ResourceEndpointHelpersTests.cs | 39 +++++++++++++++++++ 6 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs index 35b74529dc..9725d5975c 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs @@ -14,7 +14,6 @@ using Azure.Provisioning.KeyVault; using Microsoft.Azure.Cosmos; using Microsoft.Extensions.DependencyInjection; -using EndpointDisplayProperties = Aspire.Hosting.ApplicationModel.EndpointDisplayProperties; namespace Aspire.Hosting; diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs index d07a3680ef..e1ded6fd00 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs @@ -31,8 +31,7 @@ public sealed class EndpointAnnotation : IResourceAnnotation /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. /// Indicates that this endpoint should be exposed externally at publish time. /// Specifies if the endpoint will be proxied by DCP. Defaults to true. - /// Function to configure Aspire Dashboard display properties for this endpoint. - public EndpointAnnotation(ProtocolType protocol, string? uriScheme = null, string? transport = null, [EndpointName] string? name = null, int? port = null, int? targetPort = null, bool? isExternal = null, bool isProxied = true, Action? displayProperties = null) + public EndpointAnnotation(ProtocolType protocol, string? uriScheme = null, string? transport = null, [EndpointName] string? name = null, int? port = null, int? targetPort = null, bool? isExternal = null, bool isProxied = true) { // If the URI scheme is null, we'll adopt either udp:// or tcp:// based on the // protocol. If the name is null, we'll use the URI scheme as the default. This @@ -53,7 +52,6 @@ public EndpointAnnotation(ProtocolType protocol, string? uriScheme = null, strin IsExternal = isExternal ?? false; IsProxied = isProxied; DisplayProperties = new EndpointDisplayProperties(); - displayProperties?.Invoke(DisplayProperties); } /// diff --git a/src/Aspire.Hosting/Dashboard/proto/Partials.cs b/src/Aspire.Hosting/Dashboard/proto/Partials.cs index a697c34529..c98547657b 100644 --- a/src/Aspire.Hosting/Dashboard/proto/Partials.cs +++ b/src/Aspire.Hosting/Dashboard/proto/Partials.cs @@ -43,7 +43,7 @@ public static Resource FromSnapshot(ResourceSnapshot snapshot) foreach (var urlSnapshot in snapshot.Urls) { var url = new Url { Name = urlSnapshot.Name, FullUrl = urlSnapshot.Url, IsInternal = urlSnapshot.IsInternal }; - var displayProperties = new EndpointDisplayProperties(); + var displayProperties = new UrlDisplayProperties(); if (urlSnapshot.DisplayProperties?.DisplayName is not null) { displayProperties.DisplayName = urlSnapshot.DisplayProperties.DisplayName; diff --git a/src/Aspire.Hosting/Dashboard/proto/resource_service.proto b/src/Aspire.Hosting/Dashboard/proto/resource_service.proto index 243e72e5db..e01d734fe2 100644 --- a/src/Aspire.Hosting/Dashboard/proto/resource_service.proto +++ b/src/Aspire.Hosting/Dashboard/proto/resource_service.proto @@ -132,10 +132,10 @@ message Url { // If true, the url will not be shown in the list of urls in the top level resources view. bool is_internal = 3; // Display properties of the Url - optional EndpointDisplayProperties display_properties = 4; + optional UrlDisplayProperties display_properties = 4; } -message EndpointDisplayProperties { +message UrlDisplayProperties { // The sort order of the url. Lower values are displayed first in the UI. The absence of a value is treated as lowest order. optional int32 sort_order = 1; // The display name of the url, to appear in the UI. diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 64f72ddf9b..734c7e9273 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -533,11 +533,10 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build /// An optional name of the environment variable that will be used to inject the . If the target port is null one will be dynamically generated and assigned to the environment variable. /// Indicates that this endpoint should be exposed externally at publish time. /// Specifies if the endpoint will be proxied by DCP. Defaults to true. - /// Function to configure Aspire Dashboard display properties for this endpoint. /// The . /// Throws an exception if an endpoint with the same name already exists on the specified resource. [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "")] - public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, string? scheme = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true, bool? isExternal = null, Action? displayProperties = null) where T : IResourceWithEndpoints + public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, string? scheme = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true, bool? isExternal = null) where T : IResourceWithEndpoints { ArgumentNullException.ThrowIfNull(builder); @@ -548,8 +547,7 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build port: port, targetPort: targetPort, isExternal: isExternal, - isProxied: isProxied, - displayProperties: displayProperties); + isProxied: isProxied); if (builder.Resource.Annotations.OfType().Any(sb => string.Equals(sb.Name, annotation.Name, StringComparisons.EndpointAnnotationName))) { @@ -583,14 +581,13 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build /// An optional name of the endpoint. Defaults to "http" if not specified. /// An optional name of the environment variable to inject. /// Specifies if the endpoint will be proxied by DCP. Defaults to true. - /// Function to configure Aspire Dashboard display properties for this endpoint. /// The . /// Throws an exception if an endpoint with the same name already exists on the specified resource. - public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true, Action? displayProperties = null) where T : IResourceWithEndpoints + public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true) where T : IResourceWithEndpoints { ArgumentNullException.ThrowIfNull(builder); - return builder.WithEndpoint(targetPort: targetPort, port: port, scheme: "http", name: name, env: env, isProxied: isProxied, displayProperties: displayProperties); + return builder.WithEndpoint(targetPort: targetPort, port: port, scheme: "http", name: name, env: env, isProxied: isProxied); } /// @@ -604,14 +601,13 @@ public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder b /// An optional name of the endpoint. Defaults to "https" if not specified. /// An optional name of the environment variable to inject. /// Specifies if the endpoint will be proxied by DCP. Defaults to true. - /// Function to configure Aspire Dashboard display properties for this endpoint. /// The . /// Throws an exception if an endpoint with the same name already exists on the specified resource. - public static IResourceBuilder WithHttpsEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true, Action? displayProperties = null) where T : IResourceWithEndpoints + public static IResourceBuilder WithHttpsEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true) where T : IResourceWithEndpoints { ArgumentNullException.ThrowIfNull(builder); - return builder.WithEndpoint(targetPort: targetPort, port: port, scheme: "https", name: name, env: env, isProxied: isProxied, displayProperties: displayProperties); + return builder.WithEndpoint(targetPort: targetPort, port: port, scheme: "https", name: name, env: env, isProxied: isProxied); } /// @@ -641,7 +637,7 @@ public static IResourceBuilder WithExternalHttpEndpoints(this IResourceBui } /// - /// Gets an by name from the resource. These endpoints are declared either using or by launch settings (for project resources). + /// Gets an by name from the resource. These endpoints are declared either using or by launch settings (for project resources). /// The can be used to resolve the address of the endpoint in . /// /// The resource type. diff --git a/tests/Aspire.Dashboard.Tests/Model/ResourceEndpointHelpersTests.cs b/tests/Aspire.Dashboard.Tests/Model/ResourceEndpointHelpersTests.cs index d52d1356df..ca9bb39c3f 100644 --- a/tests/Aspire.Dashboard.Tests/Model/ResourceEndpointHelpersTests.cs +++ b/tests/Aspire.Dashboard.Tests/Model/ResourceEndpointHelpersTests.cs @@ -184,4 +184,43 @@ public void GetEndpoints_OrderByName() e => Assert.Equal("B", e.Name), e => Assert.Equal("D", e.Name)); } + + [Fact] + public void GetEndpoints_SortOrder_Combinations() + { + var endpoints = GetEndpoints(ModelTestHelpers.CreateResource(urls: [ + new("NoProperty", new("https://localhost:8079"), isInternal: false, displayProperties: null), + new("Zero", new("http://localhost:8080"), isInternal: false, displayProperties: new UrlDisplayPropertiesViewModel(null, 0)), + new("Null", new("http://localhost:8081"), isInternal: false, displayProperties: new UrlDisplayPropertiesViewModel(null, null)), + new("Positive", new("http://localhost:8082"), isInternal: false, displayProperties: new UrlDisplayPropertiesViewModel(null, 1)), + new("Negative", new("http://localhost:8083"), isInternal: false, displayProperties: new UrlDisplayPropertiesViewModel(null, -1)) + ])); + + Assert.Collection(endpoints, + e => + { + Assert.Equal("Positive", e.Name); + Assert.Equal("http://localhost:8082", e.Url); + }, + e => + { + Assert.Equal("NoProperty", e.Name); // tie broken by protocol (https) + Assert.Equal("https://localhost:8079", e.Url); + }, + e => + { + Assert.Equal("Null", e.Name); // tie broken by name + Assert.Equal("http://localhost:8081", e.Url); + }, + e => + { + Assert.Equal("Zero", e.Name); + Assert.Equal("http://localhost:8080", e.Url); + }, + e => + { + Assert.Equal("Negative", e.Name); + Assert.Equal("http://localhost:8083", e.Url); + }); + } } From a9f3a83cef7574ae744f75c942026ca663dc7158 Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Fri, 7 Feb 2025 16:49:53 -0500 Subject: [PATCH 08/10] Customize testshop frontend access endpoint --- .../TestShop/TestShop.AppHost/Program.cs | 4 ++++ .../ResourceBuilderExtensions.cs | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 playground/TestShop/TestShop.AppHost/ResourceBuilderExtensions.cs diff --git a/playground/TestShop/TestShop.AppHost/Program.cs b/playground/TestShop/TestShop.AppHost/Program.cs index cc48c60dac..ecec61a6b7 100644 --- a/playground/TestShop/TestShop.AppHost/Program.cs +++ b/playground/TestShop/TestShop.AppHost/Program.cs @@ -38,6 +38,10 @@ builder.AddProject("frontend") .WithExternalHttpEndpoints() + .WithModifiedEndpoints(c => + { + c.DisplayProperties.DisplayName = "TestShop frontend access"; + }) .WithReference(basketService) .WithReference(catalogService); diff --git a/playground/TestShop/TestShop.AppHost/ResourceBuilderExtensions.cs b/playground/TestShop/TestShop.AppHost/ResourceBuilderExtensions.cs new file mode 100644 index 0000000000..4b98baea0e --- /dev/null +++ b/playground/TestShop/TestShop.AppHost/ResourceBuilderExtensions.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting; + +public static class ResourceBuilderExtensions +{ + public static IResourceBuilder WithModifiedEndpoints(this IResourceBuilder builder, Action callback) where T : IResourceWithEndpoints + { + ArgumentNullException.ThrowIfNull(builder); + + if (!builder.Resource.TryGetAnnotationsOfType(out var endpoints)) + { + return builder; + } + + foreach (var endpoint in endpoints) + { + callback(endpoint); + } + + return builder; + } +} From 51f030cab2729b2a08fa38e0f1a52daa42136484 Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Mon, 10 Feb 2025 15:09:31 -0500 Subject: [PATCH 09/10] Make display properties not nullable, remove breaking change --- .../Components/Controls/ResourceDetails.razor | 19 ++++++++----------- .../Model/ResourceEndpointHelpers.cs | 12 ++++++------ .../Model/ResourceViewModel.cs | 7 ++++--- .../ResourceService/Partials.cs | 2 +- .../CustomResourceSnapshot.cs | 11 ++++++++--- .../EndpointDisplayProperties.cs | 6 +++--- .../Dashboard/proto/Partials.cs | 2 +- .../Dashboard/proto/resource_service.proto | 6 +++--- .../Dcp/ResourceSnapshotBuilder.cs | 6 +++--- 9 files changed, 37 insertions(+), 34 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor b/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor index 3426b06d0a..6e4f9c564d 100644 --- a/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor +++ b/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor @@ -65,7 +65,7 @@ @{ - var hasAnyEndpointDisplayNames = FilteredEndpoints.Any(e => e.DisplayName != null); + var hasAnyEndpointDisplayNames = FilteredEndpoints.Any(e => !string.IsNullOrEmpty(e.DisplayName)); } - + @if (hasAnyEndpointDisplayNames) { - - @if (context.DisplayName is not null) - { - - } + + } - GetEndpoints(ResourceViewModel resource, b Address = url.Url.Host, Port = url.Url.Port, Url = url.Url.Scheme is "http" or "https" ? url.Url.OriginalString : null, - SortOrder = url.DisplayProperties?.SortOrder, - DisplayName = url.DisplayProperties?.DisplayName, + SortOrder = url.DisplayProperties.SortOrder, + DisplayName = url.DisplayProperties.DisplayName, OriginalUrlString = url.Url.OriginalString, - Text = url.DisplayProperties?.DisplayName ?? url.Url.OriginalString + Text = string.IsNullOrEmpty(url.DisplayProperties.DisplayName) ? url.Url.OriginalString : url.DisplayProperties.DisplayName }); } } @@ -39,7 +39,7 @@ public static List GetEndpoints(ResourceViewModel resource, b // - other urls // - endpoint name var orderedEndpoints = endpoints - .OrderByDescending(e => e.SortOrder ?? 0) + .OrderByDescending(e => e.SortOrder) .ThenByDescending(e => e.Url?.StartsWith("https") == true) .ThenByDescending(e => e.Url != null) .ThenBy(e => e.Name, StringComparers.EndpointAnnotationName) @@ -57,8 +57,8 @@ public sealed class DisplayedEndpoint : IPropertyGridItem public string? Address { get; set; } public int? Port { get; set; } public string? Url { get; set; } - public int? SortOrder { get; set; } - public string? DisplayName { get; set; } + public int SortOrder { get; set; } + public required string DisplayName { get; set; } public required string OriginalUrlString { get; set; } /// diff --git a/src/Aspire.Dashboard/Model/ResourceViewModel.cs b/src/Aspire.Dashboard/Model/ResourceViewModel.cs index 7408d6a6cb..ef775f7804 100644 --- a/src/Aspire.Dashboard/Model/ResourceViewModel.cs +++ b/src/Aspire.Dashboard/Model/ResourceViewModel.cs @@ -324,12 +324,13 @@ public sealed class UrlViewModel public string Name { get; } public Uri Url { get; } public bool IsInternal { get; } - public UrlDisplayPropertiesViewModel? DisplayProperties { get; } + public UrlDisplayPropertiesViewModel DisplayProperties { get; } - public UrlViewModel(string name, Uri url, bool isInternal, UrlDisplayPropertiesViewModel? displayProperties = null) + public UrlViewModel(string name, Uri url, bool isInternal, UrlDisplayPropertiesViewModel displayProperties) { ArgumentException.ThrowIfNullOrWhiteSpace(name); ArgumentNullException.ThrowIfNull(url); + ArgumentNullException.ThrowIfNull(displayProperties); Name = name; Url = url; @@ -338,7 +339,7 @@ public UrlViewModel(string name, Uri url, bool isInternal, UrlDisplayPropertiesV } } -public record UrlDisplayPropertiesViewModel(string? DisplayName, int? SortOrder); +public record UrlDisplayPropertiesViewModel(string DisplayName, int SortOrder); public sealed record class VolumeViewModel(int index, string Source, string Target, string MountType, bool IsReadOnly) : IPropertyGridItem { diff --git a/src/Aspire.Dashboard/ResourceService/Partials.cs b/src/Aspire.Dashboard/ResourceService/Partials.cs index d0c5df5949..d6faa80d9e 100644 --- a/src/Aspire.Dashboard/ResourceService/Partials.cs +++ b/src/Aspire.Dashboard/ResourceService/Partials.cs @@ -95,7 +95,7 @@ ImmutableArray GetUrls() return (from u in Urls let parsedUri = Uri.TryCreate(u.FullUrl, UriKind.Absolute, out var uri) ? uri : null where parsedUri != null - select new UrlViewModel(u.Name, parsedUri, u.IsInternal, u.DisplayProperties is not null ? new UrlDisplayPropertiesViewModel(u.DisplayProperties.HasDisplayName ? u.DisplayProperties.DisplayName : null, u.DisplayProperties.HasSortOrder ? u.DisplayProperties.SortOrder : null) : null)) + select new UrlViewModel(u.Name, parsedUri, u.IsInternal,new UrlDisplayPropertiesViewModel(u.DisplayProperties.DisplayName, u.DisplayProperties.SortOrder))) .ToImmutableArray(); } diff --git a/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs b/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs index a0ee7661d7..dfb51db8e6 100644 --- a/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs +++ b/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs @@ -177,16 +177,21 @@ public sealed record EnvironmentVariableSnapshot(string Name, string? Value, boo /// Name of the url. /// The full uri. /// Determines if this url is internal. -/// The UI display properties for the url. [DebuggerDisplay("{Url}", Name = "{Name}")] -public sealed record UrlSnapshot(string Name, string Url, bool IsInternal, UrlDisplayPropertiesSnapshot? DisplayProperties = null); +public sealed record UrlSnapshot(string Name, string Url, bool IsInternal) +{ + /// + /// The UI display properties for the url. + /// + public UrlDisplayPropertiesSnapshot DisplayProperties { get; init; } = new(); +} /// /// A snapshot of the display properties for a url. /// /// The display name of the url. /// The order of the url in UI. Higher numbers are displayed first in the UI. -public sealed record UrlDisplayPropertiesSnapshot(string? DisplayName = null, int? SortOrder = null); +public sealed record UrlDisplayPropertiesSnapshot(string DisplayName = "", int SortOrder = 0); /// /// A snapshot of a volume, mounted to a container. diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointDisplayProperties.cs b/src/Aspire.Hosting/ApplicationModel/EndpointDisplayProperties.cs index 2f01f19a94..b755da9203 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointDisplayProperties.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointDisplayProperties.cs @@ -9,13 +9,13 @@ namespace Aspire.Hosting.ApplicationModel; public sealed class EndpointDisplayProperties { /// - /// Display name of the endpoint, to be displayed in the Aspire Dashboard. + /// Display name of the endpoint, to be displayed in the Aspire Dashboard. An empty display name will default to the endpoint name. /// - public string? DisplayName { get; set; } + public string DisplayName { get; set; } = string.Empty; /// /// Integer to control visual ordering of endpoints in the Aspire Dashboard. Higher values are displayed first. /// Ties are broken by protocol type first (https before others), then by endpoint name. /// - public int? SortOrder { get; set; } + public int SortOrder { get; set; } } diff --git a/src/Aspire.Hosting/Dashboard/proto/Partials.cs b/src/Aspire.Hosting/Dashboard/proto/Partials.cs index c98547657b..034a037325 100644 --- a/src/Aspire.Hosting/Dashboard/proto/Partials.cs +++ b/src/Aspire.Hosting/Dashboard/proto/Partials.cs @@ -51,7 +51,7 @@ public static Resource FromSnapshot(ResourceSnapshot snapshot) if (urlSnapshot.DisplayProperties?.SortOrder is not null) { - displayProperties.SortOrder = urlSnapshot.DisplayProperties.SortOrder.Value; + displayProperties.SortOrder = urlSnapshot.DisplayProperties.SortOrder; } url.DisplayProperties = displayProperties; diff --git a/src/Aspire.Hosting/Dashboard/proto/resource_service.proto b/src/Aspire.Hosting/Dashboard/proto/resource_service.proto index e01d734fe2..090948cfd7 100644 --- a/src/Aspire.Hosting/Dashboard/proto/resource_service.proto +++ b/src/Aspire.Hosting/Dashboard/proto/resource_service.proto @@ -132,14 +132,14 @@ message Url { // If true, the url will not be shown in the list of urls in the top level resources view. bool is_internal = 3; // Display properties of the Url - optional UrlDisplayProperties display_properties = 4; + UrlDisplayProperties display_properties = 4; } message UrlDisplayProperties { // The sort order of the url. Lower values are displayed first in the UI. The absence of a value is treated as lowest order. - optional int32 sort_order = 1; + int32 sort_order = 1; // The display name of the url, to appear in the UI. - optional string display_name = 2; + string display_name = 2; } // Data about a volume mounted to a container. diff --git a/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs b/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs index ea79286e4d..46ece4b625 100644 --- a/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs +++ b/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs @@ -236,21 +236,21 @@ string CombineUrls(string url, string launchUrl) { var url = CombineUrls(ep.Url, launchUrl); - urls.Add(new(Name: ep.EndpointName, Url: url, IsInternal: false, DisplayProperties: new(ep.DisplayProperties.DisplayName, ep.DisplayProperties.SortOrder))); + urls.Add(new(Name: ep.EndpointName, Url: url, IsInternal: false) { DisplayProperties = new(ep.DisplayProperties.DisplayName, ep.DisplayProperties.SortOrder)}); } } else { if (ep.IsAllocated) { - urls.Add(new(Name: ep.EndpointName, Url: ep.Url, IsInternal: false, DisplayProperties: new(ep.DisplayProperties.DisplayName, ep.DisplayProperties.SortOrder))); + urls.Add(new(Name: ep.EndpointName, Url: ep.Url, IsInternal: false) { DisplayProperties = new(ep.DisplayProperties.DisplayName, ep.DisplayProperties.SortOrder) }); } } if (ep.EndpointAnnotation.IsProxied) { var endpointString = $"{ep.Scheme}://{endpoint.Spec.Address}:{endpoint.Spec.Port}"; - urls.Add(new(Name: $"{ep.EndpointName} target port", Url: endpointString, IsInternal: true, DisplayProperties: new(ep.DisplayProperties.DisplayName, ep.DisplayProperties.SortOrder))); + urls.Add(new(Name: $"{ep.EndpointName} target port", Url: endpointString, IsInternal: true) { DisplayProperties = new(ep.DisplayProperties.DisplayName, ep.DisplayProperties.SortOrder) }); } } } From 8d1fe6664f0499a770a6d97ff41a0442fb4710cf Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 11 Feb 2025 10:07:02 +0800 Subject: [PATCH 10/10] Fix build --- .../TestShop/TestShop.AppHost/Program.cs | 13 +++-- .../ResourceBuilderExtensions.cs | 24 --------- .../Model/ResourceViewModel.cs | 5 +- .../Model/ResourceEndpointHelpersTests.cs | 50 ++++++++----------- .../ResourceOutgoingPeerResolverTests.cs | 2 +- 5 files changed, 35 insertions(+), 59 deletions(-) delete mode 100644 playground/TestShop/TestShop.AppHost/ResourceBuilderExtensions.cs diff --git a/playground/TestShop/TestShop.AppHost/Program.cs b/playground/TestShop/TestShop.AppHost/Program.cs index ecec61a6b7..5a450b4706 100644 --- a/playground/TestShop/TestShop.AppHost/Program.cs +++ b/playground/TestShop/TestShop.AppHost/Program.cs @@ -2,7 +2,10 @@ var catalogDb = builder.AddPostgres("postgres") .WithDataVolume() - .WithPgAdmin() + .WithPgAdmin(resource => + { + resource.WithEndpoint("http", e => e.DisplayProperties.DisplayName = "PG Admin"); + }) .AddDatabase("catalogdb"); var basketCache = builder.AddRedis("basketcache") @@ -12,10 +15,12 @@ basketCache.WithRedisCommander(c => { c.WithHostPort(33801); + c.WithEndpoint("http", e => e.DisplayProperties.DisplayName = "Redis Commander"); }) .WithRedisInsight(c => { c.WithHostPort(33802); + c.WithEndpoint("http", e => e.DisplayProperties.DisplayName = "Redis Insight"); }); #endif @@ -38,10 +43,8 @@ builder.AddProject("frontend") .WithExternalHttpEndpoints() - .WithModifiedEndpoints(c => - { - c.DisplayProperties.DisplayName = "TestShop frontend access"; - }) + .WithEndpoint("http", c => c.DisplayProperties.DisplayName = $"TestShop UI ({c.UriScheme})") + .WithEndpoint("https", c => c.DisplayProperties.DisplayName = $"TestShop UI ({c.UriScheme})") .WithReference(basketService) .WithReference(catalogService); diff --git a/playground/TestShop/TestShop.AppHost/ResourceBuilderExtensions.cs b/playground/TestShop/TestShop.AppHost/ResourceBuilderExtensions.cs deleted file mode 100644 index 4b98baea0e..0000000000 --- a/playground/TestShop/TestShop.AppHost/ResourceBuilderExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Aspire.Hosting; - -public static class ResourceBuilderExtensions -{ - public static IResourceBuilder WithModifiedEndpoints(this IResourceBuilder builder, Action callback) where T : IResourceWithEndpoints - { - ArgumentNullException.ThrowIfNull(builder); - - if (!builder.Resource.TryGetAnnotationsOfType(out var endpoints)) - { - return builder; - } - - foreach (var endpoint in endpoints) - { - callback(endpoint); - } - - return builder; - } -} diff --git a/src/Aspire.Dashboard/Model/ResourceViewModel.cs b/src/Aspire.Dashboard/Model/ResourceViewModel.cs index ef775f7804..9d87dc89a5 100644 --- a/src/Aspire.Dashboard/Model/ResourceViewModel.cs +++ b/src/Aspire.Dashboard/Model/ResourceViewModel.cs @@ -339,7 +339,10 @@ public UrlViewModel(string name, Uri url, bool isInternal, UrlDisplayPropertiesV } } -public record UrlDisplayPropertiesViewModel(string DisplayName, int SortOrder); +public record UrlDisplayPropertiesViewModel(string DisplayName, int SortOrder) +{ + public static readonly UrlDisplayPropertiesViewModel Empty = new(string.Empty, 0); +} public sealed record class VolumeViewModel(int index, string Source, string Target, string MountType, bool IsReadOnly) : IPropertyGridItem { diff --git a/tests/Aspire.Dashboard.Tests/Model/ResourceEndpointHelpersTests.cs b/tests/Aspire.Dashboard.Tests/Model/ResourceEndpointHelpersTests.cs index ca9bb39c3f..495a422532 100644 --- a/tests/Aspire.Dashboard.Tests/Model/ResourceEndpointHelpersTests.cs +++ b/tests/Aspire.Dashboard.Tests/Model/ResourceEndpointHelpersTests.cs @@ -25,7 +25,7 @@ public void GetEndpoints_Empty_NoResults() [Fact] public void GetEndpoints_HasServices_Results() { - var endpoints = GetEndpoints(ModelTestHelpers.CreateResource(urls: [new("Test", new("http://localhost:8080"), isInternal: false)])); + var endpoints = GetEndpoints(ModelTestHelpers.CreateResource(urls: [new("Test", new("http://localhost:8080"), isInternal: false, displayProperties: UrlDisplayPropertiesViewModel.Empty)])); Assert.Collection(endpoints, e => @@ -42,8 +42,8 @@ public void GetEndpoints_HasServices_Results() public void GetEndpoints_HasEndpointAndService_Results() { var endpoints = GetEndpoints(ModelTestHelpers.CreateResource(urls: [ - new("Test", new("http://localhost:8080"), isInternal: false), - new("Test2", new("http://localhost:8081"), isInternal: false)]) + new("Test", new("http://localhost:8080"), isInternal: false, displayProperties: UrlDisplayPropertiesViewModel.Empty), + new("Test2", new("http://localhost:8081"), isInternal: false, displayProperties: UrlDisplayPropertiesViewModel.Empty)]) ); Assert.Collection(endpoints, @@ -69,8 +69,8 @@ public void GetEndpoints_HasEndpointAndService_Results() public void GetEndpoints_OnlyHttpAndHttpsEndpointsSetTheUrl() { var endpoints = GetEndpoints(ModelTestHelpers.CreateResource(urls: [ - new("Test", new("http://localhost:8080"), isInternal: false), - new("Test2", new("tcp://localhost:8081"), isInternal: false)]) + new("Test", new("http://localhost:8080"), isInternal: false, displayProperties: UrlDisplayPropertiesViewModel.Empty), + new("Test2", new("tcp://localhost:8081"), isInternal: false, displayProperties: UrlDisplayPropertiesViewModel.Empty)]) ); Assert.Collection(endpoints, @@ -96,8 +96,8 @@ public void GetEndpoints_OnlyHttpAndHttpsEndpointsSetTheUrl() public void GetEndpoints_IncludeEndpointUrl_HasEndpointAndService_Results() { var endpoints = GetEndpoints(ModelTestHelpers.CreateResource(urls: [ - new("First", new("https://localhost:8080/test"), isInternal:false), - new("Test", new("https://localhost:8081/test2"), isInternal:false) + new("First", new("https://localhost:8080/test"), isInternal:false, displayProperties: UrlDisplayPropertiesViewModel.Empty), + new("Test", new("https://localhost:8081/test2"), isInternal:false, displayProperties: UrlDisplayPropertiesViewModel.Empty) ])); Assert.Collection(endpoints, @@ -123,8 +123,8 @@ public void GetEndpoints_IncludeEndpointUrl_HasEndpointAndService_Results() public void GetEndpoints_ExcludesIncludeInternalUrls() { var endpoints = GetEndpoints(ModelTestHelpers.CreateResource(urls: [ - new("First", new("https://localhost:8080/test"), isInternal:true), - new("Test", new("https://localhost:8081/test2"), isInternal:false) + new("First", new("https://localhost:8080/test"), isInternal:true, displayProperties: UrlDisplayPropertiesViewModel.Empty), + new("Test", new("https://localhost:8081/test2"), isInternal:false, displayProperties: UrlDisplayPropertiesViewModel.Empty) ])); Assert.Collection(endpoints, @@ -142,8 +142,8 @@ public void GetEndpoints_ExcludesIncludeInternalUrls() public void GetEndpoints_IncludesIncludeInternalUrls() { var endpoints = GetEndpoints(ModelTestHelpers.CreateResource(urls: [ - new("First", new("https://localhost:8080/test"), isInternal:true), - new("Test", new("https://localhost:8081/test2"), isInternal:false) + new("First", new("https://localhost:8080/test"), isInternal:true, displayProperties: UrlDisplayPropertiesViewModel.Empty), + new("Test", new("https://localhost:8081/test2"), isInternal:false, displayProperties: UrlDisplayPropertiesViewModel.Empty) ]), includeInternalUrls: true); @@ -170,11 +170,11 @@ public void GetEndpoints_IncludesIncludeInternalUrls() public void GetEndpoints_OrderByName() { var endpoints = GetEndpoints(ModelTestHelpers.CreateResource(urls: [ - new("a", new("http://localhost:8080"), isInternal: false), - new("C", new("http://localhost:8080"), isInternal: false), - new("D", new("tcp://localhost:8080"), isInternal: false), - new("B", new("tcp://localhost:8080"), isInternal: false), - new("Z", new("https://localhost:8080"), isInternal: false) + new("a", new("http://localhost:8080"), isInternal: false, displayProperties: UrlDisplayPropertiesViewModel.Empty), + new("C", new("http://localhost:8080"), isInternal: false, displayProperties: UrlDisplayPropertiesViewModel.Empty), + new("D", new("tcp://localhost:8080"), isInternal: false, displayProperties: UrlDisplayPropertiesViewModel.Empty), + new("B", new("tcp://localhost:8080"), isInternal: false, displayProperties: UrlDisplayPropertiesViewModel.Empty), + new("Z", new("https://localhost:8080"), isInternal: false, displayProperties: UrlDisplayPropertiesViewModel.Empty) ])); Assert.Collection(endpoints, @@ -189,11 +189,10 @@ public void GetEndpoints_OrderByName() public void GetEndpoints_SortOrder_Combinations() { var endpoints = GetEndpoints(ModelTestHelpers.CreateResource(urls: [ - new("NoProperty", new("https://localhost:8079"), isInternal: false, displayProperties: null), - new("Zero", new("http://localhost:8080"), isInternal: false, displayProperties: new UrlDisplayPropertiesViewModel(null, 0)), - new("Null", new("http://localhost:8081"), isInternal: false, displayProperties: new UrlDisplayPropertiesViewModel(null, null)), - new("Positive", new("http://localhost:8082"), isInternal: false, displayProperties: new UrlDisplayPropertiesViewModel(null, 1)), - new("Negative", new("http://localhost:8083"), isInternal: false, displayProperties: new UrlDisplayPropertiesViewModel(null, -1)) + new("Zero-Https", new("https://localhost:8079"), isInternal: false, displayProperties: new UrlDisplayPropertiesViewModel(string.Empty, 0)), + new("Zero-Http", new("http://localhost:8080"), isInternal: false, displayProperties: new UrlDisplayPropertiesViewModel(string.Empty, 0)), + new("Positive", new("http://localhost:8082"), isInternal: false, displayProperties: new UrlDisplayPropertiesViewModel(string.Empty, 1)), + new("Negative", new("http://localhost:8083"), isInternal: false, displayProperties: new UrlDisplayPropertiesViewModel(string.Empty, -1)) ])); Assert.Collection(endpoints, @@ -204,17 +203,12 @@ public void GetEndpoints_SortOrder_Combinations() }, e => { - Assert.Equal("NoProperty", e.Name); // tie broken by protocol (https) + Assert.Equal("Zero-Https", e.Name); // tie broken by protocol (https) Assert.Equal("https://localhost:8079", e.Url); }, e => { - Assert.Equal("Null", e.Name); // tie broken by name - Assert.Equal("http://localhost:8081", e.Url); - }, - e => - { - Assert.Equal("Zero", e.Name); + Assert.Equal("Zero-Http", e.Name); Assert.Equal("http://localhost:8080", e.Url); }, e => diff --git a/tests/Aspire.Dashboard.Tests/ResourceOutgoingPeerResolverTests.cs b/tests/Aspire.Dashboard.Tests/ResourceOutgoingPeerResolverTests.cs index 5509fa4209..8eba2d08f7 100644 --- a/tests/Aspire.Dashboard.Tests/ResourceOutgoingPeerResolverTests.cs +++ b/tests/Aspire.Dashboard.Tests/ResourceOutgoingPeerResolverTests.cs @@ -17,7 +17,7 @@ private static ResourceViewModel CreateResource(string name, string? serviceAddr return ModelTestHelpers.CreateResource( appName: name, displayName: displayName, - urls: serviceAddress is null || servicePort is null ? [] : [new UrlViewModel(name, new($"http://{serviceAddress}:{servicePort}"), isInternal: false)]); + urls: serviceAddress is null || servicePort is null ? [] : [new UrlViewModel(name, new($"http://{serviceAddress}:{servicePort}"), isInternal: false, displayProperties: UrlDisplayPropertiesViewModel.Empty)]); } [Fact]