Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion playground/TestShop/TestShop.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
.WithDataVolume()
.WithPgAdmin(resource =>
{
resource.WithUrlForEndpoint("http", u => u.DisplayText = "PG Admin");
resource.WithUrlForEndpoint("http", u =>
{
u.DisplayText = "PG Admin";
u.IconName = "DatabaseStack";
u.IconVariant = IconVariant.Filled;
});
})
.AddDatabase("catalogdb");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ else if (DisplayedUrls.Count > 1)
{
var d = (DisplayedUrl)item.Data!;
<div class="url-link">
@WriteUrl(d)
@WriteUrl(d, displayLabel: true)
</div>
}
</div>
Expand All @@ -62,10 +62,18 @@ else if (DisplayedUrls.Count > 1)
}

@code {
RenderFragment WriteUrl(DisplayedUrl displayedUrl)
RenderFragment WriteUrl(DisplayedUrl displayedUrl, bool displayLabel = false)
{
if (displayedUrl.Url != null)
{
if (string.IsNullOrWhiteSpace(displayedUrl.IconName) == false && IconResolver.ResolveIconName(displayedUrl.IconName, IconSize.Size16, iconVariant: displayedUrl.IconVariant) is { } icon)
{
return @<FluentButton Appearance="Appearance.Lightweight" Title="@displayedUrl.Text" AriaLabel="@displayedUrl.Text" StopPropagation="true" OnClick="@(() => JSRuntime.InvokeAsync<object>("open", displayedUrl.Url, "_blank"))" >
<FluentIcon Value="@icon" Width="16px" Title="@displayedUrl.Text" />
</FluentButton>;
}


return @<a href="@displayedUrl.Url" target="_blank" @onclick:stopPropagation="true">@displayedUrl.Text</a>;
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Aspire.Dashboard.Resources;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Localization;
using Microsoft.JSInterop;

namespace Aspire.Dashboard.Components;

Expand All @@ -25,5 +26,14 @@ public partial class UrlsColumnDisplay
[Inject]
public required IStringLocalizer<Columns> Loc { get; init; }

[Inject]
public required IconResolver IconResolver { get; init; }

[Inject]
public required NavigationManager NavigationManager { get; init; }

[Inject]
public required IJSRuntime JSRuntime { get; init; }

private bool _popoverVisible;
}
8 changes: 7 additions & 1 deletion src/Aspire.Dashboard/Model/ResourceUrlHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Diagnostics;
using Aspire.Dashboard.Components.Controls;
using Microsoft.FluentUI.AspNetCore.Components;

namespace Aspire.Dashboard.Model;

Expand Down Expand Up @@ -51,7 +52,9 @@ public static List<DisplayedUrl> GetUrls(ResourceViewModel resource, bool includ
SortOrder = url.DisplayProperties.SortOrder,
DisplayName = string.IsNullOrEmpty(url.DisplayProperties.DisplayName) ? null : url.DisplayProperties.DisplayName,
OriginalUrlString = url.Url.OriginalString,
Text = string.IsNullOrEmpty(url.DisplayProperties.DisplayName) ? url.Url.OriginalString : url.DisplayProperties.DisplayName
Text = string.IsNullOrEmpty(url.DisplayProperties.DisplayName) ? url.Url.OriginalString : url.DisplayProperties.DisplayName,
IconName = url.DisplayProperties.IconName,
IconVariant = url.DisplayProperties.IconVariant,
});
index++;
}
Expand Down Expand Up @@ -86,6 +89,9 @@ public sealed class DisplayedUrl : IPropertyGridItem
public string? DisplayName { get; set; }
public required string OriginalUrlString { get; set; }

public string? IconName { get; set; }
public IconVariant? IconVariant { get; set; }

/// <summary>
/// Don't display a plain string value here. The URL will be displayed as a hyperlink
/// in <see cref="ResourceDetails.RenderAddressValue(DisplayedUrl, string)"/> instead.
Expand Down
4 changes: 2 additions & 2 deletions src/Aspire.Dashboard/Model/ResourceViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -428,9 +428,9 @@ public UrlViewModel(string? endpointName, Uri url, bool isInternal, bool isInact
}
}

public record UrlDisplayPropertiesViewModel(string DisplayName, int SortOrder)
public record UrlDisplayPropertiesViewModel(string DisplayName, int SortOrder, string? IconName, IconVariant? IconVariant)
{
public static readonly UrlDisplayPropertiesViewModel Empty = new(string.Empty, 0);
public static readonly UrlDisplayPropertiesViewModel Empty = new(string.Empty, 0, string.Empty, null);
}

