You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by jo...@apache.org on 2021/09/10 11:01:18 UTC

[isis] 01/07: ISIS-2846 Create a LinkTreeDiagram (via PlantUML mindmap) from History/LogEntry

This is an automated email from the ASF dual-hosted git repository.

joergrade pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/isis.git

commit f46347040f2abaadadf4bbec99bececd1a7462a4
Author: Jörg Rade <jo...@kuehne-nagel.com>
AuthorDate: Tue Aug 31 14:06:48 2021 +0200

    ISIS-2846 Create a LinkTreeDiagram (via PlantUML mindmap) from History/LogEntry
---
 .../isis/client/kroviz/core/event/LogEntry.kt      |   5 +
 .../client/kroviz/core/event/LogEntryDecorator.kt  |  84 ++++------------
 .../client/kroviz/ui/diagram/LinkTreeDiagram.kt    | 112 ++++++++++++++++-----
 .../isis/client/kroviz/ui/dialog/DiagramDialog.kt  |  27 ++++-
 .../isis/client/kroviz/ui/dialog/EventLogDetail.kt |  35 +++----
 .../client/kroviz/ui/panel/DynamicMenuBuilder.kt   |  15 +--
 .../apache/isis/client/kroviz/utils/StringUtils.kt |  12 +++
 .../kroviz/core/event/LogEntryDecoratorTest.kt     |  23 +++++
 .../isis/client/kroviz/util/StringUtilsTest.kt     |  24 +++++
 9 files changed, 212 insertions(+), 125 deletions(-)

diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/event/LogEntry.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/event/LogEntry.kt
index 2fe5973..a7db164 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/event/LogEntry.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/event/LogEntry.kt
@@ -250,6 +250,11 @@ data class LogEntry(
         return null
     }
 
