From 49d19a05ed89e4cae796c1981b4fd464b905704e Mon Sep 17 00:00:00 2001 From: Slava Vedernikov Date: Sun, 5 May 2024 10:33:24 +0100 Subject: [PATCH] Added support for publishing a simple ReactJS Diagrams Visualiser site --- .../C4InterFlow.Automation.csproj | 4 +- C4InterFlow.Cli/C4InterFlow.Cli.csproj | 2 +- .../Properties/launchSettings.json | 2 +- C4InterFlow/C4InterFlow.csproj | 4 +- .../Options/OutputSubDirectoryOption.cs | 2 +- .../SiteContentSubDirectoriesOption.cs | 20 ++++++ .../SiteNoSitemapSubDirectoriesOption.cs | 20 ++++++ .../Cli/Commands/PublishSiteCommand.cs | 71 +++++++++++++------ C4InterFlow/Visualisation/ComponentDiagram.cs | 2 +- C4InterFlow/Visualisation/ContainerDiagram.cs | 15 ++-- C4InterFlow/Visualisation/ContextDiagram.cs | 4 +- .../Plantuml/PlantumlSequenceFlow.cs | 2 +- Publishers/StaticSite/build.bat | 1 - Publishers/StaticSite/src/App.css | 14 +++- Publishers/StaticSite/src/App.js | 16 +++-- 15 files changed, 132 insertions(+), 47 deletions(-) create mode 100644 C4InterFlow/Cli/Commands/Options/SiteContentSubDirectoriesOption.cs create mode 100644 C4InterFlow/Cli/Commands/Options/SiteNoSitemapSubDirectoriesOption.cs diff --git a/C4InterFlow.Automation/C4InterFlow.Automation.csproj b/C4InterFlow.Automation/C4InterFlow.Automation.csproj index 5eaf4b31b..dfa87cb7d 100644 --- a/C4InterFlow.Automation/C4InterFlow.Automation.csproj +++ b/C4InterFlow.Automation/C4InterFlow.Automation.csproj @@ -7,12 +7,12 @@ true true snupkg - 1.2.0 + 1.3.0 C4InterFlow.Automation - 1.2.0 + 1.3.0 Slava Vedernikov Revolutionise your Application Architecture Documentation with C4InterFlow. Designed for Architects and Engineers, this tool leverages the widely-recognised C4 Model (Architecture Visualisation framework), enhanced with unique features like Interface and Flow, to describe your Application Architecture as Code. Experience an intuitive, efficient way to document complex systems, ensuring clarity and consistency across your teams and products. Copyright 2024 Slava Vedernikov diff --git a/C4InterFlow.Cli/C4InterFlow.Cli.csproj b/C4InterFlow.Cli/C4InterFlow.Cli.csproj index b7a74afa9..7caab089d 100644 --- a/C4InterFlow.Cli/C4InterFlow.Cli.csproj +++ b/C4InterFlow.Cli/C4InterFlow.Cli.csproj @@ -5,7 +5,7 @@ net6.0 enable enable - 1.2.0 + 1.3.0 true true win-x64 diff --git a/C4InterFlow.Cli/Properties/launchSettings.json b/C4InterFlow.Cli/Properties/launchSettings.json index 80efd64f8..7738a3300 100644 --- a/C4InterFlow.Cli/Properties/launchSettings.json +++ b/C4InterFlow.Cli/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "C4InterFlow.Cli": { "commandName": "Project", - "commandLineArgs": "publish-site --site-source-dir \"C:\\C4InterFlow\\Publishers\\StaticSite\" --output-dir \"C:\\architecture-as-code-samples-visualiser\\trader-x\" --environment-variables \"HOMEPAGE=architecture-as-code-samples-visualiser/trader-x\"" + "commandLineArgs": "publish-site --site-source-dir \"C:\\C4InterFlow\\Publishers\\StaticSite\" --output-dir \"C:\\architecture-as-code-samples-visualiser\\e-commerce-platform\" --environment-variables \"HOMEPAGE=architecture-as-code-samples-visualiser/e-commerce-platform\" --site-content-sub-dirs \"Software Systems\" \".c4s\" --site-no-sitemap-sub-dirs \".c4s\"" } } } \ No newline at end of file diff --git a/C4InterFlow/C4InterFlow.csproj b/C4InterFlow/C4InterFlow.csproj index 79e0bdae1..167a395db 100644 --- a/C4InterFlow/C4InterFlow.csproj +++ b/C4InterFlow/C4InterFlow.csproj @@ -7,12 +7,12 @@ true true snupkg - 1.2.0 + 1.3.0 C4InterFlow - 1.2.0 + 1.3.0 Slava Vedernikov Revolutionise your Application Architecture Documentation with C4InterFlow. Designed for Architects and Engineers, this tool leverages the widely-recognised C4 Model (Architecture Visualisation framework), enhanced with unique features like Interface and Flow, to describe your Application Architecture as Code. Experience an intuitive, efficient way to document complex systems, ensuring clarity and consistency across your teams and products. Copyright 2024 Slava Vedernikov diff --git a/C4InterFlow/Cli/Commands/Options/OutputSubDirectoryOption.cs b/C4InterFlow/Cli/Commands/Options/OutputSubDirectoryOption.cs index 516c42c41..a1001ddbd 100644 --- a/C4InterFlow/Cli/Commands/Options/OutputSubDirectoryOption.cs +++ b/C4InterFlow/Cli/Commands/Options/OutputSubDirectoryOption.cs @@ -8,7 +8,7 @@ public static class OutputSubDirectoryOption public static Option Get() { const string description = - "The sub-directory where the Diagram(s) should be saved."; + "The output sub-directory for the current command"; var option = new Option(new[] { "--output-sub-dir", "-osd" }, description); diff --git a/C4InterFlow/Cli/Commands/Options/SiteContentSubDirectoriesOption.cs b/C4InterFlow/Cli/Commands/Options/SiteContentSubDirectoriesOption.cs new file mode 100644 index 000000000..390071a39 --- /dev/null +++ b/C4InterFlow/Cli/Commands/Options/SiteContentSubDirectoriesOption.cs @@ -0,0 +1,20 @@ +using System.CommandLine; + +namespace C4InterFlow.Cli.Commands.Options; + +public static class SiteContentSubDirectoriesOption +{ + public static Option Get() + { + const string description = + $"The sub-directories where site content can be found."; + + var option = new Option(new[] { "--site-content-sub-dirs", "-scsds" }, description) + { + AllowMultipleArgumentsPerToken = true + }; + option.IsRequired = true; + + return option; + } +} diff --git a/C4InterFlow/Cli/Commands/Options/SiteNoSitemapSubDirectoriesOption.cs b/C4InterFlow/Cli/Commands/Options/SiteNoSitemapSubDirectoriesOption.cs new file mode 100644 index 000000000..3389ccfee --- /dev/null +++ b/C4InterFlow/Cli/Commands/Options/SiteNoSitemapSubDirectoriesOption.cs @@ -0,0 +1,20 @@ +using System.CommandLine; + +namespace C4InterFlow.Cli.Commands.Options; + +public static class SiteNoSitemapSubDirectoriesOption +{ + public static Option Get() + { + const string description = + $"The sub-directories that shoud be excluded from a sitemap."; + + var option = new Option(new[] { "--site-no-sitemap-sub-dirs", "-snssds" }, description) + { + AllowMultipleArgumentsPerToken = true + }; + option.SetDefaultValue(null); + + return option; + } +} diff --git a/C4InterFlow/Cli/Commands/PublishSiteCommand.cs b/C4InterFlow/Cli/Commands/PublishSiteCommand.cs index 5d3129614..96bf91fd7 100644 --- a/C4InterFlow/Cli/Commands/PublishSiteCommand.cs +++ b/C4InterFlow/Cli/Commands/PublishSiteCommand.cs @@ -17,33 +17,41 @@ public PublishSiteCommand() : base(COMMAND_NAME, var siteBuildDirectoryOption = SiteBuildDirectoryOption.Get(); var diagramFormatsOption = DiagramFormatsOption.Get(); var environmentVariablesOption = EnvironmentVariablesOption.Get(); + var siteContentSubDirectoriesOption = SiteContentSubDirectoriesOption.Get(); + var siteNoSitemapSubDirectoriesOption = SiteNoSitemapSubDirectoriesOption.Get(); AddOption(siteSourceDirectoryOption); AddOption(outputDirectoryOption); AddOption(batchFileOption); AddOption(siteBuildDirectoryOption); + AddOption(diagramFormatsOption); AddOption(environmentVariablesOption); + AddOption(siteContentSubDirectoriesOption); + AddOption(siteNoSitemapSubDirectoriesOption); - this.SetHandler(async (siteSourceDirectory, outputDirectory, batchFile, siteBuildDirectory, diagramFormats, environmentVariables) => + this.SetHandler(async (siteSourceDirectory, outputDirectory, siteContentSubDirectories, batchFile, siteBuildDirectory, diagramFormats, environmentVariables, siteNoSitemapSubDirectories) => { - await Execute(siteSourceDirectory, outputDirectory, batchFile, siteBuildDirectory, diagramFormats, environmentVariables); + await Execute(siteSourceDirectory, outputDirectory, siteContentSubDirectories, batchFile, siteBuildDirectory, diagramFormats, environmentVariables, siteNoSitemapSubDirectories); }, - siteSourceDirectoryOption, outputDirectoryOption, batchFileOption, siteBuildDirectoryOption, diagramFormatsOption, environmentVariablesOption); + siteSourceDirectoryOption, outputDirectoryOption, siteContentSubDirectoriesOption, batchFileOption, siteBuildDirectoryOption, diagramFormatsOption, environmentVariablesOption, siteNoSitemapSubDirectoriesOption); } - private static async Task Execute(string siteSourceDirectory, string outputDirectory, string? batchFile = null, string? siteBuildDirectory = null, string[]? diagramFormats = null, string[]? environmentVariables = null) + private static async Task Execute(string siteSourceDirectory, string outputDirectory, string[] siteContentSubDirectories, string ? batchFile = null, string? siteBuildDirectory = null, string[]? diagramFormats = null, string[]? environmentVariables = null, string[]? siteNoSitemapSubDirectories = null) { diagramFormats = diagramFormats?.Length > 0 ? diagramFormats : DiagramFormatsOption.GetAllDiagramFormats(); batchFile = batchFile ?? "build.bat"; siteBuildDirectory = siteBuildDirectory ?? "build"; + try { Console.WriteLine($"'{COMMAND_NAME}' command is executing..."); + ClearDirectory(outputDirectory, siteContentSubDirectories.Select(x => Path.Join(outputDirectory, x)).ToArray()); + var sitemap = new { - urlset = BuildDirectoryMap(outputDirectory, outputDirectory, diagramFormats.Select(x => $".{x}").ToArray()) + urlset = BuildDirectoryMap(outputDirectory, outputDirectory, diagramFormats.Select(x => $".{x}").ToArray(), siteNoSitemapSubDirectories.Select(x => Path.Join(outputDirectory, x)).ToArray()) }; string json = JsonConvert.SerializeObject(sitemap, Formatting.Indented); @@ -52,7 +60,7 @@ private static async Task Execute(string siteSourceDirectory, string output RunBatchFile(Path.Join(siteSourceDirectory, batchFile.Replace(siteSourceDirectory, "").TrimStart('\\')), environmentVariables); - CopyFilesRecursively(Path.Join(siteSourceDirectory, siteBuildDirectory.Replace(siteSourceDirectory, "").TrimStart('\\')), outputDirectory); + CopyFiles(Path.Join(siteSourceDirectory, siteBuildDirectory.Replace(siteSourceDirectory, "").TrimStart('\\')), outputDirectory); Console.WriteLine($"'{COMMAND_NAME}' command completed."); return 0; @@ -64,24 +72,27 @@ private static async Task Execute(string siteSourceDirectory, string output } } - private static List BuildDirectoryMap(string directory, string rootPath, string[] fileExtensions) + private static List BuildDirectoryMap(string directory, string rootPath, string[] fileExtensions, string[]? excludedPaths = null) { List map = new List(); foreach (string subDirectory in Directory.GetDirectories(directory)) { - string label = Path.GetFileName(subDirectory); - var directoryItem = new - { - label = label, - loc = subDirectory.Replace(rootPath, "").TrimStart('\\').Replace("\\", "/"), - urlset = BuildDirectoryMap(subDirectory, rootPath, fileExtensions), - types = new HashSet(), - levelsOfDetails = new HashSet(), - formats = new HashSet() - }; + if (excludedPaths?.Contains(subDirectory) == true) + continue; - AddFileDetails(subDirectory, fileExtensions, directoryItem.types, directoryItem.levelsOfDetails, directoryItem.formats); - map.Add(directoryItem); + string label = Path.GetFileName(subDirectory); + var directoryItem = new + { + label = label, + loc = subDirectory.Replace(rootPath, "").TrimStart('\\').Replace("\\", "/"), + urlset = BuildDirectoryMap(subDirectory, rootPath, fileExtensions, excludedPaths), + types = new HashSet(), + levelsOfDetails = new HashSet(), + formats = new HashSet() + }; + + AddFileDetails(subDirectory, fileExtensions, directoryItem.types, directoryItem.levelsOfDetails, directoryItem.formats); + map.Add(directoryItem); } return map; } @@ -144,7 +155,25 @@ private static void RunBatchFile(string batchFile, string[]? environmentVariable Directory.SetCurrentDirectory(currentDirectory); } - static void CopyFilesRecursively(string sourcePath, string targetPath) + static public void ClearDirectory(string path, string[] excludedPaths) + { + foreach (var subDirectory in Directory.GetDirectories(path)) + { + if (excludedPaths.Contains(subDirectory)) + continue; + + ClearDirectory(subDirectory, excludedPaths); + Directory.Delete(subDirectory, true); + } + + var files = Directory.GetFiles(path); + foreach (var file in files) + { + File.Delete(file); + } + } + + static void CopyFiles(string sourcePath, string targetPath) { // Check if the target directory exists, if not, create it. if (!Directory.Exists(targetPath)) @@ -168,7 +197,7 @@ static void CopyFilesRecursively(string sourcePath, string targetPath) { Directory.CreateDirectory(Path.Combine(targetPath, dirName)); } - CopyFilesRecursively(directoryPath, Path.Combine(targetPath, dirName)); + CopyFiles(directoryPath, Path.Combine(targetPath, dirName)); } } diff --git a/C4InterFlow/Visualisation/ComponentDiagram.cs b/C4InterFlow/Visualisation/ComponentDiagram.cs index a24df9433..c00133f98 100644 --- a/C4InterFlow/Visualisation/ComponentDiagram.cs +++ b/C4InterFlow/Visualisation/ComponentDiagram.cs @@ -138,7 +138,7 @@ protected override IEnumerable Structures foreach (var activity in Process.Activities) { var actor = activity.GetActorInstance(); - if (actor != null && !_structures.Any(i => i.Alias != actor.Alias)) + if (actor != null && !_structures.Any(i => i.Alias == actor.Alias)) { _structures.Add(actor); } diff --git a/C4InterFlow/Visualisation/ContainerDiagram.cs b/C4InterFlow/Visualisation/ContainerDiagram.cs index cc3d50cb1..9fd0a247b 100644 --- a/C4InterFlow/Visualisation/ContainerDiagram.cs +++ b/C4InterFlow/Visualisation/ContainerDiagram.cs @@ -96,7 +96,7 @@ protected override IEnumerable Structures foreach (var activity in Process.Activities) { var actor = activity.GetActorInstance(); - if (actor != null && !_structures.Any(i => i.Alias != actor.Alias)) + if (actor != null && !_structures.Any(i => i.Alias == actor.Alias)) { if(actor is Interface @interface) { @@ -247,11 +247,8 @@ private void PopulateRelationships(IList relationships, Structure } } - if (usesInterfaceOwner is Container) - { - newToScope = usesInterfaceOwner.Alias; - } - else if (usesInterfaceOwner is Component) + + if (usesInterfaceOwner is Component) { var usesContainer = Utils.GetInstance(((Component)usesInterfaceOwner).Container); if (usesContainer != null) @@ -260,13 +257,17 @@ private void PopulateRelationships(IList relationships, Structure usesInterfaceOwner = usesContainer; } } + else if (usesInterfaceOwner is Container || usesInterfaceOwner is SoftwareSystem) + { + newToScope = usesInterfaceOwner.Alias; + } var label = $"{usesInterface.Label}"; if (relationships.Where(x => x.From == (actor).Alias && x.To == usesInterfaceOwner.Alias && x.Label == label).FirstOrDefault() == null && - (!(fromScope ?? string.Empty).Equals(newToScope))) + (!(newFromScope ?? string.Empty).Equals(newToScope))) { diff --git a/C4InterFlow/Visualisation/ContextDiagram.cs b/C4InterFlow/Visualisation/ContextDiagram.cs index bada49afd..e1bac3ec9 100644 --- a/C4InterFlow/Visualisation/ContextDiagram.cs +++ b/C4InterFlow/Visualisation/ContextDiagram.cs @@ -94,7 +94,7 @@ protected override IEnumerable Structures foreach (var activity in Process.Activities) { var actor = activity.GetActorInstance(); - if (actor != null && !_structures.Any(i => i.Alias != actor.Alias)) + if (actor != null && !_structures.Any(i => i.Alias == actor.Alias)) { _structures.Add(actor); } @@ -246,7 +246,7 @@ private void PopulateRelationships(IList relationships, Structure x.To == usesInterfaceOwner.Alias && x.Label == label && x.Protocol == protocol).FirstOrDefault() == null && - (!(fromScope ?? string.Empty).Equals(newToScope))) + (!(newFromScope ?? string.Empty).Equals(newToScope))) { relationships.Add(((actor) > usesInterfaceOwner)[ label, diff --git a/C4InterFlow/Visualisation/Plantuml/PlantumlSequenceFlow.cs b/C4InterFlow/Visualisation/Plantuml/PlantumlSequenceFlow.cs index 574a67763..4facd45a6 100644 --- a/C4InterFlow/Visualisation/Plantuml/PlantumlSequenceFlow.cs +++ b/C4InterFlow/Visualisation/Plantuml/PlantumlSequenceFlow.cs @@ -104,7 +104,7 @@ public static string ToPumlSequenceString(this Flow flow, SequenceDiagramStyle s // TODO: Investigate why doing this in C4 sequence diagrams doesn't seem to work (out of memory java error when rendering) if (style == SequenceDiagramStyle.PlantUML) { - innerFlows.AddRange(flow.GetFlowsByType(flow, Flow.FlowType.Use, false)); + innerFlows.AddRange(flow.GetFlowsByType(flow, Flow.FlowType.Use, true)); innerFlows.AddRange(flow.GetFlowsByType(flow, Flow.FlowType.Return, false)); innerFlows.AddRange(flow.GetFlowsByType(flow, Flow.FlowType.ThrowException, false)); if (innerFlows.Any()) diff --git a/Publishers/StaticSite/build.bat b/Publishers/StaticSite/build.bat index 976708192..5a86ea29f 100644 --- a/Publishers/StaticSite/build.bat +++ b/Publishers/StaticSite/build.bat @@ -1,4 +1,3 @@ @echo off call npm install call npm run build -pause diff --git a/Publishers/StaticSite/src/App.css b/Publishers/StaticSite/src/App.css index 293b1a733..7abf39420 100644 --- a/Publishers/StaticSite/src/App.css +++ b/Publishers/StaticSite/src/App.css @@ -1,4 +1,4 @@ -/* Spli- */ +/* Split- */ .split-pane { display: flex; height: 100vh; @@ -17,12 +17,20 @@ background-color: #ccc; background-repeat: no-repeat; background-position: 50%; + transition: width 0.1s ease; } + +.gutter:hover { + background-color: #aaa; + width: 5px !important; /* Increase width on hover and ensure it's applied */ +} + .gutter.gutter-horizontal { - background-image: url('data:image/png;base64,...'); /* add a vertical line image or gradient for the splitter */ - cursor: col-resize; + background-image: url('data:image/png;base64,...'); + cursor: col-resize; } + /* Tree View */ .tree-view, .tree-view ul { diff --git a/Publishers/StaticSite/src/App.js b/Publishers/StaticSite/src/App.js index 54ad888a7..e16005dad 100644 --- a/Publishers/StaticSite/src/App.js +++ b/Publishers/StaticSite/src/App.js @@ -46,7 +46,9 @@ const TreeView = ({ sitemap, onNodeSelect, levelOfDetail, setLevelOfDetail, type // Controls Component const Controls = ({ selectedNode, levelOfDetail, setLevelOfDetail, type, setType, format, setFormat }) => { - if (!selectedNode || !selectedNode.levelsOfDetails || !selectedNode.types || !selectedNode.formats) return null; + if (!selectedNode || + !selectedNode.levelsOfDetails || !selectedNode.types || !selectedNode.formats || + selectedNode.levelsOfDetails.length == 0 || selectedNode.types.length == 0 || selectedNode.formats.length == 0) return null; return (
@@ -96,7 +98,14 @@ const DiagramView = ({ selectedNode, levelOfDetail, type, format }) => { } }, [selectedNode, levelOfDetail, type, format]); - if (!selectedNode || !selectedNode.levelsOfDetails || !selectedNode.types || !selectedNode.formats) return null; + if (!selectedNode || + !selectedNode.levelsOfDetails || !selectedNode.types || !selectedNode.formats || + selectedNode.levelsOfDetails.length == 0 || selectedNode.types.length == 0 || selectedNode.formats.length == 0) + return ( +
+

No diagrams found for selected node

+
+ ); const diagramUrl = `${selectedNode.loc}/${levelOfDetail} - ${type}.${format}`; @@ -110,7 +119,6 @@ const DiagramView = ({ selectedNode, levelOfDetail, type, format }) => { return (
-

Diagram

{renderDiagram()}
); @@ -143,7 +151,7 @@ const App = () => { }, []); return ( - +