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 2020/06/20 20:25:53 UTC

[isis] 02/09: ISIS-2350 RoToolPanel renamed to RoIconBar, styling via css. Factory and action not yet possible at the same time. Chart Sample added. Code cleanup.

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 7e8e4f54a380e6ecd6177d682c3c95c74fef1173
Author: Jörg Rade <jo...@kuehne-nagel.com>
AuthorDate: Wed Jun 17 21:31:42 2020 +0200

    ISIS-2350 RoToolPanel renamed to RoIconBar, styling via css.
    Factory and action not yet possible at the same time.
    Chart Sample added. Code cleanup.
---
 .../isis/client/kroviz/core/event/EventStore.kt    |   9 ++
 .../isis/client/kroviz/core/model/DisplayModel.kt  |   9 --
 .../apache/isis/client/kroviz/ui/kv/ChartTab.kt    | 121 +++++++++++++++++++
 .../isis/client/kroviz/ui/kv/ColumnFactory.kt      |  10 +-
 .../apache/isis/client/kroviz/ui/kv/Constants.kt   |   8 ++
 .../isis/client/kroviz/ui/kv/EventLogTable.kt      |   6 +-
 .../apache/isis/client/kroviz/ui/kv/MenuFactory.kt | 127 +++++++++++++++----
 .../org/apache/isis/client/kroviz/ui/kv/RoApp.kt   |   2 +-
 .../apache/isis/client/kroviz/ui/kv/RoIconBar.kt   | 134 +++++++++++++++++++++
 .../apache/isis/client/kroviz/ui/kv/RoMenuBar.kt   |  17 ++-
 .../isis/client/kroviz/ui/kv/RoSimplePanel.kt      |  11 --
 .../org/apache/isis/client/kroviz/ui/kv/RoTable.kt |   7 +-
 .../apache/isis/client/kroviz/ui/kv/RoToolPanel.kt | 115 ------------------
 .../apache/isis/client/kroviz/ui/kv/UiManager.kt   |   5 +-
 .../apache/isis/client/kroviz/utils/IconManager.kt |  24 ++--
 .../org/apache/isis/client/kroviz/utils/Utils.kt   |  50 ++++----
 .../kroviz/src/main/resources/css/kroviz.css       |  34 +++++-
 17 files changed, 462 insertions(+), 227 deletions(-)

diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/event/EventStore.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/event/EventStore.kt
index eb7cfef..0ac65fc 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/event/EventStore.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/event/EventStore.kt
@@ -2,6 +2,7 @@ package org.apache.isis.client.kroviz.core.event
 
 import org.apache.isis.client.kroviz.core.aggregator.BaseAggregator
 import org.apache.isis.client.kroviz.to.TObject
+import org.apache.isis.client.kroviz.to.mb.Menubars
 import org.apache.isis.client.kroviz.ui.kv.UiManager
 import pl.treksoft.kvision.panel.SimplePanel
 import pl.treksoft.kvision.state.observableListOf
@@ -107,6 +108,14 @@ object EventStore {
         return null
     }
 
+    fun findMenuBars(): LogEntry? {
+        this.log.forEach {
+           if (it.obj is Menubars)
+                return it
+        }
+        return null
+    }
+
     internal fun findExact(reSpec: ResourceSpecification): LogEntry? {
         return log.firstOrNull { it.matches(reSpec) }
     }
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/model/DisplayModel.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/model/DisplayModel.kt
index 9738c4b..045d05e 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/model/DisplayModel.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/core/model/DisplayModel.kt
@@ -22,13 +22,4 @@ abstract class DisplayModel {
         // subclass responsibility
     }
 