+    fun upLink(): Link? {
+        getLinks().forEach { if (it.relation() == Relation.UP) return it }
+        return null
+    }
+
     fun getLinks(): List<Link> {
         return if (obj is HasLinks) {
             (obj as HasLinks).getLinks()
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/event/LogEntryDecorator.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/event/LogEntryDecorator.kt
index b5e41ac..68e9626 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/event/LogEntryDecorator.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/event/LogEntryDecorator.kt
@@ -21,41 +21,12 @@ package org.apache.isis.client.kroviz.core.event
 import org.apache.isis.client.kroviz.to.Link
 import org.apache.isis.client.kroviz.to.Relation
 import org.apache.isis.client.kroviz.ui.core.Constants
-import org.apache.isis.client.kroviz.utils.StringUtils
 
 class LogEntryDecorator(val logEntry: LogEntry) {
 
-    val href: String
-    val links: List<Link>
-    val linked: List<LogEntry>
-
-    init {
-        href = logEntry.selfHref()
-        links = logEntry.getLinks()
-        linked = EventStore.getLinked()
-    }
-
-    fun findOrphans(children: Set<LogEntry>): Set<String> {
-        console.log("[LED.findOrphans] $href")
-        val kids = children.map { it.url }
-        val orphans = mutableSetOf<String>()
-        links.forEach {
-//            console.log(it)
-            val rel = it.relation()
-            when {
-                (rel == Relation.UP) -> {
-                }
-                (rel == Relation.SELF) -> {
-                }
-                else -> {
-                    val url = it.href
-                    if (!kids.contains(url))
-                        orphans.add(url)
-                }
-            }
-        }
-        return orphans
-    }
+    val href: String = logEntry.selfHref()
+    val links: List<Link> = logEntry.getLinks()
+    val linked: List<LogEntry> = EventStore.getLinked()
 
     fun findChildren(): Set<LogEntry> {
         val children = findChildrenByUpRelation()
@@ -77,7 +48,7 @@ class LogEntryDecorator(val logEntry: LogEntry) {
     }
 
     private fun findChildrenByLinks(): Set<LogEntry> {
-        console.log("[LED.findChildrenByLinks] $href")
+        console.log("[LED.findChildrenByLinks]")
         val children = mutableSetOf<LogEntry>()
         links.forEach {
             console.log(it.toString())
@@ -113,29 +84,30 @@ class LogEntryDecorator(val logEntry: LogEntry) {
         return children
     }
 
-    fun selfType(): String {
-        val selfLink = logEntry.selfLink()
-        if (selfLink != null) {
-            return selfLink.representation().type
-        } else return ""
-    }
-
-    private fun hasUp(): Boolean {
-        links.forEach {
-            if (it.relation() == Relation.UP) {
-                return true
+    fun findChildrenIn(aggregatedList: List<LogEntry>): List<LogEntry> {
+        console.log("[LED.findChildrenIn]")
+        val selfUrl = href
+        val children = mutableListOf<LogEntry>()
+        aggregatedList.forEach {
+            if (it.url != selfUrl && it.response.contains(selfUrl)) {
+                children.add(it)
             }
         }
-        return false
+        return children
     }
 
-    fun hasParent(): Boolean {
-        val answer = hasUp()
-        if (answer) return true
-        return findParent() != null
+    fun selfType(): String {
+        val selfLink = logEntry.selfLink()
+        return if (selfLink != null) {
+            selfLink.representation().type
+        } else {
+            console.log("[LED.selfType]")
+            console.log(logEntry)
+            ""
+        }
     }
 
-    private fun findParent(): LogEntry? {
+    fun findParent(): LogEntry? {
         val url = logEntry.url
         linked.forEach {
             when {
@@ -146,16 +118,4 @@ class LogEntryDecorator(val logEntry: LogEntry) {
         return null
     }
 
-    fun shortTitle(): String {
-        var result = logEntry.url
-        val signature = Constants.restInfix
-        if (logEntry.url.contains(signature)) {
-            // strip off protocol, host, port
-            //           val protocolHostPort = UiManager.getUrl()
-//            result = result.replace(protocolHostPort + signature, "")
-            result = StringUtils.removeHexCode(result)
-        }
-        return result
-    }
-
 }
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/LinkTreeDiagram.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/LinkTreeDiagram.kt
index d49321c..c6f6533 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/LinkTreeDiagram.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/diagram/LinkTreeDiagram.kt
@@ -1,45 +1,107 @@
 package org.apache.isis.client.kroviz.ui.diagram
 
+import org.apache.isis.client.kroviz.core.aggregator.BaseAggregator
 import org.apache.isis.client.kroviz.core.event.EventStore
 import org.apache.isis.client.kroviz.core.event.LogEntry
 import org.apache.isis.client.kroviz.core.event.LogEntryDecorator
+import org.apache.isis.client.kroviz.core.event.ResourceSpecification
+import org.apache.isis.client.kroviz.to.HasLinks
+import org.apache.isis.client.kroviz.ui.core.UiManager
+import org.apache.isis.client.kroviz.utils.StringUtils
 
 object LinkTreeDiagram {
 
-    private val NL = "\n"
-    private val SEP = " | "
-    private val OPEN = "{"
-    private val CLOSE = "}"
-    private val PLUS = "+"
-
-    fun build(): String {
-        var code = "@startsalt$NL$OPEN$NL$OPEN T#$NL"
-        val roots: List<LogEntry> = findRoots()
-        roots.forEach {
-            code += iterateOverChildren(it, PLUS)
+    private const val NL = "\n"
+    private const val prolog = "@startmindmap$NL"
+    private const val epilog = "@endmindmap$NL"
+    private val protocolHostPort = UiManager.getUrl()
+
+    fun build(aggregator: BaseAggregator): String {
+        var code = prolog
+        val entryList: List<LogEntry> = EventStore.findAllBy(aggregator)
+        val root = findRoot(entryList)
+        if (root == null) {
+            code += "* /$NL"
+            code += createNodes(entryList, 2)
+        } else {
+            code += root.asPumlNode(1)
+            val led = LogEntryDecorator(root)
+            val childList = led.findChildrenIn(entryList)
+            console.log("[LTD.build]")
+            console.log(aggregator)
+            console.log(entryList)
+            code += createChildNodes(childList, 2)
+        }
+        code += epilog
+        return code
+    }
+
+    private fun createChildNodes(childList: List<LogEntry>, level: Int): String {
+        var code = ""
+        childList.forEach {
+            code += createNode(it, level)
+            val led = LogEntryDecorator(it)
+            val kidSet = led.findChildrenIn(childList)
+            code += createChildNodes(kidSet, level +1)
         }
-        code += "$CLOSE$NL$CLOSE$NL@endsalt"
         return code
     }
 
-    private fun findRoots(): List<LogEntry> {
-        val rootSet = mutableListOf<LogEntry>()
-        EventStore.getLinked().forEach { le ->
-            val led = LogEntryDecorator(le)
-            if (!led.hasParent()) rootSet.add(le)  // this may still include
+    private fun createNode(le: LogEntry, level: Int): String {
+        var code = ""
+        if (isInEventStore(le.url)) {
+            code += le.asPumlNode(level)
         }
-        return rootSet
+        return code
     }
 
-    private fun iterateOverChildren(logEntry: LogEntry, prefix: String): String {
-        val led = LogEntryDecorator(logEntry)
-        val children = led.findChildren()
-        val orphans = led.findOrphans(children)
-        var code = prefix + " " + led.shortTitle() + SEP + led.selfType() + SEP + orphans.toString() + NL
-        children.forEach {
-            code += iterateOverChildren(it, prefix + PLUS)
+    private fun createNodes(entryList: List<LogEntry>, level: Int): String {
+        var code = ""
+        entryList.forEach {
+            code += createNode(it, level)
         }
         return code
     }
 
+    private fun findRoot(entryList: List<LogEntry>): LogEntry? {
+        entryList.forEach {
+            val led = LogEntryDecorator(it)
+            val parent = led.findParent()
+            if (parent != null && !entryList.contains(parent)) {
+                return parent
+            }
+        }
+        return null
+    }
+
+    private fun isInEventStore(url: String): Boolean {
+        val rs = ResourceSpecification(url)
+        val le = EventStore.findBy(rs)
+        return (le != null)
+    }
+
+    fun LogEntry.asPumlNode(level: Int): String {
+        val led = LogEntryDecorator(this)
+        val title = StringUtils.shortTitle(this.url, protocolHostPort)
+        val type = led.selfType()
+        val depth = "*".repeat(level)
+        var answer = "$depth:..//<<$type>>//..$NL**$title**$NL"
+        answer += "----$NL"
+        answer += traceInfo(this)
+        return answer + ";" + NL
+    }
+
+    private fun traceInfo(logEntry: LogEntry) : String {
+        val obj = logEntry.obj!!
+        var answer = "__" + obj::class.simpleName + "__" + NL
+        if (obj is HasLinks) {
+            obj.links.forEach {
+                answer += StringUtils.shortTitle(it.href, protocolHostPort) + NL
+            }
+        }
+        console.log("[LTD.traceInfo]")
+        console.log(answer)
+        return answer
+    }
+
 }
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/dialog/DiagramDialog.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/dialog/DiagramDialog.kt
index 447b97a..750c3b1 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/dialog/DiagramDialog.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/dialog/DiagramDialog.kt
@@ -56,7 +56,7 @@ class DiagramDialog(
         )
     }
 
-    override fun execute(action:String?) {
+    override fun execute(action: String?) {
         pin()
     }
 
@@ -84,14 +84,35 @@ class DiagramDialog(
 
     fun buildMenu(): List<KvisionHtmlLink> {
         val menu = mutableListOf<KvisionHtmlLink>()
+        menu.add(buildPinAction())
+        menu.add(buildDownloadAction())
+        return menu
+    }
+
+    private fun buildPinAction(): io.kvision.html.Link {
         val action = MenuFactory.buildActionLink(
                 label = "Pin",
                 menuTitle = "Pin")
         action.onClick {
             pin()
         }
-        menu.add(action)
-        return menu
+        return action
+    }
+
+    private fun buildDownloadAction(): io.kvision.html.Link {
+        val action = MenuFactory.buildActionLink(
+                label = "Download",
+                menuTitle = "Download")
+        action.onClick {
+            download()
+        }
+        return action
+    }
+
+    private fun download() {
+        val svgCode = getDiagramCode()
+        DownloadDialog(fileName = "diagram.svg", svgCode).open()
+        dialog.close()
     }
 
 }
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/dialog/EventLogDetail.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/dialog/EventLogDetail.kt
index f73a26a..6eb29f6 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/dialog/EventLogDetail.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/dialog/EventLogDetail.kt
@@ -18,7 +18,6 @@
  */
 package org.apache.isis.client.kroviz.ui.dialog
 
-import org.apache.isis.client.kroviz.core.aggregator.CollectionAggregator
 import org.apache.isis.client.kroviz.core.event.EventStore
 import org.apache.isis.client.kroviz.core.event.LogEntry
 import org.apache.isis.client.kroviz.core.event.LogEntryDecorator
@@ -30,11 +29,13 @@ import org.apache.isis.client.kroviz.ui.core.FormItem
 import org.apache.isis.client.kroviz.ui.core.RoDialog
 import org.apache.isis.client.kroviz.ui.diagram.JsonDiagram
 import org.apache.isis.client.kroviz.ui.diagram.LayoutDiagram
+import org.apache.isis.client.kroviz.ui.diagram.LinkTreeDiagram
 import org.apache.isis.client.kroviz.utils.StringUtils
 import org.apache.isis.client.kroviz.utils.XmlHelper
 
 class EventLogDetail(val logEntryFromTabulator: LogEntry) : Command() {
     private var logEntry: LogEntry
+    private lateinit var dialog: RoDialog
 
     init {
         // For a yet unknown reason, aggregators are not transmitted via tabulator.
@@ -45,7 +46,7 @@ class EventLogDetail(val logEntryFromTabulator: LogEntry) : Command() {
 
     // callback parameter
     private val LOG: String = "log"
-    private val OBJ: String = "obj"
+    private val LNK: String = "lnk"
 
     fun open() {
         val responseStr = if (logEntry.subType == Constants.subTypeJson) {
@@ -58,23 +59,21 @@ class EventLogDetail(val logEntryFromTabulator: LogEntry) : Command() {
         val children = led.findChildren()
         var kids = ""
         children.forEach { kids += it.url + "\n" }
-        var orphans = ""
-        led.findOrphans(children).forEach { orphans += it + "\n" }
         val formItems = mutableListOf<FormItem>()
         formItems.add(FormItem("Url", ValueType.TEXT, logEntry.title))
         formItems.add(FormItem("Response", ValueType.TEXT_AREA, responseStr, 10))
         formItems.add(FormItem("Aggregators", ValueType.TEXT, content = logEntry.aggregators))
         formItems.add(FormItem("Children", ValueType.TEXT_AREA, kids, size = 5))
-        formItems.add(FormItem("Orphans", ValueType.TEXT_AREA, orphans, size = 5))
-        formItems.add(FormItem("Object Diagram", ValueType.BUTTON, null, callBack = this, callBackAction = OBJ))
+        formItems.add(FormItem("Link Tree Diagram", ValueType.BUTTON, null, callBack = this, callBackAction = LNK))
         formItems.add(FormItem("Console", ValueType.BUTTON, null, callBack = this, callBackAction = LOG))
 
-        RoDialog(
+        dialog = RoDialog(
                 caption = "Details :" + logEntry.title,
                 items = formItems,
                 command = this,
                 defaultAction = "Diagram",
-                widthPerc = 60).open()
+                widthPerc = 60)
+        dialog.open()
     }
 
     override fun execute(action: String?) {
@@ -83,8 +82,8 @@ class EventLogDetail(val logEntryFromTabulator: LogEntry) : Command() {
             action == LOG -> {
                 console.log(logEntry)
             }
-            action == OBJ -> {
-                objectDiagram()
+            action == LNK -> {
+                linkTreeDiagram()
             }
             else -> {
                 console.log(logEntry)
@@ -93,18 +92,12 @@ class EventLogDetail(val logEntryFromTabulator: LogEntry) : Command() {
         }
     }
 
-    private fun objectDiagram() {
+    private fun linkTreeDiagram() {
         logEntry.aggregators.forEach {
-            console.log(it)
-            if (it is CollectionAggregator) {
-                val displayModel = it.dpm
-                // https://github.com/moll/json-stringify-safe/blob/master/stringify.js
-                val jsonStr = JSON.stringify(displayModel)
-                console.log(jsonStr)
-                val pumlCode = JsonDiagram.build(jsonStr)
-                DiagramDialog("Object Diagram", pumlCode).open()
-            }
+            val code = LinkTreeDiagram.build(it)
+            DiagramDialog("Link Tree Diagram", code).open()
         }
+        dialog.close()
     }
 
     private fun defaultAction() {
@@ -118,7 +111,7 @@ class EventLogDetail(val logEntryFromTabulator: LogEntry) : Command() {
                 JsonDiagram.build(str)
             else -> "{}"
         }
-        DiagramDialog("Response Diagram", pumlCode).open()
+        DiagramDialog("Layout Diagram", pumlCode).open()
     }
 
 }
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/panel/DynamicMenuBuilder.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/panel/DynamicMenuBuilder.kt
index 055df30..7f01d61 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/panel/DynamicMenuBuilder.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/panel/DynamicMenuBuilder.kt
@@ -25,8 +25,6 @@ import org.apache.isis.client.kroviz.core.event.ResourceProxy
 import org.apache.isis.client.kroviz.to.TObject
 import org.apache.isis.client.kroviz.ui.chart.ChartFactory
 import org.apache.isis.client.kroviz.ui.core.UiManager
-import org.apache.isis.client.kroviz.ui.diagram.LinkTreeDiagram
-import org.apache.isis.client.kroviz.ui.dialog.DiagramDialog
 import org.apache.isis.client.kroviz.ui.dialog.EventExportDialog
 import org.apache.isis.client.kroviz.utils.IconManager
 import org.apache.isis.client.kroviz.utils.StringUtils
@@ -40,7 +38,7 @@ class DynamicMenuBuilder {
             val title = StringUtils.deCamel(it.id)
             val icon = IconManager.find(title)
             val invokeLink = it.getInvokeLink()!!
-            val command =  { ResourceProxy().fetch(invokeLink) }
+            val command = { ResourceProxy().fetch(invokeLink) }
             val me = buildMenuEntry(icon, title, command)
             menu.add(me)
         }
@@ -50,10 +48,6 @@ class DynamicMenuBuilder {
     fun buildTableMenu(table: EventLogTable): dynamic {
         val menu = mutableListOf<dynamic>()
 
-        val a1 = buildMenuEntry("Hierarchy", "Link Tree Diagram",
-                { this.linkTreeDiagram() })
-        menu.add(a1)
-
         val a2 = buildMenuEntry("Export", "Export Events ...", {
             EventExportDialog().open()
         })
@@ -82,13 +76,6 @@ class DynamicMenuBuilder {
         }
     }
 
-    private fun linkTreeDiagram() {
-        val code = LinkTreeDiagram.build()
-        console.log("[DMB.linkTreeDiagram]")
-        console.log(code)
-        DiagramDialog("Link Tree Diagram", code).open()
-    }
-
     private fun downLoadCsv(table: EventLogTable) {
         table.tabulator.downloadCSV("data.csv")
     }
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/utils/StringUtils.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/utils/StringUtils.kt
index 311b8d7..a042665 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/utils/StringUtils.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/utils/StringUtils.kt
@@ -21,6 +21,7 @@ package org.apache.isis.client.kroviz.utils
 import org.apache.isis.client.kroviz.to.Argument
 import org.apache.isis.client.kroviz.to.Link
 import org.apache.isis.client.kroviz.to.TObject
+import org.apache.isis.client.kroviz.ui.core.Constants
 
 object StringUtils {
 
@@ -62,6 +63,17 @@ object StringUtils {
         return if (input == outputWithoutWhiteSpace) input else output
     }
 
+    fun shortTitle(url: String, protocolHostPort: String): String {
+        var title = url
+        val signature = Constants.restInfix
+        if (title.contains(signature)) {
+            // strip off protocol, host, port
+            title = title.replace(protocolHostPort + signature, "")
+            title = StringUtils.removeHexCode(title)
+        }
+        return title
+    }
+
     fun removeHexCode(input: String): String {
         var output = ""
         val list: List<String> = input.split("/")
diff --git a/incubator/clients/kroviz/src/test/kotlin/org/apache/isis/client/kroviz/core/event/LogEntryDecoratorTest.kt b/incubator/clients/kroviz/src/test/kotlin/org/apache/isis/client/kroviz/core/event/LogEntryDecoratorTest.kt
new file mode 100644
index 0000000..2aa8a54
--- /dev/null
+++ b/incubator/clients/kroviz/src/test/kotlin/org/apache/isis/client/kroviz/core/event/LogEntryDecoratorTest.kt
@@ -0,0 +1,23 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.client.kroviz.core.event
+
+class LogEntryDecoratorTest {
+
+}
diff --git a/incubator/clients/kroviz/src/test/kotlin/org/apache/isis/client/kroviz/util/StringUtilsTest.kt b/incubator/clients/kroviz/src/test/kotlin/org/apache/isis/client/kroviz/util/StringUtilsTest.kt
new file mode 100644
index 0000000..ba15e96
--- /dev/null
+++ b/incubator/clients/kroviz/src/test/kotlin/org/apache/isis/client/kroviz/util/StringUtilsTest.kt
@@ -0,0 +1,24 @@
+package org.apache.isis.client.kroviz.util
+
+import org.apache.isis.client.kroviz.ui.core.Constants
+import org.apache.isis.client.kroviz.ui.core.UiManager
+import org.apache.isis.client.kroviz.utils.StringUtils
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class StringUtilsTest {
+    @Test
+    fun testShortTitle() {
+        // given
+        UiManager.login(Constants.demoUrl, Constants.demoUser, Constants.demoPass)
+        val url = "http://localhost:8080/restful/domain-types/demo.JavaLangStrings/collections/entities"
+
+        // when
+        val actual = StringUtils.shortTitle(url, UiManager.getUrl())
+
+        // then
+        val expected = "/domain-types/demo.JavaLangStrings/collections/entities"
+        assertEquals(expected, actual)
+    }
+
+}