From 1673909da134614a1f1374602ed242ec26ece46c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= <1165805+sebastienros@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:16:30 -0700 Subject: [PATCH] Fix dashboard graph multi-path icons Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ResourceGraph/ResourceGraphMapper.cs | 19 +++++++++++--- .../Model/ResourceGraphMapperTests.cs | 26 +++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/Aspire.Dashboard/Model/ResourceGraph/ResourceGraphMapper.cs b/src/Aspire.Dashboard/Model/ResourceGraph/ResourceGraphMapper.cs index 501afdf034d..ccc40dfce33 100644 --- a/src/Aspire.Dashboard/Model/ResourceGraph/ResourceGraphMapper.cs +++ b/src/Aspire.Dashboard/Model/ResourceGraph/ResourceGraphMapper.cs @@ -86,8 +86,21 @@ private static string ResolvedEndpointText(DisplayedUrl? endpoint) public static string GetIconPathData(Icon icon) { - var p = icon.Content; - var e = XElement.Parse(p); - return e.Attribute("d")!.Value; + // Fluent UI icon content is an SVG fragment. Most icons contain one path: + // + // Some icons, such as DocumentMultiple, contain sibling paths: + // + // Wrap the fragment so XML parsing accepts both shapes, then combine the path data into one compound SVG path. + var iconContent = XElement.Parse($"{icon.Content}"); + var pathData = iconContent.Elements() + .Select(e => e.Attribute("d")?.Value ?? throw new InvalidOperationException($"Icon '{icon.Name}' contains an element without path data.")) + .ToArray(); + + if (pathData.Length == 0) + { + throw new InvalidOperationException($"Icon '{icon.Name}' doesn't contain path data."); + } + + return string.Join(' ', pathData); } } diff --git a/tests/Aspire.Dashboard.Tests/Model/ResourceGraphMapperTests.cs b/tests/Aspire.Dashboard.Tests/Model/ResourceGraphMapperTests.cs index ab705fabbce..b89dff237ee 100644 --- a/tests/Aspire.Dashboard.Tests/Model/ResourceGraphMapperTests.cs +++ b/tests/Aspire.Dashboard.Tests/Model/ResourceGraphMapperTests.cs @@ -2,12 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; +using System.Xml.Linq; using Aspire.Dashboard.Model; using Aspire.Dashboard.Model.ResourceGraph; using Aspire.Dashboard.Resources; using Aspire.Tests.Shared.DashboardModel; using Microsoft.Extensions.Logging.Abstractions; using Xunit; +using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons; namespace Aspire.Dashboard.Tests.Model; @@ -156,4 +158,28 @@ public void MapResource_ReferenceToResourceExcludedFromGraph_Ignored() Assert.Empty(dto.ReferencedNames); } + + [Fact] + public void GetIconPathData_SinglePath_ReturnsPathData() + { + var icon = new Icons.Filled.Size24.Box(); + var expectedPathData = XElement.Parse(icon.Content).Attribute("d")!.Value; + + var pathData = ResourceGraphMapper.GetIconPathData(icon); + + Assert.Equal(expectedPathData, pathData); + } + + [Fact] + public void GetIconPathData_MultiplePaths_ReturnsCombinedPathData() + { + var icon = new Icons.Filled.Size24.DocumentMultiple(); + var iconContent = XElement.Parse($"{icon.Content}"); + var expectedPaths = iconContent.Elements().Select(e => e.Attribute("d")!.Value).ToArray(); + Assert.Equal(3, expectedPaths.Length); + + var pathData = ResourceGraphMapper.GetIconPathData(icon); + + Assert.Equal(string.Join(' ', expectedPaths), pathData); + } }