-    fun extractTitle(): String {
-        val strList = this.title.split("/")
-        val len = strList.size
-        var answer = ""
-        if (len > 2) answer =
-                strList[len - 2]
-        return answer
-    }
-
 }
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/ChartTab.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/ChartTab.kt
new file mode 100644
index 0000000..4276382
--- /dev/null
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/ChartTab.kt
@@ -0,0 +1,121 @@
+package org.apache.isis.client.kroviz.ui.kv
+
+import pl.treksoft.kvision.chart.*
+import pl.treksoft.kvision.core.Col
+import pl.treksoft.kvision.core.Color
+import pl.treksoft.kvision.i18n.I18n.tr
+import pl.treksoft.kvision.panel.SimplePanel
+import pl.treksoft.kvision.panel.gridPanel
+import pl.treksoft.kvision.utils.obj
+import pl.treksoft.kvision.utils.px
+import kotlin.math.sin
+
+class ChartTab : SimplePanel() {
+    init {
+        this.marginTop = 10.px
+
+        gridPanel(templateColumns = "50% 50%", columnGap = 30, rowGap = 30) {
+            @Suppress("UnsafeCastFromDynamic")
+            chart(
+                    Configuration(
+                            ChartType.SCATTER,
+                            listOf(
+                                    DataSets(
+                                            pointBorderColor = listOf(Color.name(Col.RED)),
+                                            backgroundColor = listOf(Color.name(Col.LIGHTGREEN)),
+                                            data = (-60..60).map {
+                                                obj {
+                                                    x = it.toDouble() / 10
+                                                    y = sin(it.toDouble() / 10)
+                                                }
+                                            }
+                                    )
+                            ),
+                            options = ChartOptions(legend = LegendOptions(display = false), showLines = true)
+                    )
+            )
+            chart(
+                    Configuration(
+                            ChartType.BAR,
+                            listOf(
+                                    DataSets(
+                                            data = listOf(6, 12, 19, 13, 7, 3),
+                                            backgroundColor = listOf(
+                                                    Color.hex(0xC0504D),
+                                                    Color.hex(0xF79646),
+                                                    Color.hex(0x9BBB59),
+                                                    Color.hex(0x4BACC6),
+                                                    Color.hex(0x4F81BD),
+                                                    Color.hex(0x8064A2)
+                                            )
+                                    )
+                            ),
+                            listOf(
+                                    tr("Africa"),
+                                    tr("Asia"),
+                                    tr("Europe"),
+                                    tr("Latin America"),
+                                    tr("North America"),
+                                    tr("Australia")
+                            ),
+                            ChartOptions(legend = LegendOptions(display = false), scales = ChartScales(yAxes = listOf(obj {
+                                ticks = obj {
+                                    suggestedMin = 0
+                                    suggestedMax = 20
+                                }
+                            })), title = TitleOptions(display = false))
+                    )
+            )
+            chart(
+                    Configuration(
+                            ChartType.PIE,
+                            listOf(
+                                    DataSets(
+                                            data = listOf(6, 12, 19, 13, 7, 3),
+                                            backgroundColor = listOf(
+                                                    Color.hex(0xC0504D),
+                                                    Color.hex(0xF79646),
+                                                    Color.hex(0x9BBB59),
+                                                    Color.hex(0x4BACC6),
+                                                    Color.hex(0x4F81BD),
+                                                    Color.hex(0x8064A2)
+                                            )
+                                    )
+                            ), listOf(
+                            tr("Africa"),
+                            tr("Asia"),
+                            tr("Europe"),
+                            tr("Latin America"),
+                            tr("North America"),
+                            tr("Australia")
+                    )
+                    )
+            )
+            chart(
+                    Configuration(
+                            ChartType.POLARAREA,
+                            listOf(
+                                    DataSets(
+                                            data = listOf(6, 12, 19, 13, 7, 3),
+                                            backgroundColor = listOf(
+                                                    Color.hex(0xC0504D),
+                                                    Color.hex(0xF79646),
+                                                    Color.hex(0x9BBB59),
+                                                    Color.hex(0x4BACC6),
+                                                    Color.hex(0x4F81BD),
+                                                    Color.hex(0x8064A2)
+                                            )
+                                    )
+                            ), listOf(
+                            tr("Africa"),
+                            tr("Asia"),
+                            tr("Europe"),
+                            tr("Latin America"),
+                            tr("North America"),
+                            tr("Australia")
+                    )
+                    )
+            )
+        }
+    }
+}
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/ColumnFactory.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/ColumnFactory.kt
index a7cec95..121b871 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/ColumnFactory.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/ColumnFactory.kt
@@ -70,7 +70,7 @@ class ColumnFactory {
         return ColumnDefinition<dynamic>(
                 "",
                 field = "iconName",
-                align = Align.CENTER,
+                hozAlign = Align.CENTER,
                 width = "40",
                 formatterComponentFunction = { _, _, data ->
                     buildButton(data, data["iconName"] as? String)
@@ -83,7 +83,7 @@ class ColumnFactory {
             UiManager.displayModel(tObject)
         }
         val logEntry = EventStore.find(tObject)!!
-        b.setDragDropData(RoToolPanel.format, logEntry.url)
+        b.setDragDropData(Constants.format, logEntry.url)
         return b
     }
 
@@ -113,7 +113,7 @@ class ColumnFactory {
                                        obj {"<i class='fa fa-square-o'></i>"}
                                    }
                                }, */
-                align = Align.CENTER,
+                hozAlign = Align.CENTER,
                 width = "40",
                 headerSort = false,
                 cellClick = { evt, cell ->
@@ -127,7 +127,7 @@ class ColumnFactory {
                 field = "iconName", // any existing field can be used
                 formatter = Formatter.TICKCROSS,
                 formatterParams = menuFormatterParams,
-                align = Align.CENTER,
+                hozAlign = Align.CENTER,
                 width = "60",
                 headerSort = false,
                 formatterComponentFunction = { _, _, data ->
@@ -135,7 +135,7 @@ class ColumnFactory {
                     MenuFactory.buildFor(
                             tObject,
                             false,
-                            ButtonStyle.LINK)
+                            style = ButtonStyle.LINK)
                 })
     }
 
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/Constants.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/Constants.kt
new file mode 100644
index 0000000..4afcbeb
--- /dev/null
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/Constants.kt
@@ -0,0 +1,8 @@
+package org.apache.isis.client.kroviz.ui.kv
+
+object Constants {
+
+    const val format = "text/plain"
+    const val calcHeight = "calc(100vh - 128px)"
+    const val actionSeparator = "\n"
+}
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/EventLogTable.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/EventLogTable.kt
index 06f94f2..7df1a88 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/EventLogTable.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/EventLogTable.kt
@@ -17,8 +17,6 @@ import pl.treksoft.kvision.utils.px
 
 class EventLogTable(val model: List<LogEntry>) : VPanel() {
 
-    private val calcHeight = "calc(100vh - 128px)"
-
     private val columns = listOf(
             ColumnDefinition(
                     title = "",
@@ -67,7 +65,7 @@ class EventLogTable(val model: List<LogEntry>) : VPanel() {
                 style = ButtonStyle.LINK).onClick {
             console.log(data)
         }
-        b.setDragDropData(RoToolPanel.format, data.url)
+        b.setDragDropData(Constants.format, data.url)
         return b
     }
 
@@ -80,7 +78,7 @@ class EventLogTable(val model: List<LogEntry>) : VPanel() {
 
         val options = TabulatorOptions(
                 movableColumns = true,
-                height = calcHeight,
+                height = Constants.calcHeight,
                 layout = Layout.FITCOLUMNS,
                 columns = columns,
                 persistenceMode = false
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/MenuFactory.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/MenuFactory.kt
index ed8d7c7..aa840b6 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/MenuFactory.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/MenuFactory.kt
@@ -1,52 +1,62 @@
 package org.apache.isis.client.kroviz.ui.kv
 
 import org.apache.isis.client.kroviz.core.aggregator.ActionDispatcher
+import org.apache.isis.client.kroviz.core.event.EventStore
 import org.apache.isis.client.kroviz.to.Link
 import org.apache.isis.client.kroviz.to.Member
 import org.apache.isis.client.kroviz.to.TObject
 import org.apache.isis.client.kroviz.to.mb.Menu
+import org.apache.isis.client.kroviz.to.mb.MenuEntry
+import org.apache.isis.client.kroviz.to.mb.Menubars
 import org.apache.isis.client.kroviz.utils.IconManager
 import org.apache.isis.client.kroviz.utils.Utils
 import pl.treksoft.kvision.core.Component
-import pl.treksoft.kvision.dropdown.Direction
 import pl.treksoft.kvision.dropdown.DropDown
-import pl.treksoft.kvision.dropdown.ddLink
 import pl.treksoft.kvision.dropdown.separator
 import pl.treksoft.kvision.html.ButtonStyle
+import pl.treksoft.kvision.utils.set
+import pl.treksoft.kvision.html.Link as KvisionHtmlLink
 
 object MenuFactory {
 
     fun buildFor(tObject: TObject,
                  withText: Boolean = true,
-                 style: ButtonStyle = ButtonStyle.LIGHT): DropDown {
+                 iconName: String = "Actions",
+                 style: ButtonStyle = ButtonStyle.LIGHT)
+            : DropDown {
         val type = tObject.domainType
-        val dd = DropDown(
-                type,
-                style = style,
-                direction = Direction.DROPDOWN
-        )
-        if (withText) {
-            dd.text = "Actions for $type"
-        } else {
-            dd.text = ""
-        }
-        dd.icon = IconManager.find("Actions")
+        val text = if (withText) "Actions for $type" else ""
+        val icon = IconManager.find(iconName)
+        val dd = DropDown(text = text, icon = icon, style = style)
         val actions = tObject.getActions()
         actions.forEach {
             val title = it.id
             val link = it.getInvokeLink()!!
-            action(dd, title, link)
+            val action = buildAction(title, link, text)
+            dd.add(action)
         }
         return dd
     }
 
-    fun buildFor(menu: Menu): DropDown {
-        val title = menu.named
-        val dd = DropDown(text = title, style = ButtonStyle.LIGHT, forNavbar = false)
-        dd.icon = IconManager.find(title)
+    fun buildFor(menu: Menu,
+                 style: ButtonStyle = ButtonStyle.LIGHT,
+                 withText: Boolean = true,
+                 classes: Set<String> = setOf())
+            : DropDown {
+        val menuTitle = menu.named
+        val dd = DropDown(
+                text = if (withText) menuTitle else "",
+                icon = IconManager.find(menuTitle),
+                style = style,
+                classes = classes,
+                forNavbar = false)
+        //dd.setDragDropData(Constants.format, menuTitle)
+        // action.setDragDropData gets always overridden by dd.setDragDropData
         menu.section.forEachIndexed { index, section ->
             section.serviceAction.forEach { sa ->
-                action(dd, sa.id!!, sa.link!!)
+                val action = buildAction(sa.id!!, sa.link!!, menuTitle)
+                action.setDragDropData(Constants.format, action.id!!)
+                dd.add(action)
             }
             if (index < menu.section.size - 1) {
                 dd.separator()
@@ -55,13 +65,70 @@ object MenuFactory {
         return dd
     }
 
-    private fun action(dd: DropDown, label: String, link: Link) {
+    fun buildForTitle(title: String): DropDown? {
+        val menu = findMenuByTitle(title)
+        return if (menu == null) null else
+            buildFor(
+                    menu = menu,
+                    withText = false)
+    }
+
+    private fun findMenuByTitle(title: String): Menu? {
+        val menuBars = EventStore.findMenuBars()!!.obj as Menubars
+        var menu = findMenu(menuBars.primary, title)
+        if (menu == null) menu = findMenu(menuBars.secondary, title)
+        if (menu == null) menu = findMenu(menuBars.tertiary, title)
+        return menu
+    }
+
+    private fun findMenu(menuEntry: MenuEntry, title: String): Menu? {
+        return menuEntry.menu.firstOrNull { it.named == title }
+    }
+
+    fun buildForAction(menuTitle: String, actionTitle: String): KvisionHtmlLink? {
+        val menu = findMenuByTitle(menuTitle)!!
+        menu.section.forEachIndexed { _, section ->
+            section.serviceAction.forEach { sa ->
+                if (actionTitle == sa.named) {
+                    val action = buildAction(sa.id!!, sa.link!!, menuTitle)
+                    action.label = ""
+                    return action
+                }
+            }
+        }
+        return null
+    }
+
+    private fun buildAction(label: String, link: Link, menuTitle: String): KvisionHtmlLink {
         val icon = IconManager.find(label)
         val classes = IconManager.findStyleFor(label)
-        val title = Utils.deCamel(label)
-        dd.ddLink(title, icon = icon, classes = classes).onClick {
+        val actionTitle = Utils.deCamel(label)
+        val action: KvisionHtmlLink = ddLink(actionTitle, icon = icon, classes = classes)
+        action.onClick {
             ActionDispatcher().invoke(link)
         }
+        val title = "$menuTitle${Constants.actionSeparator}$actionTitle"
+        action.setDragDropData(Constants.format, title)
+        action.id = title
+        return action
+    }
+
+    private fun ddLink(
+            label: String,
+            icon: String? = null,
+            classes: Set<String>? = null,
+            init: (KvisionHtmlLink.() -> Unit)? = null
+    ): KvisionHtmlLink {
+        return KvisionHtmlLink(
+                label = label,
+                url = null,
+                icon = icon,
+                image = null,
+                separator = null,
+                labelFirst = true,
+                classes = (classes ?: null.set) + "dropdown-item").apply {
+            init?.invoke(this)
+        }
     }
 
     // initially added items will be enabled
@@ -69,10 +136,18 @@ object MenuFactory {
         dd.separator()
 
         val saveLink = tObject.links.first()
-        action(dd, "save", saveLink)
+        val saveAction = buildAction(
+                label = "save",
+                link = saveLink,
+                menuTitle = tObject.domainType)
+        dd.add(saveAction)
 
         val undoLink = Link(href = "")
-        action(dd, "undo", undoLink)
+        val undoAction = buildAction(
+                label = "undo",
+                link = undoLink,
+                menuTitle = tObject.domainType)
+        dd.add(undoAction)
     }
 
     // disabled when tObject.isClean
@@ -102,7 +177,7 @@ object MenuFactory {
         menuItem.addCssClass(to)
     }
 
-    fun Member.getInvokeLink(): Link? {
+    private fun Member.getInvokeLink(): Link? {
         return links.firstOrNull { it.rel.indexOf(id) > 0 }
     }
 
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/RoApp.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/RoApp.kt
index 5625c95..85fe59c 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/RoApp.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/RoApp.kt
@@ -8,7 +8,7 @@ object RoApp : SimplePanel() {
         this.add(RoMenuBar.navbar)
 
         val view = HPanel()
-        view.add(RoToolPanel.panel)
+        view.add(RoIconBar.panel)
         view.add(RoView.tabPanel)
         this.add(view)
 
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/RoIconBar.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/RoIconBar.kt
new file mode 100644
index 0000000..7a01de3
--- /dev/null
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/RoIconBar.kt
@@ -0,0 +1,134 @@
+package org.apache.isis.client.kroviz.ui.kv
+
+import org.apache.isis.client.kroviz.core.event.EventStore
+import org.apache.isis.client.kroviz.core.event.ResourceSpecification
+import org.apache.isis.client.kroviz.core.model.Exposer
+import org.apache.isis.client.kroviz.to.TObject
+import org.apache.isis.client.kroviz.ui.kv.MenuFactory.buildForTitle
+import org.apache.isis.client.kroviz.utils.IconManager
+import org.apache.isis.client.kroviz.utils.Utils
+import pl.treksoft.kvision.core.CssSize
+import pl.treksoft.kvision.core.UNIT
+import pl.treksoft.kvision.core.Widget
+import pl.treksoft.kvision.dropdown.DropDown
+import pl.treksoft.kvision.html.Button
+import pl.treksoft.kvision.html.ButtonStyle
+import pl.treksoft.kvision.panel.SimplePanel
+import pl.treksoft.kvision.panel.VPanel
+import kotlin.browser.document
+import kotlin.dom.removeClass
+
+object RoIconBar : SimplePanel() {
+
+    val panel = VPanel()
+    private val icons = mutableListOf<SimplePanel>()
+
+    init {
+        panel.addCssClass("icon-bar")
+        panel.title = "Drop objects, factories, or actions here"
+        this.add(createDeleteIcon())
+        panel.setDropTargetData(Constants.format) { id ->
+            when {
+                Utils.isUrl(id!!) ->
+                    this.add(createObjectIcon(id)!!)
+                id.contains(Constants.actionSeparator) ->
+                    this.add(createActionIcon(id))
+                else ->
+                    this.add(createFactoryIcon(id))
+            }
+        }
+        hide()
+    }
+
+    private fun add(icon: SimplePanel) {
+        icons.add(icon)
+        panel.add(icon)
+    }
+
+    fun toggle() {
+        if (panel.width?.first == 0) show() else hide()
+    }
+
+    override fun hide(): Widget {
+        panel.width = CssSize(0, UNIT.px)
+        panel.removeAll()
+        return super.hide()
+    }
+
+    override fun show(): Widget {
+        panel.width = CssSize(40, UNIT.px)
+        icons.forEach { panel.add(it) }
+        return super.show()
+    }
+
+    private fun createDeleteIcon(): Button {
+        val del = Button(
+                text = "",
+                icon = IconManager.find("Delete"),
+                style = ButtonStyle.LIGHT).apply {
+            padding = CssSize(-16, UNIT.px)
+            margin = CssSize(0, UNIT.px)
+            title = "Drop icon here in order to remove it"
+        }
+        del.setDropTargetData(Constants.format) {
+            icons.forEach { ii ->
+                if (ii.id == it) {
+                    icons.remove(ii)
+                    panel.remove(ii)
+                }
+            }
+        }
+        return del
+    }
+
+    private fun createObjectIcon(url: String): DropDown? {
+        val reSpec = ResourceSpecification(url)
+        val logEntry = EventStore.find(reSpec)!!
+        val obj = logEntry.obj!!
+        return if (obj is TObject) {
+            val exp = Exposer(obj)
+            val ed = exp.dynamise()
+            val hasIconName = ed.hasOwnProperty("iconName") as Boolean
+            val iconName = if (hasIconName) (ed["iconName"] as String) else ""
+
+            val icon = MenuFactory.buildFor(
+                    tObject = obj,
+                    iconName = iconName,
+                    withText = false)
+            val title = Utils.extractTitle(logEntry.title)
+            initIcon(icon, url, title, "icon-bar-object", icon.buttonId()!!)
+            icon
+        } else null
+    }
+
+    private fun createActionIcon(id: String): SimplePanel {
+        val titles = id.split(Constants.actionSeparator)
+        val menuTitle = titles[0]
+        val actionTitle = titles[1]
+        val icon = MenuFactory.buildForAction(menuTitle, actionTitle)!!
+        return initIcon(icon, id, id, "icon-bar-action", icon.id!!)
+    }
+
+    private fun createFactoryIcon(id: String): SimplePanel {
+        val icon = buildForTitle(id)!!
+        return initIcon(icon, id, id, "icon-bar-factory", icon.buttonId()!!)
+    }
+
+    private fun initIcon(icon: SimplePanel,
+                         id: String,
+                         title: String,
+                         cssClass: String,
+                         btnId: String)
+            : SimplePanel {
+        icon.setDragDropData(Constants.format, id)
+        icon.id = id
+        icon.title = title
+        icon.addCssClass(cssClass)
+        icon.afterInsertHook = {
+            val btn = document.getElementById(btnId)!!
+            btn.removeClass("dropdown-toggle")
+        }
+        return icon
+    }
+
+}
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/RoMenuBar.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/RoMenuBar.kt
index c519fcf..8aca267 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/RoMenuBar.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/RoMenuBar.kt
@@ -36,10 +36,9 @@ object RoMenuBar : SimplePanel() {
     }
 
     private fun buildMainMenu(): DropDown {
-        val iconName = IconManager.find("Burger")
         return dropDown(
                 "",
-                icon = iconName,
+                icon = IconManager.find("Burger"),
                 forNavbar = false,
                 style = ButtonStyle.LIGHT)
         {
@@ -51,11 +50,11 @@ object RoMenuBar : SimplePanel() {
                 LoginPrompt().open(at)
             }
 
-            val toolTitle = "Toolbox"
+            val toolTitle = "Toolbar"
             ddLink(toolTitle,
                     icon = IconManager.find(toolTitle)
             ).onClick {
-                RoToolPanel.toggle()
+                RoIconBar.toggle()
             }
 
             val sampleTitle = "History"
@@ -72,13 +71,21 @@ object RoMenuBar : SimplePanel() {
             ).onClick {
                 ExportDialog().open()
             }
+
+            val chartTitle = "Sample Chart"
+            ddLink(chartTitle,
+                    icon = IconManager.find("Chart")
+            ).onClick {
+                UiManager.add(chartTitle, ChartTab())
+            }
         }
     }
 
     fun amendMenu(menuBars: Menubars) {
         logoButton()
         menuBars.primary.menu.forEach { m ->
-            nav.add(MenuFactory.buildFor(m))
+            val dd = MenuFactory.buildFor(m)
+            if (dd.getChildren().isNotEmpty()) nav.add(dd)
         }
         nav.add(MenuFactory.buildFor(menuBars.secondary.menu.first()))
         nav.add(MenuFactory.buildFor(menuBars.tertiary.menu.first()))
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/RoSimplePanel.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/RoSimplePanel.kt
deleted file mode 100644
index ee03e52..0000000
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/RoSimplePanel.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package org.apache.isis.client.kroviz.ui.kv
-
-import org.apache.isis.client.kroviz.core.model.DisplayModel
-import pl.treksoft.kvision.panel.SimplePanel
-
-class RoSimplePanel(displayable: DisplayModel) : SimplePanel() {
-    init {
-        title = displayable.extractTitle()
-    }
-
-}
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/RoTable.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/RoTable.kt
index 8c6f071..356f0cb 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/RoTable.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/RoTable.kt
@@ -2,6 +2,7 @@ package org.apache.isis.client.kroviz.ui.kv
 
 import org.apache.isis.client.kroviz.core.model.Exposer
 import org.apache.isis.client.kroviz.core.model.ListDM
+import org.apache.isis.client.kroviz.utils.Utils
 import pl.treksoft.kvision.core.CssSize
 import pl.treksoft.kvision.core.UNIT
 import pl.treksoft.kvision.panel.SimplePanel
@@ -19,10 +20,8 @@ import pl.treksoft.kvision.tabulator.tabulator
  */
 class RoTable(displayList: ListDM) : SimplePanel() {
 
-    private val calcHeight = "calc(100vh - 128px)"
-
     init {
-        title = displayList.extractTitle()
+        title = Utils.extractTitle(displayList.title)
         width = CssSize(100, UNIT.perc)
         val model = displayList.data
         val columns = ColumnFactory().buildColumns(
@@ -30,7 +29,7 @@ class RoTable(displayList: ListDM) : SimplePanel() {
                 true)
         val options = TabulatorOptions(
                 movableColumns = true,
-                height = calcHeight,
+                height = Constants.calcHeight,
                 layout = Layout.FITCOLUMNS,
                 columns = columns,
                 persistenceMode = false//,
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/RoToolPanel.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/RoToolPanel.kt
deleted file mode 100644
index a7289ee..0000000
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/RoToolPanel.kt
+++ /dev/null
@@ -1,115 +0,0 @@
-package org.apache.isis.client.kroviz.ui.kv
-
-import kotlinx.serialization.UnstableDefault
-import org.apache.isis.client.kroviz.core.event.EventStore
-import org.apache.isis.client.kroviz.core.event.ResourceSpecification
-import org.apache.isis.client.kroviz.core.model.Exposer
-import org.apache.isis.client.kroviz.to.TObject
-import org.apache.isis.client.kroviz.ui.BrowserWindow
-import org.apache.isis.client.kroviz.utils.IconManager
-import pl.treksoft.kvision.core.*
-import pl.treksoft.kvision.html.Button
-import pl.treksoft.kvision.html.ButtonStyle
-import pl.treksoft.kvision.panel.SimplePanel
-import pl.treksoft.kvision.panel.VPanel
-
-@OptIn(UnstableDefault::class)
-object RoToolPanel : SimplePanel() {
-
-    const val format = "text/plain"
-    val panel = VPanel()
-    private val buttons = mutableListOf<Button>()
-
-    init {
-        panel.marginTop = CssSize(40, UNIT.px)
-        panel.width = CssSize(40, UNIT.px)
-        panel.height = CssSize(100, UNIT.perc)
-        panel.background = Background(color = Color.name(Col.GHOSTWHITE))
-        panel.setDropTargetData(format) { url ->
-            val reSpec = ResourceSpecification(url!!)
-            val logEntry = EventStore.find(reSpec)!!
-            val obj = logEntry.obj!!
-            if (obj is TObject) {
-                val exp = Exposer(obj)
-                addButton(exp)
-            }
-        }
-
-        initButtons()
-        panel.addAll(buttons)
-    }
-
-    private fun initButtons() {
-        val drop: Button = buildButton("Toolbox", "Sample drop target")
-        drop.setDropTarget(format) {
-            //IMPROVE use string for wikipedia search
-            BrowserWindow("http://isis.apache.org").open()
-        }
-        buttons.add(drop)
-        //
-        val drag = buildButton("Object", "Sample drag object")
-        drag.setDragDropData(format, "element")
-        buttons.add(drag)
-    }
-
-    fun toggle() {
-        if (panel.width?.first == 0) show() else hide()
-    }
-
-    override fun hide(): Widget {
-        panel.width = CssSize(0, UNIT.px)
-        panel.removeAll()
-        return super.hide()
-    }
-
-    override fun show(): Widget {
-        panel.width = CssSize(40, UNIT.px)
-        buttons.forEach { panel.add(it) }
-        return super.show()
-    }
-
-    private fun addButton(exp: Exposer) {
-        var iconName = ""
-        val ed = exp.dynamise()
-        if (ed.hasOwnProperty("iconName") as Boolean) {
-            iconName = ed["iconName"] as String
-        }
-        val b = buildButton(iconName, "dynamic sample")
-        val tObject = exp.delegate
-        val m = MenuFactory.buildFor(
-                tObject,
-                false,
-                ButtonStyle.LINK)
-        console.log("[RoToolPanel.addButton]")
-        console.log(exp)
-        console.log(m)
-        b.apply {
-            onEvent {
-                dblclick = {
-                    console.log("dblclick")
-                    m.toggle()
-                    m.show()
-                }
-            }
-        }
-        buttons.add(b)
-        panel.add(b)
-    }
-
-    private fun buildButton(iconName: String, toolTip: String): Button {
-        val icon =
-                if (iconName.startsWith("fa")) iconName else {
-                    IconManager.find(iconName)
-                }
-        val b = Button(
-                text = "",
-                icon = icon,
-                style = ButtonStyle.LINK).apply {
-            padding = CssSize(-16, UNIT.px)
-            margin = CssSize(0, UNIT.px)
-            title = toolTip
-        }
-        return b
-    }
-
-}
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/UiManager.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/UiManager.kt
index 2ccfcc0..290ed5b 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/UiManager.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/ui/kv/UiManager.kt
@@ -11,6 +11,7 @@ import org.apache.isis.client.kroviz.core.model.ListDM
 import org.apache.isis.client.kroviz.core.model.ObjectDM
 import org.apache.isis.client.kroviz.to.TObject
 import org.apache.isis.client.kroviz.to.mb.Menubars
+import org.apache.isis.client.kroviz.utils.Utils
 import org.w3c.dom.events.KeyboardEvent
 import pl.treksoft.kvision.core.Component
 import pl.treksoft.kvision.core.Widget
@@ -82,7 +83,7 @@ object UiManager {
 
     fun openListView(aggregator: BaseAggregator) {
         val displayable = aggregator.dsp
-        val title: String = displayable.extractTitle()
+        val title: String = Utils.extractTitle(displayable.title)
         val panel = RoTable(displayable as ListDM)
         add(title, panel, aggregator)
         displayable.isRendered = true
@@ -90,7 +91,7 @@ object UiManager {
 
     fun openObjectView(aggregator: ObjectAggregator) {
         val dm = aggregator.dsp as ObjectDM
-        var title: String = dm.extractTitle()
+        var title: String = Utils.extractTitle(dm.title)
         if (title.isEmpty()) {
             title = aggregator.actionTitle
         }
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/utils/IconManager.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/utils/IconManager.kt
index 86fe7e6..4a949af 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/utils/IconManager.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/utils/IconManager.kt
@@ -1,21 +1,22 @@
 package org.apache.isis.client.kroviz.utils
 
 object IconManager {
-    val PREFIX = "fas fa-"
-    val DEFAULT_ICON = PREFIX + "bolt"
+    private const val PREFIX = "fas fa-"
+    const val DEFAULT_ICON = PREFIX + "bolt"
 
     const val DANGER = "text-danger"
     const val DISABLED = "text-disabled"
-    const val NORMAL = "text-normal"
+    private const val NORMAL = "text-normal"
     const val OK = "text-ok"
     const val WARN = "text-warn"
 
     /* Merge with configuration values*/
-    val word2Icon = mapOf<String, String>(
+    private val word2Icon = mapOf(
             "All" to "asterisk",
             "Actions" to "ellipsis-v",
             "Blobs" to "cloud",
             "Burger" to "bars",
+            "Chart" to "magic",
             "Close" to "times",
             "Configuration" to "wrench",
             "Connect" to "plug",
@@ -59,7 +60,7 @@ object IconManager {
             "Target" to "bullseye",
             "Text" to "font",
             "Toast" to "bread-slice", //comment-alt-plus/minus/exclamation
-            "Toolbox" to "magic",
+            "Toolbar" to "step-backward",
             "Tooltips" to "comment-alt",
             "Temporals" to "clock",
             "Trees" to "tree",
@@ -69,10 +70,11 @@ object IconManager {
             "Wikipedia" to "wikipedia-w")
 
     fun find(query: String): String {
+        if (query.startsWith("fa")) return query
         val actionTitle = Utils.deCamel(query)
         val mixedCaseList = actionTitle.split(" ")
         for (w in mixedCaseList) {
-            val hit = word2Icon.get(w)
+            val hit = word2Icon[w]
             if (hit != null) {
                 return PREFIX + hit
             }
@@ -81,11 +83,11 @@ object IconManager {
     }
 
     fun findStyleFor(actionName: String): Set<String> {
-        when {
-            actionName == "delete" -> return setOf(DANGER)
-            actionName == "undo" -> return setOf(WARN)
-            actionName == "save" -> return setOf(OK)
-            else -> return setOf(NORMAL)
+        return when (actionName) {
+            "delete" -> setOf(DANGER)
+            "undo" -> setOf(WARN)
+            "save" -> setOf(OK)
+            else -> setOf(NORMAL)
         }
     }
 
diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/utils/Utils.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/utils/Utils.kt
index 1dbd0d7..d8e9891 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/utils/Utils.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/isis/client/kroviz/utils/Utils.kt
@@ -10,7 +10,7 @@ object Utils {
         var output = ""
         val words = input.split(" ")
         for (w in words) {
-            output = output + w.capitalize()
+            output += w.capitalize()
         }
         return decapitalize(output)
     }
@@ -22,26 +22,18 @@ object Utils {
 
     fun deCamel(input: String): String {
         var output = ""
-        var i = 0
-        for (c in input) {
+        for ((i, c) in input.withIndex()) {
+            val cuc = c.toUpperCase()
             if (i == 0) {
-                output += c.toUpperCase()
+                output += cuc
             } else {
-                val o = if (c.toUpperCase() == c) {
-                    " $c"
-                } else {
-                    c.toString()
-                }
+                val o = if (cuc == c) " $c" else c.toString()
                 output += o
             }
-            i++
         }
         // Skip acronyms like OK, USA
         val outputWithoutWhiteSpace = output.replace("\\s".toRegex(), "")
-        if (input.equals(outputWithoutWhiteSpace)) {
-            return input
-        }
-        return output
+        return if (input == outputWithoutWhiteSpace) input else output
     }
 
     fun removeHexCode(input: String): String {
@@ -50,11 +42,7 @@ object Utils {
         //split string by "/" and remove parts longer than 40chars
         list.forEach { s ->
             output += "/"
-            output += if (s.length > 40) {
-                "..."
-            } else {
-                s
-            }
+            output += if (s.length > 40) "..." else s
         }
         return output
     }
@@ -102,9 +90,7 @@ object Utils {
             start: String,
             sep: String,
             end: String): String {
-        if (args.isNullOrEmpty()) {
-            return ""
-        } else {
+        return if (args.isNullOrEmpty()) "" else {
             var answer = start
             args.forEach { kv ->
                 val arg = kv.value!!
@@ -112,7 +98,7 @@ object Utils {
             }
             val len = answer.length
             answer = answer.replaceRange(len - 1, len, end)
-            return answer
+            answer
         }
     }
 
@@ -121,9 +107,7 @@ object Utils {
             start: String,
             sep: String,
             end: String): String {
-        if (args.isNullOrEmpty()) {
-            return ""
-        } else {
+        return if (args.isNullOrEmpty()) "" else {
             var answer = start
             args.forEach { kv ->
                 val arg = kv.value!!
@@ -131,13 +115,13 @@ object Utils {
             }
             val len = answer.length
             answer = answer.replaceRange(len - 1, len, end)
-            return answer
+            answer
         }
     }
 
     internal fun asBody(arg: Argument): String {
         var v = arg.value!!
-        val isHttp = v.startsWith("http")
+        val isHttp = isUrl(v)
         v = quote(v)
         if (isHttp) {
             v = enbrace("href", v)
@@ -145,6 +129,10 @@ object Utils {
         return quote(arg.key) + ": " + enbrace("value", v)
     }
 
+    fun isUrl(s: String): Boolean {
+        return s.startsWith("http")
+    }
+
     private fun enbrace(k: String, v: String): String {
         return "{" + quote(k) + ": " + v + "}"
     }
@@ -158,4 +146,10 @@ object Utils {
         return JSON.stringify(s1, null, 2)
     }
 
+    fun extractTitle(title: String): String {
+        val strList = title.split("/")
+        val len = strList.size
+        return if (len > 2) strList[len - 2] else ""
+    }
+
 }
diff --git a/incubator/clients/kroviz/src/main/resources/css/kroviz.css b/incubator/clients/kroviz/src/main/resources/css/kroviz.css
index cc16a33..b2e24ed 100644
--- a/incubator/clients/kroviz/src/main/resources/css/kroviz.css
+++ b/incubator/clients/kroviz/src/main/resources/css/kroviz.css
@@ -71,7 +71,7 @@ label.control-label {
 }
 
 pre.highlight {
-    margin: 7px 0px;
+    margin: 7px 0;
     padding: 14px;
     background: #FDF6E3;
 }
@@ -118,7 +118,7 @@ textarea {
 }
 
 div {
-   /* font-family: Chicago, sans-serif;*/
+    /* font-family: Chicago, sans-serif;*/
 }
 
 .logo-button-image:before {
@@ -128,10 +128,10 @@ div {
     display: inline-block;
     vertical-align: text-top;
     background-color: transparent;
-    background-position : center center;
-    background-repeat:no-repeat;
+    background-position: center center;
+    background-repeat: no-repeat;
     background-size: contain, cover;
-    background-image : url("https://svn.apache.org/repos/asf/comdev/project-logos/originals/isis.svg");
+    background-image: url("https://svn.apache.org/repos/asf/comdev/project-logos/originals/isis.svg");
 }
 
 /*background-image: url("https://www.apache.org/img/ASF20thAnniversary.jpg");*/
@@ -142,5 +142,27 @@ https://svn.apache.org/repos/asf/comdev/project-logos/originals/isis.svg
 */
 
 .kv-window {
-  /* background-color: rgba(255,255,255, 0) !important;*/
+    /* background-color: rgba(255,255,255, 0) !important;*/
+}
+
+.icon-bar-object > .btn {
+    color: #007bff;
+}
+
+.icon-bar-factory > .btn {
+    color: green;
+}
+
+.icon-bar-action {
+    color: orange !important;
+    padding-left: 12px !important;
+    padding-right: 12px !important;
+}
+
+.icon-bar {
+    margin-top: 40px;
+    width: 40px;
+    height: calc(100vh - 80px);
+    scroll-behavior: auto;
+    background: GHOSTWHITE;
 }