From dc6c7f4cf402a4609f4580ca3f833b93552c76c1 Mon Sep 17 00:00:00 2001 From: Jens Peters Date: Fri, 27 Oct 2023 20:38:35 +0200 Subject: [PATCH 1/9] Do not bail out when structurizr.groupSeparator is not defined Structurizr only enforces this property with nested groups, but not with groups on the first level only. --- .../structurizr/site/generatr/site/model/MenuViewModel.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModel.kt index e131250b..5985e81c 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModel.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModel.kt @@ -25,7 +25,7 @@ class MenuViewModel(generatorContext: GeneratorContext, private val pageViewMode createMenuItem(it.name, SoftwareSystemPageViewModel.url(it, SoftwareSystemPageViewModel.Tab.HOME), false) } - private val groupSeparator = generatorContext.workspace.model.properties["structurizr.groupSeparator"] + private val groupSeparator = generatorContext.workspace.model.properties["structurizr.groupSeparator"] ?: "/" private val softwareSystemPaths = generatorContext.workspace.model.includedSoftwareSystems .filter { it.group != null } @@ -39,8 +39,6 @@ class MenuViewModel(generatorContext: GeneratorContext, private val pageViewMode data class MutableMenuNode(val name: String, val children: MutableList) { fun toMenuNode(): MenuNodeViewModel = MenuNodeViewModel(name, children.map { it.toMenuNode() }) } - if (groupSeparator == null) - throw IllegalStateException("Property structurizr.groupSeparator not defined for model") // This is also validated earlier by structurizr when parsing the model val rootNode = MutableMenuNode("", mutableListOf()) From 054ec2de3bed3786a7fbc377985ea692010d6e19 Mon Sep 17 00:00:00 2001 From: Jens Peters Date: Fri, 27 Oct 2023 21:08:18 +0200 Subject: [PATCH 2/9] Do not filter for software systems in the menu view model We have `SoftwareSystem.includedSoftwareSystem` for that. --- .../structurizr/site/generatr/site/model/MenuViewModel.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModel.kt index 5985e81c..54a8155e 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModel.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModel.kt @@ -28,8 +28,7 @@ class MenuViewModel(generatorContext: GeneratorContext, private val pageViewMode private val groupSeparator = generatorContext.workspace.model.properties["structurizr.groupSeparator"] ?: "/" private val softwareSystemPaths = generatorContext.workspace.model.includedSoftwareSystems - .filter { it.group != null } - .map { it.group + groupSeparator + it.name } + .map { "${it.group ?: ""}$groupSeparator${it.name}" } .sortedBy { it.lowercase() } private fun createMenuItem(title: String, href: String, exact: Boolean = true) = From 03160d79157203ff43d5a698fac53339d6c13323 Mon Sep 17 00:00:00 2001 From: Jens Peters Date: Sun, 29 Oct 2023 07:42:49 +0100 Subject: [PATCH 3/9] Add test for validating a single group usage In other words, validate groups only on the first level, without setting 'structurizr.groupSeparator' property. Also split other test and fix some formatting issues. --- .../generatr/site/model/MenuViewModelTest.kt | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt index cafa89b1..a18aa90e 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt @@ -22,7 +22,7 @@ class MenuViewModelTest : ViewModelTest() { val viewModel = MenuViewModel(generatorContext, pageViewModel) assertThat(viewModel.generalItems).containsExactly( - LinkViewModel(pageViewModel, "Home", HomePageViewModel.url()), + LinkViewModel(pageViewModel, "Home", HomePageViewModel.url()) ) } @@ -83,9 +83,8 @@ class MenuViewModelTest : ViewModelTest() { @ValueSource(strings = ["main", "branch-2"]) fun `links to software system pages sorted alphabetically case insensitive`(currentBranch: String) { val generatorContext = generatorContext(branches = listOf("main", "branch-2"), currentBranch = currentBranch) - val system2 = generatorContext.workspace.model.addSoftwareSystem(Location.Internal, "System 2", "") - val system1 = generatorContext.workspace.model.addSoftwareSystem(Location.Internal, "system 1", "") - generatorContext.workspace.model.addSoftwareSystem(Location.External, "External", "") + val system2 = generatorContext.workspace.model.addSoftwareSystem("System 2", "") + val system1 = generatorContext.workspace.model.addSoftwareSystem("system 1", "") val pageViewModel = createPageViewModel(generatorContext) val viewModel = MenuViewModel(generatorContext, pageViewModel) @@ -101,7 +100,7 @@ class MenuViewModelTest : ViewModelTest() { "System 2", SoftwareSystemPageViewModel.url(system2, SoftwareSystemPageViewModel.Tab.HOME), false - ), + ) ) } @@ -109,7 +108,7 @@ class MenuViewModelTest : ViewModelTest() { @ValueSource(strings = ["main", "branch-2"]) fun `active links`(currentBranch: String) { val generatorContext = generatorContext(branches = listOf("main", "branch-2"), currentBranch = currentBranch) - val system = generatorContext.workspace.model.addSoftwareSystem(Location.Internal, "System 1", "") + val system = generatorContext.workspace.model.addSoftwareSystem("System 1", "") MenuViewModel(generatorContext, createPageViewModel(generatorContext, url = HomePageViewModel.url())) .let { assertThat(it.generalItems[0].active).isTrue() } @@ -136,6 +135,32 @@ class MenuViewModelTest : ViewModelTest() { } } + @Test + fun `do not show menu entries for software systems with an external location`() { + val generatorContext = generatorContext(branches = listOf("main", "branch-2"), currentBranch = "main") + generatorContext.workspace.model.addSoftwareSystem(Location.External, "System 1", "") + + MenuViewModel(generatorContext, createPageViewModel(generatorContext, url = HomePageViewModel.url())) + .let { + assertThat(it.softwareSystemItems).hasSize(0) + } + } + + @Test + fun `show software systems from single group in software systems list`() { + val generatorContext = generatorContext(branches = listOf("main", "branch-2"), currentBranch = "main") + generatorContext.workspace.views.configuration.addProperty("generatr.site.nestGroups", "true") + generatorContext.workspace.model.addSoftwareSystem("System 1").group = "Group 1" + + MenuViewModel(generatorContext, createPageViewModel(generatorContext, url = HomePageViewModel.url())) + .let { + assertThat(it.softwareSystemNodes().children).hasSize(1) + assertThat(it.softwareSystemNodes().children[0].name).isEqualTo("Group 1") + assertThat(it.softwareSystemNodes().children[0].children).hasSize(1) + assertThat(it.softwareSystemNodes().children[0].children[0].name).isEqualTo("System 1") + } + } + @Test fun `show nested groups in software systems list`() { val generatorContext = generatorContext(branches = listOf("main", "branch-2"), currentBranch = "main") From 7329c3ef4edceeb23f9ea3e99722648a888e6d52 Mon Sep 17 00:00:00 2001 From: Jens Peters Date: Sun, 29 Oct 2023 07:48:52 +0100 Subject: [PATCH 4/9] Detect included software systems consistently Only use 'Model.includedSoftwareSystems' to determine if we include a software system. A side effect is unfortunately that we extended our PageViewModel. --- .../generatr/site/C4PlantUmlExporterWithElementLinks.kt | 7 +++---- .../structurizr/site/generatr/site/model/MenuViewModel.kt | 5 ++--- .../structurizr/site/generatr/site/model/PageViewModel.kt | 2 ++ .../site/generatr/site/model/SearchViewModel.kt | 4 +--- .../generatr/site/model/SoftwareSystemTableUtilities.kt | 6 +++--- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/C4PlantUmlExporterWithElementLinks.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/C4PlantUmlExporterWithElementLinks.kt index 63629d3f..696af505 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/C4PlantUmlExporterWithElementLinks.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/C4PlantUmlExporterWithElementLinks.kt @@ -11,7 +11,7 @@ import nl.avisi.structurizr.site.generatr.* class C4PlantUmlExporterWithElementLinks( private val url: String -): C4PlantUMLExporter() { +) : C4PlantUMLExporter() { companion object { const val TEMP_URI = "https://will-be-changed-to-relative/" @@ -48,7 +48,7 @@ class C4PlantUmlExporterWithElementLinks( } private fun needsLinkToSoftwareSystem(element: Element?, view: ModelView?) = - element is SoftwareSystem && element.includedSoftwareSystem && element != view?.softwareSystem + element is SoftwareSystem && view != null && view.model.includedSoftwareSystems.contains(element) && element != view.softwareSystem private fun getUrlToSoftwareSystem(element: Element?): String { val path = "/${element?.name?.normalize()}/context/".asUrlToDirectory(url) @@ -56,7 +56,7 @@ class C4PlantUmlExporterWithElementLinks( } private fun needsLinkToContainerViews(element: Element?, view: ModelView?) = - element is SoftwareSystem && element.includedSoftwareSystem && element == view?.softwareSystem && element.hasContainers + element is SoftwareSystem && view != null && view.model.includedSoftwareSystems.contains(element) && element == view.softwareSystem && element.hasContainers private fun getUrlToContainerViews(element: Element?): String { val path = "/${element?.name?.normalize()}/container/".asUrlToDirectory(url) @@ -88,5 +88,4 @@ class C4PlantUmlExporterWithElementLinks( .split(System.lineSeparator()) .forEach { line -> writer?.writeLine(line) } } - } diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModel.kt index 54a8155e..0e7f0d2d 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModel.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModel.kt @@ -1,6 +1,5 @@ package nl.avisi.structurizr.site.generatr.site.model -import nl.avisi.structurizr.site.generatr.includedSoftwareSystems import nl.avisi.structurizr.site.generatr.site.GeneratorContext class MenuViewModel(generatorContext: GeneratorContext, private val pageViewModel: PageViewModel) { @@ -19,7 +18,7 @@ class MenuViewModel(generatorContext: GeneratorContext, private val pageViewMode .forEach { yield(createMenuItem(it.contentTitle(), WorkspaceDocumentationSectionPageViewModel.url(it))) } }.toList() - val softwareSystemItems = generatorContext.workspace.model.includedSoftwareSystems + val softwareSystemItems = pageViewModel.includedSoftwareSystems .sortedBy { it.name.lowercase() } .map { createMenuItem(it.name, SoftwareSystemPageViewModel.url(it, SoftwareSystemPageViewModel.Tab.HOME), false) @@ -27,7 +26,7 @@ class MenuViewModel(generatorContext: GeneratorContext, private val pageViewMode private val groupSeparator = generatorContext.workspace.model.properties["structurizr.groupSeparator"] ?: "/" - private val softwareSystemPaths = generatorContext.workspace.model.includedSoftwareSystems + private val softwareSystemPaths = pageViewModel.includedSoftwareSystems .map { "${it.group ?: ""}$groupSeparator${it.name}" } .sortedBy { it.lowercase() } diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PageViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PageViewModel.kt index 063f5574..b24fb262 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PageViewModel.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PageViewModel.kt @@ -1,5 +1,6 @@ package nl.avisi.structurizr.site.generatr.site.model +import nl.avisi.structurizr.site.generatr.includedSoftwareSystems import nl.avisi.structurizr.site.generatr.site.GeneratorContext abstract class PageViewModel(protected val generatorContext: GeneratorContext) { @@ -19,6 +20,7 @@ abstract class PageViewModel(protected val generatorContext: GeneratorContext) { val flexmarkConfig by lazy { buildFlexmarkConfig(generatorContext) } val includeAdmonition = flexmarkConfig.selectedExtensionMap.containsKey("Admonition") val includeKatex = flexmarkConfig.selectedExtensionMap.containsKey("GitLab") + val includedSoftwareSystems = generatorContext.workspace.model.includedSoftwareSystems val configuration = generatorContext.workspace.views.configuration.properties val includeTreeview = configuration.getOrDefault("generatr.site.nestGroups", "false").toBoolean() diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SearchViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SearchViewModel.kt index b410f42e..0c8f5d41 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SearchViewModel.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SearchViewModel.kt @@ -1,6 +1,5 @@ package nl.avisi.structurizr.site.generatr.site.model -import nl.avisi.structurizr.site.generatr.includedSoftwareSystem import nl.avisi.structurizr.site.generatr.site.GeneratorContext import nl.avisi.structurizr.site.generatr.site.model.indexing.home import nl.avisi.structurizr.site.generatr.site.model.indexing.softwareSystemComponents @@ -25,8 +24,7 @@ class SearchViewModel(generatorContext: GeneratorContext) : PageViewModel(genera addAll(workspaceDecisions(generatorContext.workspace.documentation, this@SearchViewModel)) addAll(workspaceSections(generatorContext.workspace.documentation, this@SearchViewModel)) addAll( - generatorContext.workspace.model.softwareSystems - .filter { it.includedSoftwareSystem } + includedSoftwareSystems .flatMap { buildList { add(softwareSystemHome(it, this@SearchViewModel)) diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemTableUtilities.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemTableUtilities.kt index 1dc352f1..be75c605 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemTableUtilities.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemTableUtilities.kt @@ -6,11 +6,11 @@ import com.structurizr.model.SoftwareSystem fun TableViewModel.TableViewInitializerContext.softwareSystemCell( pageViewModel: PageViewModel, system: SoftwareSystem -) = if (system.location == Location.External) - headerCell("${system.name} (External)", greyText = true) -else +) = if (pageViewModel.includedSoftwareSystems.contains(system)) headerCellWithLink( pageViewModel, system.name, SoftwareSystemPageViewModel.url(system, SoftwareSystemPageViewModel.Tab.HOME) ) +else + headerCell("${system.name} (External)", greyText = true) From 8e2aaa845f8d00f6f143cc7e648c1f401c5fc418 Mon Sep 17 00:00:00 2001 From: Jens Peters Date: Sun, 29 Oct 2023 07:52:13 +0100 Subject: [PATCH 5/9] Introduce idea of external software system to groups Consider a software system external when groups are used and a software system is outside of any group. --- README.md | 6 ++++-- .../site/generatr/StructurizrUtilities.kt | 8 ++++---- .../generatr/site/model/MenuViewModelTest.kt | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index fb7adca5..5462e31a 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,9 @@ is generated from the example workspace in this repository. - Generate a static HTML site, based on a Structurizr DSL workspace. - Generates diagrams in SVG, PNG and PlantUML format, which can be viewed and downloaded from the generated site. -- Easy browsing through the site by clicking on software system and container elements in the diagrams. +- Easy browsing through the site by clicking on software system and container elements in the diagrams. Note that + external software systems are excluded from the menu. A software system is considered external when it lives outside + the (deprecated) enterprise boundary or when groups are used and the software system is outside of any group. - Start a development server which generates a site, serves it and updates the site automatically whenever a file that's part of the Structurizr workspace changes. - Include documentation (in Markdown or AsciiDoc format) in the generated site. Both workspace level documentation and software @@ -268,7 +270,7 @@ architecture model: See the included example for usage of some those properties in the -[C4 architecture model example](https://github.com/avisi-cloud/structurizr-site-generatr/blob/main/docs/example/workspace.dsl#L159). +[C4 architecture model example](https://github.com/avisi-cloud/structurizr-site-generatr/blob/main/docs/example/workspace.dsl#L163). ## Contributing diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/StructurizrUtilities.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/StructurizrUtilities.kt index 5ef8234c..2fdf6ccf 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/StructurizrUtilities.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/StructurizrUtilities.kt @@ -7,10 +7,10 @@ import com.structurizr.model.SoftwareSystem import com.structurizr.view.ViewSet val Model.includedSoftwareSystems: List - get() = softwareSystems.filter { it.includedSoftwareSystem } - -val SoftwareSystem.includedSoftwareSystem - get() = this.location != Location.External + get() = if (softwareSystems.any { it.group != null }) + softwareSystems.filter { it.group != null } + else + softwareSystems.filter { it.location != Location.External } val Container.hasComponents get() = this.components.isNotEmpty() diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt index a18aa90e..a41be598 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt @@ -181,6 +181,22 @@ class MenuViewModelTest : ViewModelTest() { } } + @Test + fun `do not show menu items of software systems outside of groups when groups are used`() { + val generatorContext = generatorContext(branches = listOf("main", "branch-2"), currentBranch = "main") + generatorContext.workspace.views.configuration.addProperty("generatr.site.nestGroups", "true") + generatorContext.workspace.model.addSoftwareSystem("System 1").group = "Group 1" + generatorContext.workspace.model.addSoftwareSystem("External system") + + MenuViewModel(generatorContext, createPageViewModel(generatorContext, url = HomePageViewModel.url())) + .let { + assertThat(it.softwareSystemNodes().children).hasSize(1) + assertThat(it.softwareSystemNodes().children[0].name).isEqualTo("Group 1") + assertThat(it.softwareSystemNodes().children[0].children).hasSize(1) + assertThat(it.softwareSystemNodes().children[0].children[0].name).isEqualTo("System 1") + } + } + private fun createPageViewModel(generatorContext: GeneratorContext, url: String = "/master/page"): PageViewModel { return object : PageViewModel(generatorContext) { override val url = url From 1795f5d8679812c53ddb9c3582f6feb36f84450a Mon Sep 17 00:00:00 2001 From: Jens Peters Date: Sun, 29 Oct 2023 07:55:08 +0100 Subject: [PATCH 6/9] Use groups in example model The enterprise boundary is deprecated. Also add other properties for convenience. Due to earlier work, this does not change the presentation of that model in the generated site. --- docs/example/workspace.dsl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/example/workspace.dsl b/docs/example/workspace.dsl index 8a2d52ee..d09dbaaf 100644 --- a/docs/example/workspace.dsl +++ b/docs/example/workspace.dsl @@ -9,11 +9,15 @@ workspace "Big Bank plc" "This is an example workspace to illustrate the key fea !adrs workspace-adrs model { + properties { + "structurizr.groupSeparator" "/" + } + customer = person "Personal Banking Customer" "A customer of the bank, with personal bank accounts." "Customer" acquirer = softwaresystem "Acquirer" "Facilitates PIN transactions for merchants." "External System" - enterprise "Big Bank plc" { + group "Big Bank plc" { supportStaff = person "Customer Service Staff" "Customer service staff within the bank." "Bank Staff" { properties { "Location" "Customer Services" @@ -177,6 +181,8 @@ workspace "Big Bank plc" "This is an example workspace to illustrate the key fea // * it's not possible to use "GitLab" and "ResizableImage" extensions together // default behaviour, if no generatr.markdown.flexmark.extensions property is specified, is to load the Tables extension only "generatr.markdown.flexmark.extensions" "Abbreviation,Admonition,AnchorLink,Attributes,Autolink,Definition,Emoji,Footnotes,GfmTaskList,GitLab,MediaTags,Tables,TableOfContents,Typographic" + + "generatr.site.nestGroups" "false" } systemlandscape "SystemLandscape" { From 0c9d82bf95dbd2605796529ca9b5ead32ae8b9fc Mon Sep 17 00:00:00 2001 From: Jens Peters Date: Sun, 29 Oct 2023 07:57:19 +0100 Subject: [PATCH 7/9] Add some ktlint properties Those settings mostly match the styling in the project. We don't enforce those rules, they are just for convenience for setups with a coresponding IDE plugin. --- .editorconfig | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.editorconfig b/.editorconfig index 2228b7c3..62a40587 100644 --- a/.editorconfig +++ b/.editorconfig @@ -29,3 +29,12 @@ indent_style = space [Dockerfile] # indent_size = 4 indent_style = space + +[*.{kt,kts}] +max_line_length = 160 +ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_allow_trailing_comma = false + +ktlint_standard_no-wildcard-imports = disabled +ktlint_standard_argument-list-wrapping = disabled +ktlint_standard_multiline_if_else = disabled \ No newline at end of file From 2f1612d4ebc8319d9e753ea4f3da372f9fdeef07 Mon Sep 17 00:00:00 2001 From: Jens Peters Date: Sun, 29 Oct 2023 08:31:20 +0100 Subject: [PATCH 8/9] Extend tests for external software systems --- .../generatr/site/model/MenuViewModelTest.kt | 39 +++++++++---------- ...wareSystemDependenciesPageViewModelTest.kt | 16 +++++++- .../model/SoftwareSystemsPageViewModelTest.kt | 13 ++++++- 3 files changed, 46 insertions(+), 22 deletions(-) diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt index a41be598..e4821a99 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt @@ -136,7 +136,7 @@ class MenuViewModelTest : ViewModelTest() { } @Test - fun `do not show menu entries for software systems with an external location`() { + fun `do not show menu entries for software systems with an external location (outside enterprise boundary)`() { val generatorContext = generatorContext(branches = listOf("main", "branch-2"), currentBranch = "main") generatorContext.workspace.model.addSoftwareSystem(Location.External, "System 1", "") @@ -147,10 +147,10 @@ class MenuViewModelTest : ViewModelTest() { } @Test - fun `show software systems from single group in software systems list`() { + fun `do not show menu entries for software systems with an external location (outside of any group when using groups)`() { val generatorContext = generatorContext(branches = listOf("main", "branch-2"), currentBranch = "main") - generatorContext.workspace.views.configuration.addProperty("generatr.site.nestGroups", "true") - generatorContext.workspace.model.addSoftwareSystem("System 1").group = "Group 1" + generatorContext.workspace.model.addSoftwareSystem("System 1").apply { group = "Group 1" } + generatorContext.workspace.model.addSoftwareSystem("External system") MenuViewModel(generatorContext, createPageViewModel(generatorContext, url = HomePageViewModel.url())) .let { @@ -162,38 +162,37 @@ class MenuViewModelTest : ViewModelTest() { } @Test - fun `show nested groups in software systems list`() { + fun `show software systems in nested groups from single group in software systems list`() { val generatorContext = generatorContext(branches = listOf("main", "branch-2"), currentBranch = "main") generatorContext.workspace.views.configuration.addProperty("generatr.site.nestGroups", "true") - generatorContext.workspace.model.addProperty("structurizr.groupSeparator", "/") - generatorContext.workspace.model.addSoftwareSystem("System 1").group = "Group 1" - generatorContext.workspace.model.addSoftwareSystem("System 2").group = "Group 1" - generatorContext.workspace.model.addSoftwareSystem("System 3").group = "Group 2" - generatorContext.workspace.model.addSoftwareSystem("System 4").group = "Group 1/Group 3" + generatorContext.workspace.model.addSoftwareSystem("System 1").apply { group = "Group 1" } MenuViewModel(generatorContext, createPageViewModel(generatorContext, url = HomePageViewModel.url())) .let { - assertThat(it.softwareSystemNodes().children).hasSize(2) + assertThat(it.softwareSystemNodes().children).hasSize(1) assertThat(it.softwareSystemNodes().children[0].name).isEqualTo("Group 1") - assertThat(it.softwareSystemNodes().children[0].children).hasSize(3) - assertThat(it.softwareSystemNodes().children[0].children[0].name).isEqualTo("Group 3") - assertThat(it.softwareSystemNodes().children[0].children[0].children[0].name).isEqualTo("System 4") + assertThat(it.softwareSystemNodes().children[0].children).hasSize(1) + assertThat(it.softwareSystemNodes().children[0].children[0].name).isEqualTo("System 1") } } @Test - fun `do not show menu items of software systems outside of groups when groups are used`() { + fun `show nested groups in software systems list`() { val generatorContext = generatorContext(branches = listOf("main", "branch-2"), currentBranch = "main") generatorContext.workspace.views.configuration.addProperty("generatr.site.nestGroups", "true") - generatorContext.workspace.model.addSoftwareSystem("System 1").group = "Group 1" - generatorContext.workspace.model.addSoftwareSystem("External system") + generatorContext.workspace.model.addProperty("structurizr.groupSeparator", "/") + generatorContext.workspace.model.addSoftwareSystem("System 1").apply { group = "Group 1" } + generatorContext.workspace.model.addSoftwareSystem("System 2").apply { group = "Group 1" } + generatorContext.workspace.model.addSoftwareSystem("System 3").apply { group = "Group 2" } + generatorContext.workspace.model.addSoftwareSystem("System 4").apply { group = "Group 1/Group 3" } MenuViewModel(generatorContext, createPageViewModel(generatorContext, url = HomePageViewModel.url())) .let { - assertThat(it.softwareSystemNodes().children).hasSize(1) + assertThat(it.softwareSystemNodes().children).hasSize(2) assertThat(it.softwareSystemNodes().children[0].name).isEqualTo("Group 1") - assertThat(it.softwareSystemNodes().children[0].children).hasSize(1) - assertThat(it.softwareSystemNodes().children[0].children[0].name).isEqualTo("System 1") + assertThat(it.softwareSystemNodes().children[0].children).hasSize(3) + assertThat(it.softwareSystemNodes().children[0].children[0].name).isEqualTo("Group 3") + assertThat(it.softwareSystemNodes().children[0].children[0].children[0].name).isEqualTo("System 4") } } diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDependenciesPageViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDependenciesPageViewModelTest.kt index b2a6a128..9641fbfc 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDependenciesPageViewModelTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDependenciesPageViewModelTest.kt @@ -90,7 +90,7 @@ class SoftwareSystemDependenciesPageViewModelTest : ViewModelTest() { } @Test - fun `dependencies from and to external systems`() { + fun `dependencies from and to external systems (outside enterprise boundary)`() { val externalSystem = generatorContext.workspace.model .addSoftwareSystem(Location.External, "External system", "") externalSystem.uses(softwareSystem1, "Uses", "REST") @@ -103,6 +103,20 @@ class SoftwareSystemDependenciesPageViewModelTest : ViewModelTest() { .isEqualTo(TableViewModel.TextCellViewModel("External system (External)", isHeader = true, greyText = true)) } + @Test + fun `dependencies from and to external systems (outside of any group when using groups)`() { + softwareSystem1.group = "Group 1" + val externalSystem = generatorContext.workspace.model.addSoftwareSystem("External system") + externalSystem.uses(softwareSystem1, "Uses", "REST") + softwareSystem1.uses(externalSystem, "Uses", "REST") + + val viewModel = SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1) + assertThat(viewModel.dependenciesInboundTable.bodyRows[0].columns[0]) + .isEqualTo(TableViewModel.TextCellViewModel("External system (External)", isHeader = true, greyText = true)) + assertThat(viewModel.dependenciesOutboundTable.bodyRows[0].columns[0]) + .isEqualTo(TableViewModel.TextCellViewModel("External system (External)", isHeader = true, greyText = true)) + } + @Test fun `sort by source system name case insensitive`() { val system = generatorContext.workspace.model.addSoftwareSystem("Software system 3") diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemsPageViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemsPageViewModelTest.kt index 598303db..50f75be3 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemsPageViewModelTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemsPageViewModelTest.kt @@ -69,7 +69,7 @@ class SoftwareSystemsPageViewModelTest : ViewModelTest() { } @Test - fun `external systems have grey text`() { + fun `external systems have grey text (outside enterprise boundary)`() { val generatorContext = generatorContext() generatorContext.workspace.model.addSoftwareSystem(Location.External, "system 1", "System 1 description") val viewModel = SoftwareSystemsPageViewModel(generatorContext) @@ -77,4 +77,15 @@ class SoftwareSystemsPageViewModelTest : ViewModelTest() { assertThat(viewModel.softwareSystemsTable.bodyRows[0].columns[0]) .isEqualTo(TableViewModel.TextCellViewModel("system 1 (External)", isHeader = true, greyText = true)) } + + @Test + fun `external systems have grey text (outside of any group when using groups)`() { + val generatorContext = generatorContext() + generatorContext.workspace.model.addSoftwareSystem("system 1", "System 1 description").apply { group = "Group 1" } + generatorContext.workspace.model.addSoftwareSystem("system 2", "System 2 description") + val viewModel = SoftwareSystemsPageViewModel(generatorContext) + + assertThat(viewModel.softwareSystemsTable.bodyRows[1].columns[0]) + .isEqualTo(TableViewModel.TextCellViewModel("system 2 (External)", isHeader = true, greyText = true)) + } } From 9d29ce92e905233f2a5bd4ea1255c28fd4847ea6 Mon Sep 17 00:00:00 2001 From: Jens Peters Date: Fri, 3 Nov 2023 09:27:43 +0100 Subject: [PATCH 9/9] Detect external systems by custom tag Replace our own heuristics based on groups. Therefore add a property "generatr.site.excludedTag" to define the name of the tag that identifies external systems. Also adjust example. --- README.md | 6 +- docs/example/workspace.dsl | 1 + .../site/generatr/StructurizrUtilities.kt | 18 +-- .../C4PlantUmlExporterWithElementLinks.kt | 6 +- .../site/generatr/site/DiagramGenerator.kt | 3 +- .../site/generatr/site/SiteGenerator.kt | 2 +- .../site/generatr/site/model/PageViewModel.kt | 2 +- .../C4PlantUmlExporterWithElementLinksTest.kt | 125 ++++++++++++------ .../generatr/site/model/MenuViewModelTest.kt | 5 +- .../site/model/SearchViewModelTest.kt | 26 ++++ ...wareSystemDependenciesPageViewModelTest.kt | 12 +- .../model/SoftwareSystemsPageViewModelTest.kt | 8 +- 12 files changed, 149 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 5462e31a..651ae8de 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ is generated from the example workspace in this repository. - Generates diagrams in SVG, PNG and PlantUML format, which can be viewed and downloaded from the generated site. - Easy browsing through the site by clicking on software system and container elements in the diagrams. Note that external software systems are excluded from the menu. A software system is considered external when it lives outside - the (deprecated) enterprise boundary or when groups are used and the software system is outside of any group. + the (deprecated) enterprise boundary or when it contains a specific tag, see [Customizing the generated website](#customizing-the-generated-website). - Start a development server which generates a site, serves it and updates the site automatically whenever a file that's part of the Structurizr workspace changes. - Include documentation (in Markdown or AsciiDoc format) in the generated site. Both workspace level documentation and software @@ -266,8 +266,8 @@ architecture model: | `generatr.search.language` | Indexing/stemming language for the search index. See [Lunr language support](https://github.com/olivernn/lunr-languages) | `en` | `nl` | | `generatr.markdown.flexmark.extensions` | Additional extensions to the markdown generator to add new markdown capabilities. [More Details](https://avisi-cloud.github.io/structurizr-site-generatr/main/extended-markdown-features/) | Tables | `Tables,Admonition` | | `generatr.svglink.target` | Specifies the link target for element links in the exported svg | `_top` | `_self` | -| `generatr.site.nestGroups` | Will show software systems in the left side navigator in collapsable groups | `false` | `true` | - +| `generatr.site.externalTag` | Software systems containing this tag will be considered external | | | +| `generatr.site.nestGroups` | Will show software systems in the left side navigator in collapsable ~~~~groups | `false` | `true` | See the included example for usage of some those properties in the [C4 architecture model example](https://github.com/avisi-cloud/structurizr-site-generatr/blob/main/docs/example/workspace.dsl#L163). diff --git a/docs/example/workspace.dsl b/docs/example/workspace.dsl index d09dbaaf..2f2ef324 100644 --- a/docs/example/workspace.dsl +++ b/docs/example/workspace.dsl @@ -182,6 +182,7 @@ workspace "Big Bank plc" "This is an example workspace to illustrate the key fea // default behaviour, if no generatr.markdown.flexmark.extensions property is specified, is to load the Tables extension only "generatr.markdown.flexmark.extensions" "Abbreviation,Admonition,AnchorLink,Attributes,Autolink,Definition,Emoji,Footnotes,GfmTaskList,GitLab,MediaTags,Tables,TableOfContents,Typographic" + "generatr.site.externalTag" "External System" "generatr.site.nestGroups" "false" } diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/StructurizrUtilities.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/StructurizrUtilities.kt index 2fdf6ccf..22b58559 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/StructurizrUtilities.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/StructurizrUtilities.kt @@ -1,19 +1,16 @@ package nl.avisi.structurizr.site.generatr +import com.structurizr.Workspace import com.structurizr.model.Container import com.structurizr.model.Location -import com.structurizr.model.Model import com.structurizr.model.SoftwareSystem import com.structurizr.view.ViewSet -val Model.includedSoftwareSystems: List - get() = if (softwareSystems.any { it.group != null }) - softwareSystems.filter { it.group != null } - else - softwareSystems.filter { it.location != Location.External } - -val Container.hasComponents - get() = this.components.isNotEmpty() +val Workspace.includedSoftwareSystems: List + get() = model.softwareSystems.filter { + val externalTag = views.configuration.properties.getOrDefault("generatr.site.externalTag", null) + it.location != Location.External && if (externalTag != null) !it.tags.contains(externalTag) else true + } val SoftwareSystem.hasContainers get() = this.containers.isNotEmpty() @@ -21,6 +18,9 @@ val SoftwareSystem.hasContainers val SoftwareSystem.includedProperties get() = this.properties.filterNot { (name, _) -> name == "structurizr.dsl.identifier" } +val Container.hasComponents + get() = this.components.isNotEmpty() + fun SoftwareSystem.hasDecisions() = documentation.decisions.isNotEmpty() fun SoftwareSystem.hasContainerDecisions() = containers.any { it.hasDecisions() } diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/C4PlantUmlExporterWithElementLinks.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/C4PlantUmlExporterWithElementLinks.kt index 696af505..57b07949 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/C4PlantUmlExporterWithElementLinks.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/C4PlantUmlExporterWithElementLinks.kt @@ -1,5 +1,6 @@ package nl.avisi.structurizr.site.generatr.site +import com.structurizr.Workspace import com.structurizr.export.Diagram import com.structurizr.export.IndentingWriter import com.structurizr.export.plantuml.C4PlantUMLExporter @@ -10,6 +11,7 @@ import com.structurizr.view.* import nl.avisi.structurizr.site.generatr.* class C4PlantUmlExporterWithElementLinks( + private val workspace: Workspace, private val url: String ) : C4PlantUMLExporter() { companion object { @@ -48,7 +50,7 @@ class C4PlantUmlExporterWithElementLinks( } private fun needsLinkToSoftwareSystem(element: Element?, view: ModelView?) = - element is SoftwareSystem && view != null && view.model.includedSoftwareSystems.contains(element) && element != view.softwareSystem + element is SoftwareSystem && workspace.includedSoftwareSystems.contains(element) && element != view?.softwareSystem private fun getUrlToSoftwareSystem(element: Element?): String { val path = "/${element?.name?.normalize()}/context/".asUrlToDirectory(url) @@ -56,7 +58,7 @@ class C4PlantUmlExporterWithElementLinks( } private fun needsLinkToContainerViews(element: Element?, view: ModelView?) = - element is SoftwareSystem && view != null && view.model.includedSoftwareSystems.contains(element) && element == view.softwareSystem && element.hasContainers + element is SoftwareSystem && workspace.includedSoftwareSystems.contains(element) && element == view?.softwareSystem && element.hasContainers private fun getUrlToContainerViews(element: Element?): String { val path = "/${element?.name?.normalize()}/container/".asUrlToDirectory(url) diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/DiagramGenerator.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/DiagramGenerator.kt index 83e13e5e..71b2ec2d 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/DiagramGenerator.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/DiagramGenerator.kt @@ -9,6 +9,7 @@ import com.structurizr.view.View import net.sourceforge.plantuml.FileFormat import net.sourceforge.plantuml.FileFormatOption import net.sourceforge.plantuml.SourceStringReader +import nl.avisi.structurizr.site.generatr.includedSoftwareSystems import nl.avisi.structurizr.site.generatr.site.C4PlantUmlExporterWithElementLinks.Companion.export import java.io.ByteArrayOutputStream import java.io.File @@ -83,7 +84,7 @@ private fun saveAsPng(diagram: Diagram, pngDir: File) { } private fun generatePlantUMLDiagramWithElementLinks(workspace: Workspace, view: View, url: String): Diagram { - val plantUMLExporter = C4PlantUmlExporterWithElementLinks(url) + val plantUMLExporter = C4PlantUmlExporterWithElementLinks(workspace, url) if (workspace.views.configuration.properties.containsKey("generatr.svglink.target")) { plantUMLExporter.addSkinParam( diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/SiteGenerator.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/SiteGenerator.kt index c97037f0..badf0261 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/SiteGenerator.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/SiteGenerator.kt @@ -131,7 +131,7 @@ private fun generateHtmlFiles(context: GeneratorContext, branchDir: File) { add { writeHtmlFile(branchDir, WorkspaceDecisionPageViewModel(context, it)) } } - context.workspace.model.includedSoftwareSystems.forEach { + context.workspace.includedSoftwareSystems.forEach { add { writeHtmlFile(branchDir, SoftwareSystemHomePageViewModel(context, it)) } add { writeHtmlFile(branchDir, SoftwareSystemContextPageViewModel(context, it)) } add { writeHtmlFile(branchDir, SoftwareSystemContainerPageViewModel(context, it)) } diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PageViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PageViewModel.kt index b24fb262..f591720f 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PageViewModel.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PageViewModel.kt @@ -20,7 +20,7 @@ abstract class PageViewModel(protected val generatorContext: GeneratorContext) { val flexmarkConfig by lazy { buildFlexmarkConfig(generatorContext) } val includeAdmonition = flexmarkConfig.selectedExtensionMap.containsKey("Admonition") val includeKatex = flexmarkConfig.selectedExtensionMap.containsKey("GitLab") - val includedSoftwareSystems = generatorContext.workspace.model.includedSoftwareSystems + val includedSoftwareSystems = generatorContext.workspace.includedSoftwareSystems val configuration = generatorContext.workspace.views.configuration.properties val includeTreeview = configuration.getOrDefault("generatr.site.nestGroups", "false").toBoolean() diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/C4PlantUmlExporterWithElementLinksTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/C4PlantUmlExporterWithElementLinksTest.kt index b9918f1d..764157b7 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/C4PlantUmlExporterWithElementLinksTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/C4PlantUmlExporterWithElementLinksTest.kt @@ -4,17 +4,16 @@ import assertk.assertThat import assertk.assertions.contains import assertk.assertions.isEqualTo import com.structurizr.Workspace -import com.structurizr.view.ContainerView -import com.structurizr.view.SystemContextView +import com.structurizr.model.Location import kotlin.test.Test class C4PlantUmlExporterWithElementLinksTest { @Test fun `adds skinparam to remove explicit size from generated svg`() { - val view = createWorkspaceWithOneSystem() + val workspace = createWorkspaceWithOneSystem() - val diagram = C4PlantUmlExporterWithElementLinks("/landscape/") - .export(view) + val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/landscape/") + .export(workspace.views.systemContextViews.first()) assertThat(diagram.definition) .contains("skinparam svgDimensionStyle false") @@ -22,10 +21,10 @@ class C4PlantUmlExporterWithElementLinksTest { @Test fun `adds skinparam to preserve the aspect ratio of the generated svg`() { - val view = createWorkspaceWithOneSystem() + val workspace = createWorkspaceWithOneSystem() - val diagram = C4PlantUmlExporterWithElementLinks("/landscape/") - .export(view) + val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/landscape/") + .export(workspace.views.systemContextViews.first()) assertThat(diagram.definition) .contains("skinparam preserveAspectRatio meet") @@ -33,10 +32,10 @@ class C4PlantUmlExporterWithElementLinksTest { @Test fun `renders diagram`() { - val view = createWorkspaceWithOneSystem() + val workspace = createWorkspaceWithOneSystem() - val diagram = C4PlantUmlExporterWithElementLinks("/landscape/") - .export(view) + val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/landscape/") + .export(workspace.views.systemContextViews.first()) assertThat(diagram.definition.withoutHeaderAndFooter()).isEqualTo( """ @@ -47,10 +46,10 @@ class C4PlantUmlExporterWithElementLinksTest { @Test fun `renders System Diagram with link to container`() { - val view = createWorkspaceWithOneSystemWithContainers() + val workspace = createWorkspaceWithOneSystemWithContainers() - val diagram = C4PlantUmlExporterWithElementLinks("/container/") - .export(view) + val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/container/") + .export(workspace.views.systemContextViews.first()) assertThat(diagram.definition.withoutHeaderAndFooter()).isEqualTo( """ @@ -61,10 +60,10 @@ class C4PlantUmlExporterWithElementLinksTest { @Test fun `renders System Diagram with link to other system and link to container`() { - val view = createWorkspaceWithTwoSystemWithContainers() + val workspace = createSystemContextViewForWorkspaceWithTwoSystemWithContainers() - val diagram = C4PlantUmlExporterWithElementLinks("/container/") - .export(view) + val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/container/") + .export(workspace.views.systemContextViews.first()) assertThat(diagram.definition.withoutHeaderAndFooter()).isEqualTo( """ @@ -78,10 +77,10 @@ class C4PlantUmlExporterWithElementLinksTest { @Test fun `renders Container Diagram with link to component diagram`() { - val view = createWorkspaceWithOneSystemWithContainersAndComponents() + val workspace = createWorkspaceWithOneSystemWithContainersAndComponents() - val diagram = C4PlantUmlExporterWithElementLinks("/container/") - .export(view) + val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/container/") + .export(workspace.views.containerViews.first()) assertThat(diagram.definition.withoutHeaderAndFooter()).isEqualTo( """ @@ -105,7 +104,7 @@ class C4PlantUmlExporterWithElementLinksTest { val view = workspace.views.createComponentView(container1, "Component2", "") .apply { addAllElements() } - val diagram = C4PlantUmlExporterWithElementLinks("/system-1/component/") + val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/system-1/component/") .export(view) assertThat(diagram.definition.withoutHeaderAndFooter()).isEqualTo( @@ -125,10 +124,10 @@ class C4PlantUmlExporterWithElementLinksTest { @Test fun `link to other software system`() { - val view = createWorkspaceWithTwoSystems() + val workspace = createWorkspaceWithTwoSystems() - val diagram = C4PlantUmlExporterWithElementLinks("/landscape/") - .export(view) + val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/landscape/") + .export(workspace.views.systemContextViews.first()) assertThat(diagram.definition.withoutHeaderAndFooter()).isEqualTo( """ @@ -141,12 +140,50 @@ class C4PlantUmlExporterWithElementLinksTest { } @Test - fun `link to other software system from two path segments deep`() { - val view = createWorkspaceWithTwoSystems() + fun `external software system (outside enterprise boundary)`() { + val workspace = createWorkspaceWithTwoSystems() + workspace.model.softwareSystems.single { it.name == "System 2" }.location = Location.External + + val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/landscape/") + .export(workspace.views.systemContextViews.first()) + + assertThat(diagram.definition.withoutHeaderAndFooter()).isEqualTo( + """ + System(System1, "System 1", ${'$'}descr="", ${'$'}tags="", ${'$'}link="") + System_Ext(System2, "System 2", ${'$'}descr="", ${'$'}tags="", ${'$'}link="") - val diagram = C4PlantUmlExporterWithElementLinks("/system-1/context/") + Rel(System2, System1, "uses", ${'$'}techn="", ${'$'}tags="", ${'$'}link="") + """.trimIndent() + ) + } + + @Test + fun `external software system (declared external by tag)`() { + val workspace = createWorkspaceWithTwoSystems() + workspace.views.configuration.addProperty("generatr.site.externalTag", "External System") + workspace.model.softwareSystems.single { it.name == "System 2" }.addTags("External System") + val view = workspace.views.systemContextViews.first() + + val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/landscape/") .export(view) + assertThat(diagram.definition.withoutHeaderAndFooter()).isEqualTo( + """ + System(System1, "System 1", ${'$'}descr="", ${'$'}tags="", ${'$'}link="") + System(System2, "System 2", ${'$'}descr="", ${'$'}tags="", ${'$'}link="") + + Rel(System2, System1, "uses", ${'$'}techn="", ${'$'}tags="", ${'$'}link="") + """.trimIndent() + ) + } + + @Test + fun `link to other software system from two path segments deep`() { + val workspace = createWorkspaceWithTwoSystems() + + val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/system-1/context/") + .export(workspace.views.systemContextViews.first()) + assertThat(diagram.definition.withoutHeaderAndFooter()).isEqualTo( """ System(System1, "System 1", ${'$'}descr="", ${'$'}tags="", ${'$'}link="") @@ -157,15 +194,19 @@ class C4PlantUmlExporterWithElementLinksTest { ) } - private fun createWorkspaceWithOneSystem(): SystemContextView { - val workspace = Workspace("workspace name", "") + private fun createWorkspaceWithOneSystem(): Workspace { + val workspace = Workspace("workspace name", "").apply { + views.configuration.addProperty("generatr.site.excludedTag", "External System") + } val system = workspace.model.addSoftwareSystem("System 1") - return workspace.views.createSystemContextView(system, "Context1", "") + workspace.views.createSystemContextView(system, "Context1", "") .apply { addAllElements() } + + return workspace } - private fun createWorkspaceWithOneSystemWithContainers(): SystemContextView? { + private fun createWorkspaceWithOneSystemWithContainers(): Workspace { val workspace = Workspace("workspace name", "") val system = workspace.model.addSoftwareSystem("System 1") system.addContainer("Container 1") @@ -174,11 +215,13 @@ class C4PlantUmlExporterWithElementLinksTest { workspace.views.createContainerView(system, "Container1", "") .apply { addAllElements() } - return workspace.views.createSystemContextView(system, "Context 1", "") + workspace.views.createSystemContextView(system, "Context 1", "") .apply { addAllElements() } + + return workspace } - private fun createWorkspaceWithTwoSystemWithContainers(): SystemContextView? { + private fun createSystemContextViewForWorkspaceWithTwoSystemWithContainers(): Workspace { val workspace = Workspace("workspace name", "") val system = workspace.model.addSoftwareSystem("System 1") workspace.model.addSoftwareSystem("System 2").apply { uses(system, "uses") } @@ -188,11 +231,13 @@ class C4PlantUmlExporterWithElementLinksTest { workspace.views.createContainerView(system, "Container1", "") .apply { addAllElements() } - return workspace.views.createSystemContextView(system, "Context 1", "") + workspace.views.createSystemContextView(system, "Context 1", "") .apply { addAllElements() } + + return workspace } - private fun createWorkspaceWithOneSystemWithContainersAndComponents(): ContainerView? { + private fun createWorkspaceWithOneSystemWithContainersAndComponents(): Workspace { val workspace = Workspace("workspace name", "") val system = workspace.model.addSoftwareSystem("System 1") val container = system.addContainer("Container 1") @@ -205,17 +250,21 @@ class C4PlantUmlExporterWithElementLinksTest { workspace.views.createComponentView(container, "Component1", "") - return workspace.views.createContainerView(system, "Container1", "") + workspace.views.createContainerView(system, "Container1", "") .apply { addAllElements() } + + return workspace } - private fun createWorkspaceWithTwoSystems(): SystemContextView { + private fun createWorkspaceWithTwoSystems(): Workspace { val workspace = Workspace("workspace name", "") val system = workspace.model.addSoftwareSystem("System 1") workspace.model.addSoftwareSystem("System 2").apply { uses(system, "uses") } - return workspace.views.createSystemContextView(system, "Context 1", "") + workspace.views.createSystemContextView(system, "Context 1", "") .apply { addAllElements() } + + return workspace } private fun String.withoutHeaderAndFooter() = this diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt index e4821a99..954a7dea 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt @@ -147,10 +147,11 @@ class MenuViewModelTest : ViewModelTest() { } @Test - fun `do not show menu entries for software systems with an external location (outside of any group when using groups)`() { + fun `do not show menu entries for software systems with an external location (declared external by tag)`() { val generatorContext = generatorContext(branches = listOf("main", "branch-2"), currentBranch = "main") + generatorContext.workspace.views.configuration.addProperty("generatr.site.externalTag", "External System") generatorContext.workspace.model.addSoftwareSystem("System 1").apply { group = "Group 1" } - generatorContext.workspace.model.addSoftwareSystem("External system") + generatorContext.workspace.model.addSoftwareSystem("External system").apply { addTags("External System") } MenuViewModel(generatorContext, createPageViewModel(generatorContext, url = HomePageViewModel.url())) .let { diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SearchViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SearchViewModelTest.kt index 440890cb..68ca803b 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SearchViewModelTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SearchViewModelTest.kt @@ -7,6 +7,7 @@ import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import com.structurizr.documentation.Format import com.structurizr.documentation.Section +import com.structurizr.model.Location import org.junit.jupiter.api.Test class SearchViewModelTest : ViewModelTest() { @@ -61,6 +62,31 @@ class SearchViewModelTest : ViewModelTest() { .containsExactly("Context views") } + @Test + fun `indexes no external software system (outside enterprise boundary)`() { + val generatorContext = generatorContext() + generatorContext.workspace.apply { + model.addSoftwareSystem("Software system").apply { location = Location.External } + } + val viewModel = SearchViewModel(generatorContext) + + assertThat(viewModel.documents.map { it.type }) + .isEmpty() + } + + @Test + fun `indexes no external software system (declared external by tag)`() { + val generatorContext = generatorContext() + generatorContext.workspace.apply { + views.configuration.addProperty("generatr.site.externalTag", "External System") + model.addSoftwareSystem("Software system").apply { addTags("External System") } + } + val viewModel = SearchViewModel(generatorContext) + + assertThat(viewModel.documents.map { it.type }) + .isEmpty() + } + @Test fun `indexes all software system information`() { val generatorContext = generatorContext() diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDependenciesPageViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDependenciesPageViewModelTest.kt index 9641fbfc..53b8bb74 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDependenciesPageViewModelTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDependenciesPageViewModelTest.kt @@ -81,6 +81,7 @@ class SoftwareSystemDependenciesPageViewModelTest : ViewModelTest() { softwareSystem1.uses(backend2, "Uses from system 1 to container 2", "REST") val viewModel = SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1) + // Inbound Table assertThat(viewModel.dependenciesInboundTable.bodyRows.extractTitle()) .containsExactly("Software system 2") @@ -95,8 +96,8 @@ class SoftwareSystemDependenciesPageViewModelTest : ViewModelTest() { .addSoftwareSystem(Location.External, "External system", "") externalSystem.uses(softwareSystem1, "Uses", "REST") softwareSystem1.uses(externalSystem, "Uses", "REST") - val viewModel = SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1) + assertThat(viewModel.dependenciesInboundTable.bodyRows[0].columns[0]) .isEqualTo(TableViewModel.TextCellViewModel("External system (External)", isHeader = true, greyText = true)) assertThat(viewModel.dependenciesOutboundTable.bodyRows[0].columns[0]) @@ -104,13 +105,14 @@ class SoftwareSystemDependenciesPageViewModelTest : ViewModelTest() { } @Test - fun `dependencies from and to external systems (outside of any group when using groups)`() { - softwareSystem1.group = "Group 1" - val externalSystem = generatorContext.workspace.model.addSoftwareSystem("External system") + fun `dependencies from and to external systems (declared external by tag)`() { + generatorContext.workspace.views.configuration.addProperty("generatr.site.externalTag", "External System") + val externalSystem = generatorContext.workspace.model.addSoftwareSystem("External system").apply { addTags("External System") } externalSystem.uses(softwareSystem1, "Uses", "REST") softwareSystem1.uses(externalSystem, "Uses", "REST") val viewModel = SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1) + assertThat(viewModel.dependenciesInboundTable.bodyRows[0].columns[0]) .isEqualTo(TableViewModel.TextCellViewModel("External system (External)", isHeader = true, greyText = true)) assertThat(viewModel.dependenciesOutboundTable.bodyRows[0].columns[0]) @@ -123,8 +125,8 @@ class SoftwareSystemDependenciesPageViewModelTest : ViewModelTest() { system.uses(softwareSystem1, "Uses", "REST") softwareSystem1.uses(softwareSystem2, "Uses REST", "REST") softwareSystem2.uses(softwareSystem1, "Uses SOAP", "SOAP") - val viewModel = SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1) + // Inbound Table assertThat(viewModel.dependenciesInboundTable.bodyRows.extractTitle()) .containsExactly("Software system 2", "Software system 3") diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemsPageViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemsPageViewModelTest.kt index 50f75be3..d96d1d7f 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemsPageViewModelTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemsPageViewModelTest.kt @@ -79,10 +79,12 @@ class SoftwareSystemsPageViewModelTest : ViewModelTest() { } @Test - fun `external systems have grey text (outside of any group when using groups)`() { + fun `external systems have grey text (declared external by tag)`() { val generatorContext = generatorContext() - generatorContext.workspace.model.addSoftwareSystem("system 1", "System 1 description").apply { group = "Group 1" } - generatorContext.workspace.model.addSoftwareSystem("system 2", "System 2 description") + generatorContext.workspace.views.configuration.addProperty("generatr.site.externalTag", "External System") + generatorContext.workspace.model.addSoftwareSystem("system 1", "System 1 description") + generatorContext.workspace.model.addSoftwareSystem("system 2", "System 2 description").apply { addTags("External System") } + val viewModel = SoftwareSystemsPageViewModel(generatorContext) assertThat(viewModel.softwareSystemsTable.bodyRows[1].columns[0])