Skip to content

Commit e7cea79

Browse files
CopilotmaddymontaquilaadamintDamianEdwardsJamesNK
authored
Allow resources to specify custom icons to use when displayed in the dashboard (#10760)
Co-authored-by: maddymontaquila <[email protected]> Co-authored-by: adamint <[email protected]> Co-authored-by: DamianEdwards <[email protected]> Co-authored-by: JamesNK <[email protected]> Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: James Newton-King <[email protected]>
1 parent 95ec9ad commit e7cea79

File tree

18 files changed

+538
-8
lines changed

18 files changed

+538
-8
lines changed

playground/Stress/Stress.AppHost/Program.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
// TODO: OTEL env var can be removed when OTEL libraries are updated to 1.9.0
3939
// See https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/RELEASENOTES.md#1100
4040
var serviceBuilder = builder.AddProject<Projects.Stress_ApiService>("stress-apiservice", launchProfileName: null)
41-
.WithEnvironment("OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE", "true");
41+
.WithEnvironment("OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE", "true")
42+
.WithIconName("Server");
4243
serviceBuilder
4344
.WithEnvironment("HOST", $"{serviceBuilder.GetEndpoint("http").Property(EndpointProperty.Host)}")
4445
.WithEnvironment("PORT", $"{serviceBuilder.GetEndpoint("http").Property(EndpointProperty.Port)}")
@@ -135,7 +136,8 @@
135136

136137
for (var i = 0; i < 3; i++)
137138
{
138-
var resourceBuilder = builder.AddProject<Projects.Stress_Empty>($"empty-{i:0000}");
139+
var resourceBuilder = builder.AddProject<Projects.Stress_Empty>($"empty-{i:0000}")
140+
.WithIconName("Document");
139141
if (previousResourceBuilder != null)
140142
{
141143
resourceBuilder.WaitFor(previousResourceBuilder);

src/Aspire.Dashboard/Model/ResourceIconHelpers.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,21 @@ namespace Aspire.Dashboard.Model;
1010
internal static class ResourceIconHelpers
1111
{
1212
/// <summary>
13-
/// Maps a resource to a default icon.
13+
/// Maps a resource to an icon, checking for custom icons first, then falling back to default icons.
1414
/// </summary>
1515
public static Icon GetIconForResource(ResourceViewModel resource, IconSize desiredSize, IconVariant desiredVariant = IconVariant.Filled)
1616
{
17+
// Check if the resource has a custom icon specified
18+
if (!string.IsNullOrWhiteSpace(resource.IconName))
19+
{
20+
var customIcon = IconResolver.ResolveIconName(resource.IconName, desiredSize, resource.IconVariant ?? IconVariant.Filled);
21+
if (customIcon != null)
22+
{
23+
return customIcon;
24+
}
25+
}
26+
27+
// Fall back to default icons based on resource type
1728
var icon = resource.ResourceType switch
1829
{
1930
KnownResourceTypes.Executable => IconResolver.ResolveIconName("Apps", desiredSize, desiredVariant),

src/Aspire.Dashboard/Model/ResourceViewModel.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ public sealed class ResourceViewModel
4343
public HealthStatus? HealthStatus { get; private set; }
4444
public bool IsHidden { private get; init; }
4545
public bool SupportsDetailedTelemetry { get; init; }
46+
public string? IconName { get; init; }
47+
public IconVariant? IconVariant { get; init; }
4648

4749
/// <summary>
4850
/// Gets the cached addresses for this resource that can be used for peer matching.

src/Aspire.Dashboard/ServiceClient/Partials.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ public ResourceViewModel ToViewModel(IKnownPropertyLookup knownPropertyLookup, I
4040
Commands = GetCommands(),
4141
HealthReports = HealthReports.Select(ToHealthReportViewModel).OrderBy(vm => vm.Name).ToImmutableArray(),
4242
IsHidden = IsHidden,
43-
SupportsDetailedTelemetry = SupportsDetailedTelemetry
43+
SupportsDetailedTelemetry = SupportsDetailedTelemetry,
44+
IconName = HasIconName ? IconName : null,
45+
IconVariant = HasIconVariant ? MapResourceIconVariant(IconVariant) : null
4446
};
4547
}
4648
catch (Exception ex)
@@ -131,6 +133,16 @@ static FluentUIIconVariant MapIconVariant(IconVariant iconVariant)
131133
};
132134
}
133135
}
136+
137+
static FluentUIIconVariant MapResourceIconVariant(IconVariant iconVariant)
138+
{
139+
return iconVariant switch
140+
{
141+
IconVariant.Regular => FluentUIIconVariant.Regular,
142+
IconVariant.Filled => FluentUIIconVariant.Filled,
143+
_ => throw new InvalidOperationException("Unknown icon variant: " + iconVariant),
144+
};
145+
}
134146
}
135147

136148
private ImmutableDictionary<string, ResourcePropertyViewModel> CreatePropertyViewModels(RepeatedField<ResourceProperty> properties, IKnownPropertyLookup knownPropertyLookup, ILogger logger)

src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,17 @@ internal init
133133
/// </summary>
134134
internal bool SupportsDetailedTelemetry { get; init; }
135135

136+
/// <summary>
137+
/// The custom icon name for the resource. This should be a valid FluentUI icon name.
138+
/// If not specified, the dashboard will use default icons based on the resource type.
139+
/// </summary>
140+
public string? IconName { get; init; }
141+
142+
/// <summary>
143+
/// The custom icon variant for the resource.
144+
/// </summary>
145+
public IconVariant? IconVariant { get; init; }
146+
136147
internal static HealthStatus? ComputeHealthStatus(ImmutableArray<HealthReportSnapshot> healthReports, string? state)
137148
{
138149
if (state != KnownResourceStates.Running)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
6+
namespace Aspire.Hosting.ApplicationModel;
7+
8+
/// <summary>
9+
/// Specifies the icon to use when displaying a resource in the dashboard.
10+
/// </summary>
11+
/// <param name="iconName">The name of the FluentUI icon to use.</param>
12+
/// <param name="iconVariant">The variant of the icon (Regular or Filled).</param>
13+
[DebuggerDisplay("Type = {GetType().Name,nq}, IconName = {IconName}, IconVariant = {IconVariant}")]
14+
public sealed class ResourceIconAnnotation(string iconName, IconVariant iconVariant = IconVariant.Filled) : IResourceAnnotation
15+
{
16+
/// <summary>
17+
/// Gets the name of the FluentUI icon to use for the resource.
18+
/// </summary>
19+
/// <remarks>
20+
/// The icon name should be a valid FluentUI icon name.
21+
/// See https://aka.ms/fluentui-system-icons for available icons.
22+
/// </remarks>
23+
public string IconName { get; } = iconName ?? throw new ArgumentNullException(nameof(iconName));
24+
25+
/// <summary>
26+
/// Gets the variant of the icon (Regular or Filled).
27+
/// </summary>
28+
public IconVariant IconVariant { get; } = iconVariant;
29+
}

src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,8 @@ public Task PublishUpdateAsync(IResource resource, string resourceId, Func<Custo
574574

575575
newState = UpdateCommands(resource, newState);
576576

577+
newState = UpdateIcons(resource, newState);
578+
577579
notificationState.LastSnapshot = newState;
578580

579581
OnResourceUpdated?.Invoke(new ResourceEvent(resource, resourceId, newState));
@@ -716,6 +718,36 @@ static ResourceCommandSnapshot CreateCommandFromAnnotation(ResourceCommandAnnota
716718
}
717719
}
718720

721+
/// <summary>
722+
/// Use icon annotations to update resource snapshot.
723+
/// </summary>
724+
private static CustomResourceSnapshot UpdateIcons(IResource resource, CustomResourceSnapshot previousState)
725+
{
726+
var iconAnnotation = resource.Annotations.OfType<ResourceIconAnnotation>().FirstOrDefault();
727+
728+
if (iconAnnotation == null)
729+
{
730+
// No icon annotation, keep existing icon information
731+
return previousState;
732+
}
733+
734+
// Only update icon information if not already set
735+
var newIconName = string.IsNullOrEmpty(previousState.IconName) ? iconAnnotation.IconName : previousState.IconName;
736+
var newIconVariant = previousState.IconVariant ?? iconAnnotation.IconVariant;
737+
738+
// Only create new snapshot if there are changes
739+
if (previousState.IconName == newIconName && previousState.IconVariant == newIconVariant)
740+
{
741+
return previousState;
742+
}
743+
744+
return previousState with
745+
{
746+
IconName = newIconName,
747+
IconVariant = newIconVariant
748+
};
749+
}
750+
719751
/// <summary>
720752
/// Updates the snapshot of the <see cref="CustomResourceSnapshot"/> for a resource.
721753
/// </summary>

src/Aspire.Hosting/Dashboard/DashboardServiceData.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ static GenericResourceSnapshot CreateResourceSnapshot(IResource resource, string
5757
HealthReports = snapshot.HealthReports,
5858
Commands = snapshot.Commands,
5959
IsHidden = snapshot.IsHidden,
60-
SupportsDetailedTelemetry = snapshot.SupportsDetailedTelemetry
60+
SupportsDetailedTelemetry = snapshot.SupportsDetailedTelemetry,
61+
IconName = snapshot.IconName,
62+
IconVariant = snapshot.IconVariant
6163
};
6264
}
6365

src/Aspire.Hosting/Dashboard/ResourceSnapshot.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ internal abstract class ResourceSnapshot
3030
public required ImmutableArray<ResourceCommandSnapshot> Commands { get; init; }
3131
public required bool IsHidden { get; init; }
3232
public required bool SupportsDetailedTelemetry { get; init; }
33+
public required string? IconName { get; init; }
34+
public required IconVariant? IconVariant { get; init; }
3335

3436
protected abstract IEnumerable<(string Key, Value Value, bool IsSensitive)> GetProperties();
3537

src/Aspire.Hosting/Dashboard/proto/Partials.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ public static Resource FromSnapshot(ResourceSnapshot snapshot)
2222
SupportsDetailedTelemetry = snapshot.SupportsDetailedTelemetry
2323
};
2424

25+
if (snapshot.IconName is not null)
26+
{
27+
resource.IconName = snapshot.IconName;
28+
}
29+
30+
if (snapshot.IconVariant is not null)
31+
{
32+
resource.IconVariant = MapIconVariant(snapshot.IconVariant);
33+
}
34+
2535
if (snapshot.CreationTimeStamp.HasValue)
2636
{
2737
resource.CreatedAt = Timestamp.FromDateTime(snapshot.CreationTimeStamp.Value.ToUniversalTime());

0 commit comments

Comments
 (0)