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 diff --git a/README.md b/README.md index fb7adca5..651ae8de 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 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 @@ -264,11 +266,11 @@ 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#L159). +[C4 architecture model example](https://github.com/avisi-cloud/structurizr-site-generatr/blob/main/docs/example/workspace.dsl#L163). ## Contributing diff --git a/docs/example/workspace.dsl b/docs/example/workspace.dsl index 8a2d52ee..2f2ef324 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,9 @@ 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.externalTag" "External System" + "generatr.site.nestGroups" "false" } systemlandscape "SystemLandscape" { 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..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() = softwareSystems.filter { it.includedSoftwareSystem } - -val SoftwareSystem.includedSoftwareSystem - get() = this.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 63629d3f..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,8 +11,9 @@ import com.structurizr.view.* import nl.avisi.structurizr.site.generatr.* class C4PlantUmlExporterWithElementLinks( + private val workspace: Workspace, private val url: String -): C4PlantUMLExporter() { +) : C4PlantUMLExporter() { companion object { const val TEMP_URI = "https://will-be-changed-to-relative/" @@ -48,7 +50,7 @@ class C4PlantUmlExporterWithElementLinks( } private fun needsLinkToSoftwareSystem(element: Element?, view: ModelView?) = - element is SoftwareSystem && element.includedSoftwareSystem && 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 && element.includedSoftwareSystem && 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) @@ -88,5 +90,4 @@ class C4PlantUmlExporterWithElementLinks( .split(System.lineSeparator()) .forEach { line -> writer?.writeLine(line) } } - } 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/MenuViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModel.kt index e131250b..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,17 +18,16 @@ 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) } - 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 } - .map { it.group + groupSeparator + it.name } + private val softwareSystemPaths = pageViewModel.includedSoftwareSystems + .map { "${it.group ?: ""}$groupSeparator${it.name}" } .sortedBy { it.lowercase() } private fun createMenuItem(title: String, href: String, exact: Boolean = true) = @@ -39,8 +37,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()) 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..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 @@ -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.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) 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 cafa89b1..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 @@ -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,15 +135,57 @@ class MenuViewModelTest : ViewModelTest() { } } + @Test + 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", "") + + MenuViewModel(generatorContext, createPageViewModel(generatorContext, url = HomePageViewModel.url())) + .let { + assertThat(it.softwareSystemItems).hasSize(0) + } + } + + @Test + 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").apply { addTags("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") + } + } + + @Test + 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.addSoftwareSystem("System 1").apply { 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") 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" } + 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 { 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 b2a6a128..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") @@ -90,13 +91,28 @@ 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") 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 `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]) @@ -109,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 598303db..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 @@ -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,17 @@ 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 (declared external by tag)`() { + val generatorContext = generatorContext() + 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]) + .isEqualTo(TableViewModel.TextCellViewModel("system 2 (External)", isHeader = true, greyText = true)) + } }