public sealed record class VolumeViewModel(int index, string Source, string Target, string MountType, bool IsReadOnly) : IPropertyGridItem
Expand Down
12 changes: 11 additions & 1 deletion src/Aspire.Dashboard/ServiceClient/Partials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,21 @@ static string TranslateKnownUrlName(Url url)
};
}

static FluentUIIconVariant MapIconVariant(IconVariant iconVariant)
{
return iconVariant switch
{
IconVariant.Regular => FluentUIIconVariant.Regular,
IconVariant.Filled => FluentUIIconVariant.Filled,
_ => throw new InvalidOperationException("Unknown icon variant: " + iconVariant),
};
}

// Filter out bad urls
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.EndpointName, parsedUri, u.IsInternal, u.IsInactive, new UrlDisplayPropertiesViewModel(TranslateKnownUrlName(u), u.DisplayProperties.SortOrder)))
select new UrlViewModel(u.EndpointName, parsedUri, u.IsInternal, u.IsInactive, new UrlDisplayPropertiesViewModel(TranslateKnownUrlName(u), u.DisplayProperties.SortOrder, u.DisplayProperties.IconName, MapIconVariant(u.DisplayProperties.IconVariant))))
.ToImmutableArray();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,10 @@ internal void Deconstruct(out string? name, out string url, out bool isInternal,
/// </summary>
/// <param name="DisplayName">The display name of the url.</param>
/// <param name="SortOrder">The order of the url in UI. Higher numbers are displayed first in the UI.</param>
public sealed record UrlDisplayPropertiesSnapshot(string DisplayName = "", int SortOrder = 0);
/// <param name="IconName">The icon name for the Url. The name should be a valid FluentUI icon name. https://aka.ms/fluentui-system-icons</param>
/// <param name="IconVariant">The icon variant.</param>

public sealed record UrlDisplayPropertiesSnapshot(string DisplayName = "", int SortOrder = 0, string? IconName = null, IconVariant? IconVariant = null);

/// <summary>
/// A snapshot of a volume, mounted to a container.
Expand Down
12 changes: 11 additions & 1 deletion src/Aspire.Hosting/ApplicationModel/ResourceUrlAnnotation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ public sealed class ResourceUrlAnnotation : IResourceAnnotation
/// </summary>
public UrlDisplayLocation DisplayLocation { get; set; } = UrlDisplayLocation.SummaryAndDetails;

/// <summary>
/// The name of the icon to display with this URL.
/// </summary>
public string? IconName { get; set; }

/// <summary>
/// The variant of the icon to display with this URL.
/// </summary>
public IconVariant? IconVariant { get; set; }

internal bool IsInternal => DisplayLocation == UrlDisplayLocation.DetailsOnly;

internal ResourceUrlAnnotation WithEndpoint(EndpointReference endpoint)
Expand All @@ -46,7 +56,7 @@ internal ResourceUrlAnnotation WithEndpoint(EndpointReference endpoint)
DisplayText = DisplayText,
Endpoint = endpoint,
DisplayOrder = DisplayOrder,
DisplayLocation = DisplayLocation
DisplayLocation = DisplayLocation,
Copy link

Copilot AI Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WithEndpoint method does not copy the IconName and IconVariant properties to the new annotation instance. These properties should be included in the object initializer to preserve icon settings when creating endpoint-specific annotations.

Suggested change
DisplayLocation = DisplayLocation,
DisplayLocation = DisplayLocation,
IconName = IconName,
IconVariant = IconVariant,

Copilot uses AI. Check for mistakes.
};
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/Aspire.Hosting/Dashboard/proto/Partials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ public static Resource FromSnapshot(ResourceSnapshot snapshot)
displayProperties.SortOrder = urlSnapshot.DisplayProperties.SortOrder;
}

if (urlSnapshot.DisplayProperties?.IconName is not null)
{
displayProperties.IconName = urlSnapshot.DisplayProperties.IconName;
}

if (urlSnapshot.DisplayProperties?.IconVariant is not null)
{
displayProperties.IconVariant = MapIconVariant(urlSnapshot.DisplayProperties.IconVariant);
}

url.DisplayProperties = displayProperties;
resource.Urls.Add(url);
}
Expand Down
5 changes: 5 additions & 0 deletions src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ message UrlDisplayProperties {
int32 sort_order = 1;
// The display name of the url, to appear in the UI.
string display_name = 2;
// Optional icon name. This name should be a valid FluentUI icon name.
// https://aka.ms/fluentui-system-icons
optional string icon_name = 3;
// Optional icon variant.
optional IconVariant icon_variant = 4;
}

