Jelajahi Sumber

Mermaid Generator amended to look better

James 1 bulan lalu
induk
melakukan
ec3bdb684e

+ 18 - 6
RackPeek.Domain/UseCases/Mermaid/MermaidDiagramExportUseCase.cs

@@ -1,11 +1,23 @@
-using RackPeek.Domain.Persistence;
+using System.Collections.Generic;
+using System.Threading.Tasks;
 using RackPeek.Domain.Resources;
+using RackPeek.Domain.Persistence;
+
+namespace RackPeek.Domain.UseCases.Mermaid
+{
+    public class MermaidDiagramExportUseCase : IUseCase
+    {
+        private readonly IResourceCollection _repository;
 
-namespace RackPeek.Domain.UseCases.Mermaid;
+        public MermaidDiagramExportUseCase(IResourceCollection repository)
+        {
+            _repository = repository;
+        }
 
-public class MermaidDiagramExportUseCase(IResourceCollection repository) : IUseCase {
-    public async Task<MermaidExportResult?> ExecuteAsync(MermaidExportOptions options) {
-        IReadOnlyList<Resource> resources = await repository.GetAllOfTypeAsync<Resource>();
-        return resources.ToMermaidDiagram(options);
+        public async Task<MermaidExportResult?> ExecuteAsync(MermaidExportOptions options)
+        {
+            IReadOnlyList<Resource> resources = await _repository.GetAllOfTypeAsync<Resource>();
+            return resources.ToMermaidDiagram(options);
+        }
     }
 }

+ 61 - 65
RackPeek.Domain/UseCases/Mermaid/MermaidDiagramGenerator.cs

@@ -1,90 +1,86 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
 using System.Text;
 using RackPeek.Domain.Resources;
 
-namespace RackPeek.Domain.UseCases.Mermaid;
-
-public static class MermaidDiagramGenerator
+namespace RackPeek.Domain.UseCases.Mermaid
 {
-    public static MermaidExportResult ToMermaidDiagram(
-        this IReadOnlyList<Resource> resources,
-        MermaidExportOptions? options = null)
+    public static class MermaidDiagramGenerator
     {
-        MermaidExportOptions resolvedOptions = options ?? new MermaidExportOptions();
+        public static MermaidExportResult ToMermaidDiagram(
+            this IReadOnlyList<Resource> resources,
+            MermaidExportOptions? options = null)
+        {
+            MermaidExportOptions resolvedOptions = options ?? new MermaidExportOptions();
+            var sb = new StringBuilder();
+            var warnings = new List<string>();
 
-        var sb = new StringBuilder();
-        var warnings = new List<string>();
+            sb.AppendLine(resolvedOptions.DiagramType);
 
-        sb.AppendLine(resolvedOptions.DiagramType);
+            // Group resources by Kind
+            IOrderedEnumerable<IGrouping<string, Resource>> grouped = resources
+                .Where(r => resolvedOptions.IncludeTags.Count == 0
+                            || (r.Tags != null && r.Tags.Any(t => resolvedOptions.IncludeTags.Contains(t, StringComparer.OrdinalIgnoreCase))))
+                .GroupBy(r => r.Kind)
+                .OrderBy(g => g.Key);
 
-        foreach (Resource r in resources.OrderBy(x => x.Name))
-        {
-            if (resolvedOptions.IncludeTags.Any())
+            foreach (IGrouping<string, Resource> group in grouped)
             {
-                var tags = r.Tags ?? Array.Empty<string>();
-
-                var match = resolvedOptions
-                    .IncludeTags
-                    .Any(t => tags.Contains(t, StringComparer.OrdinalIgnoreCase));
+                sb.AppendLine($"  subgraph {SanitizeId(group.Key)}");
+                foreach (Resource r in group.OrderBy(x => x.Name))
+                {
+                    var nodeId = SanitizeId(r.Name);
+                    var label = BuildNodeLabel(r, resolvedOptions);
+                    sb.AppendLine($"    {nodeId}[\"{label}\"]");
+                }
+                sb.AppendLine("  end");
+            }
 
-                if (!match)
-                    continue;
+            // Map RunsOn relationships if requested
+            if (resolvedOptions.IncludeEdges)
+            {
+                foreach (Resource r in resources)
+                {
+                    var nodeId = SanitizeId(r.Name);
+                    foreach (var depName in r.RunsOn ?? new List<string>())
+                    {
+                        var depId = SanitizeId(depName);
+                        sb.AppendLine($"  {nodeId} --> {depId}");
+                    }
+                }
             }
 
-            var nodeId = SanitizeId(r.Name);
-            var label = BuildNodeLabel(r, resolvedOptions);
+            if (sb.Length == 0)
+                warnings.Add("No Mermaid diagram entries generated.");
 
-            sb.AppendLine($"    {nodeId}[\"{label}\"]");
+            return new MermaidExportResult(sb.ToString().TrimEnd(), warnings);
         }
 
-        if (sb.Length == 0)
-            warnings.Add("No Mermaid diagram entries generated.");
-
-        return new MermaidExportResult(sb.ToString().TrimEnd(), warnings);
-    }
-
-    private static string BuildNodeLabel(Resource r, MermaidExportOptions options)
-    {
-        if (!options.IncludeLabels || r.Labels.Count == 0)
-            return r.Name;
-
-        IEnumerable<KeyValuePair<string, string>> filtered;
-
-        if (options.LabelWhitelist is null)
+        private static string BuildNodeLabel(Resource r, MermaidExportOptions options)
         {
-            filtered = r.Labels;
-        }
-        else
-        {
-            filtered = r.Labels.Where(kvp =>
-                options.LabelWhitelist.Contains(kvp.Key, StringComparer.OrdinalIgnoreCase));
-        }
+            if (!options.IncludeLabels || r.Labels.Count == 0)
+                return r.Name;
 
-        var labelParts = filtered
-            .Select(kvp => $"{kvp.Key}: {kvp.Value}")
-            .ToList();
-
-        if (labelParts.Count == 0)
-            return r.Name;
-
-        return $"{r.Name}\\n{string.Join("\\n", labelParts)}";
-    }
+            IEnumerable<KeyValuePair<string, string>> filtered = options.LabelWhitelist is null
+                ? r.Labels
+                : r.Labels.Where(kvp => options.LabelWhitelist.Contains(kvp.Key, StringComparer.OrdinalIgnoreCase));
 
-    private static string SanitizeId(string name)
-    {
-        var sb = new StringBuilder();
+            var labelParts = filtered.Select(kvp => $"{kvp.Key}: {kvp.Value}").ToList();
+            return labelParts.Count == 0 ? r.Name : $"{r.Name}\\n{string.Join("\\n", labelParts)}";
+        }
 
-        foreach (var ch in name.Trim().ToLowerInvariant())
+        private static string SanitizeId(string name)
         {
-            if (char.IsLetterOrDigit(ch) || ch == '_')
+            var sb = new StringBuilder();
+            foreach (var ch in name.Trim().ToLowerInvariant())
             {
-                sb.Append(ch);
-            }
-            else if (ch == '-' || ch == '.' || ch == ' ')
-            {
-                sb.Append('_');
+                if (char.IsLetterOrDigit(ch) || ch == '_')
+                    sb.Append(ch);
+                else if (ch == '-' || ch == '.' || ch == ' ')
+                    sb.Append('_');
             }
+            return sb.Length == 0 ? "node" : sb.ToString();
         }
-
-        return sb.Length == 0 ? "node" : sb.ToString();
     }
 }

+ 31 - 27
RackPeek.Domain/UseCases/Mermaid/MermaidExportOptions.cs

@@ -1,33 +1,37 @@
-namespace RackPeek.Domain.UseCases.Mermaid;
+using System.Collections.Generic;
 
-public sealed record MermaidExportOptions {
-    /// <summary>
-    /// Only include resources with these tags (optional)
-    /// </summary>
-    public IReadOnlyList<string> IncludeTags { get; init; } = [];
+namespace RackPeek.Domain.UseCases.Mermaid
+{
+    public sealed record MermaidExportOptions
+    {
+        /// <summary>
+        /// Only include resources with these tags (optional)
+        /// </summary>
+        public IReadOnlyList<string> IncludeTags { get; init; } = new List<string>();
 
-    /// <summary>
-    /// Diagram type: "flowchart", "sequence", "class", "er", etc.
-    /// Default: flowchart TD
-    /// </summary>
-    public string DiagramType { get; init; } = "flowchart TD";
+        /// <summary>
+        /// Diagram type: "flowchart", "sequence", "class", "er", etc.
+        /// Default: flowchart TD
+        /// </summary>
+        public string DiagramType { get; init; } = "flowchart TD";
 
-    /// <summary>
-    /// Whether to include resource labels as annotations
-    /// </summary>
-    public bool IncludeLabels { get; init; } = true;
+        /// <summary>
+        /// Whether to include resource labels as annotations
+        /// </summary>
+        public bool IncludeLabels { get; init; } = true;
 
-    /// <summary>
-    /// Whether to include relationships (edges)
-    /// </summary>
-    public bool IncludeEdges { get; init; } = true;
+        /// <summary>
+        /// Whether to include relationships (edges)
+        /// </summary>
+        public bool IncludeEdges { get; init; } = true;
 
-    /// <summary>
-    /// Optional label keys to include (null = include all)
-    /// </summary>
-    public IReadOnlyList<string>? LabelWhitelist { get; init; }
-}
+        /// <summary>
+        /// Optional label keys to include (null = include all)
+        /// </summary>
+        public IReadOnlyList<string>? LabelWhitelist { get; init; }
+    }
 
-public sealed record MermaidExportResult(
-    string DiagramText,
-    IReadOnlyList<string> Warnings);
+    public sealed record MermaidExportResult(
+        string DiagramText,
+        IReadOnlyList<string> Warnings);
+}