// Data about a volume mounted to a container.
Expand Down
4 changes: 2 additions & 2 deletions src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -255,15 +255,15 @@ private ImmutableArray<UrlSnapshot> GetUrls(CustomResource resource, string? res
var activeEndpoint = _resourceState.EndpointsMap.SingleOrDefault(e => e.Value.Spec.ServiceName == serviceName && e.Value.Metadata.OwnerReferences?.Any(or => or.Kind == resource.Kind && or.Name == name) == true).Value;
var isInactive = activeEndpoint is null;

urls.Add(new(Name: endpointUrl.Endpoint!.EndpointName, Url: endpointUrl.Url, IsInternal: endpointUrl.IsInternal) { IsInactive = isInactive, DisplayProperties = new(endpointUrl.DisplayText ?? "", endpointUrl.DisplayOrder ?? 0) });
urls.Add(new(Name: endpointUrl.Endpoint!.EndpointName, Url: endpointUrl.Url, IsInternal: endpointUrl.IsInternal) { IsInactive = isInactive, DisplayProperties = new(endpointUrl.DisplayText ?? "", endpointUrl.DisplayOrder ?? 0, endpointUrl.IconName, endpointUrl.IconVariant) });
}
}

// Add the non-endpoint URLs
var resourceRunning = string.Equals(resourceState, KnownResourceStates.Running, StringComparisons.ResourceState);
foreach (var url in nonEndpointUrls)
{
urls.Add(new(Name: null, Url: url.Url, IsInternal: url.IsInternal) { IsInactive = !resourceRunning, DisplayProperties = new(url.DisplayText ?? "", url.DisplayOrder ?? 0) });
urls.Add(new(Name: null, Url: url.Url, IsInternal: url.IsInternal) { IsInactive = !resourceRunning, DisplayProperties = new(url.DisplayText ?? "", url.DisplayOrder ?? 0, url.IconName, url.IconVariant) });
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ private static IEnumerable<UrlSnapshot> GetResourceUrls(IResource resource)
// Endpoint URLs are inactive (hidden in the dashboard) when published here. It is assumed they will get activated later when the endpoint is considered active
// by whatever allocated the endpoint in the first place, e.g. for resources controlled by DCP, when DCP detects the endpoint is listening.
IsInactive = url.Endpoint is not null,
DisplayProperties = new(url.DisplayText ?? "", url.DisplayOrder ?? 0)
DisplayProperties = new(url.DisplayText ?? "", url.DisplayOrder ?? 0, url.IconName, url.IconVariant)
});
}
return urls;
Expand Down
8 changes: 4 additions & 4 deletions tests/Aspire.Dashboard.Tests/Model/ResourceUrlHelpersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,10 @@
public void GetUrls_SortOrder_Combinations()
{
var endpoints = GetUrls(ModelTestHelpers.CreateResource(urls: [
new("Zero-Https", new("https://localhost:8079"), isInternal: false, isInactive: false, displayProperties: new UrlDisplayPropertiesViewModel(string.Empty, 0)),
new("Zero-Http", new("http://localhost:8080"), isInternal: false, isInactive: false, displayProperties: new UrlDisplayPropertiesViewModel(string.Empty, 0)),
new("Positive", new("http://localhost:8082"), isInternal: false, isInactive: false, displayProperties: new UrlDisplayPropertiesViewModel(string.Empty, 1)),
new("Negative", new("http://localhost:8083"), isInternal: false, isInactive: false, displayProperties: new UrlDisplayPropertiesViewModel(string.Empty, -1))
new("Zero-Https", new("https://localhost:8079"), isInternal: false, isInactive: false, displayProperties: new UrlDisplayPropertiesViewModel(string.Empty, 0, null, null)),
new("Zero-Http", new("http://localhost:8080"), isInternal: false, isInactive: false, displayProperties: new UrlDisplayPropertiesViewModel(string.Empty, 0 null, null)),

Check failure on line 248 in tests/Aspire.Dashboard.Tests/Model/ResourceUrlHelpersTests.cs

View check run for this annotation

Azure Pipelines / dotnet.aspire (Build Linux)

tests/Aspire.Dashboard.Tests/Model/ResourceUrlHelpersTests.cs#L248

tests/Aspire.Dashboard.Tests/Model/ResourceUrlHelpersTests.cs(248,167): error CS1003: (NETCORE_ENGINEERING_TELEMETRY=Build) Syntax error, ',' expected
new("Positive", new("http://localhost:8082"), isInternal: false, isInactive: false, displayProperties: new UrlDisplayPropertiesViewModel(string.Empty, 1 null, null)),
new("Negative", new("http://localhost:8083"), isInternal: false, isInactive: false, displayProperties: new UrlDisplayPropertiesViewModel(string.Empty, -1 null, null))
]));

Assert.Collection(endpoints,
Expand Down
Loading