Source

client/ui/data/dashboard.js

/** 
 * 
 * The **Dashboard** derives from [DataComponent](kiss.ui.DataComponent.html).
 * 
 * A dashboard is a group of charts that are displayed together in a single screen.
 * 
 * @param {object} config
 * @param {string} config.name - The dashboard title. We use the "name" property instead of "title" to be consistent with all view names.
 * @param {Collection} config.collection - The data source collection
 * @param {object} [config.record] - Record to persist the view configuration into the db
 * @param {boolean} [config.canEditView] - true if the user can edit the view setup. Default is true.
 * @param {boolean} [config.useCDN] - Set to false to use the local version of ChartJS. Default is true.
 * @returns this
 * 
 * ## Generated markup
 * ```
 * <a-dashboard class="a-dashboard">
 *      <div class="dashboard-header">
 *          <div class="dashboard-title">
 *              <!-- Dashboard title -->
 *          </div>
 *          <div class="dashboard-toolbar">
 *              <!-- Dashboard toolbar items -->
 *          </div>
 *      </div>
 *      <div class="dashboard-container">
 *          <!-- Embedded charts -->
 *      </div>
 * </a-dashboard>
 * ```
 */
kiss.ui.Dashboard = class Dashboard extends kiss.ui.DataComponent {
    /**
     * Its a Custom Web Component. Do not use the constructor directly with the **new** keyword.
     * Instead, use one of the following methods:
     * 
     * Create the Web Component and call its **init** method:
     * ```
     * const myDashboard = document.createElement("a-dashboard").init(config)
     * ```
     * 
     * Or use the shorthand for it:
     * ```
     * const myDashboard = createDashboard({
     *   id: "my-dashboard",
     *   collection: kiss.app.collections["opportunity"]
     * })
     * 
     * myDashboard.render()
     * ```
     */
    constructor() {
        super()
    }

    /**
     * Generates a Dashboard from a JSON config
     * 
     * @ignore
     * @param {object} config - JSON config
     * @returns {HTMLElement}
     */
    init(config) {
        // This component must be resized with its parent container
        config.autoSize = true

        // Init the parent DataComponent
        super.init(config)

        // Options
        this.color = config.color || "#00aaee"
        this.canEditView = (config.canEditView === false) ? false : true
        this.useCDN = (config.useCDN === false) ? false : true
        this.CHART_TOP_SPACE = 13 // Busy space at the top of each chart, in rem

        // Build dashboard skeletton markup
        let id = this.id
        this.innerHTML = /*html*/
            `<div class="dashboard-container">
                <div class="dashboard-header">
                    <div class="dashboard-title">
                        ${this.name || ""}
                    </div>
                    <div style="flex: 1"></div>
                    <div id="dashboard-toolbar:${id}" class="dashboard-toolbar">
                        <div id="setup:${id}"></div>
                    </div>
                </div>
                <div id="dashboard-groups:${id}" class="dashboard-groups"></div>
            </div>`.removeExtraSpaces()

        // Set dashboard components
        this.header = this.querySelector(".dashboard-header")
        this.headerTitle = this.querySelector(".dashboard-title")
        this.toolbar = this.querySelector(".dashboard-toolbar")
        this.dashboardGroups = this.querySelector(".dashboard-groups")

        this._initClickEvents()
        this._initSubscriptions()

        return this
    }

    /**
     * Load the dashboard
     * 
     * @ignore
     */
    async load() {
        await this.collection.find()
        this._render()
    }

    /**
     * Render the dashboard
     * 
     * @private
     * @ignore
     * @returns this
     */
    _render() {
        let isFirstGroup = false
        this.dashboardGroups.innerHTML = ""
        let items = this.record.config || []

        // Init an empty group if no items are found
        if (items.length == 0) {
            isFirstGroup = true
            const groupId = "dashboard-group:" + uid()
            const viewId = uid()

            items.push({
                id: groupId,
                class: "dashboard-group",
                height: `calc(calc(calc(100vh - ${this.CHART_TOP_SPACE}rem) * 0.5) - 1px)`, // 50% of the container's available height
                items: [{
                    id: viewId,
                    type: "chartview",
                    chartType: "bar",
                    dashboard: true,
                    name: txtTitleCase("untitled"),
                    color: this.color,
                    collection: this.collection.clone("memory"),
                    useCDN: this.useCDN
                }]
            })
        }

        // Hide the setup button of each chart if the user cannot update the dashboard
        if (!this.canEditView) {
            items.forEach(group => {
                group.items.forEach(item => {
                    if (item.type == "chartview") {
                        item.showActions = false
                    }
                })
            })
        }

        // Constrain CDN parameter of each chart to reflect the dashboard
        items.forEach(group => {
            group.items.forEach(item => {
                if (item.type == "chartview") {
                    item.useCDN = this.useCDN
                }
            })
        })

        // Inject the new group
        this.dashboardGroups.appendChild(
            createBlock({
                id: "dashboard-groups-container:" + this.id,
                width: "100%",
                flex: 1,
                overflow: "auto",
                items
            }
        ).render())

        // Add buttons to manage the dashboard (= add a chart to a group)
        this._manageDashboardButtons()

        // Enable drag and drop
        this._enableDragAndDrop()

        // Adjust buttons color
        this.setColor(this.color)

        // Show the quick tips for the 1st created group
        if (isFirstGroup) this._showQuickTips()

        return this
    }

    /**
     * Set the dashboard title
     * 
     * @param {string} newTitle 
     */
    setTitle(newTitle) {
        this.headerTitle.innerHTML = newTitle
    }    

    /**
     * Set the color of the dashboard (mainly buttons at the moment)
     * 
     * @param {string} color 
     * @returns this
     */
    setColor(color) {
        const charts = this._getCharts()
        for (const chart of charts) {
            chart.setColor(color)
        }
        return this
    }

    /**
     * Add a group of charts to the dashboard
     * 
     * @async
     * @returns this
     */
    async addGroup() {
        const newGroupId = uid()
        const newGroup = createBlock({
            id: "dashboard-group:" + newGroupId,
            class: "dashboard-group",
            items: []
        }).render()

        const groupsContainer = this._getGroupsContainer()
        groupsContainer.appendChild(newGroup)
        await this.saveConfig()

        const escapedId = CSS.escape("dashboard-group:" + newGroupId)
        kiss.tools.waitForElement("#" + escapedId).then(() => {
            $("dashboard-group:" + newGroupId).scrollIntoView({
                behavior: "smooth"
            })
        })

        this.addChart("dashboard-group:" + newGroupId)
        this._manageDashboardButtons()
        this._enableDragAndDrop()

        return this
    }

    /**
     * Delete a group of charts from the dashboard
     * 
     * @param {string} groupId - The group ID to delete
     */
    async deleteGroup(groupId) {
        createDialog({
            title: txtTitleCase("delete this row"),
            icon: "fas fa-trash",
            headerBackgroundColor: "var(--red)",
            message: txtTitleCase("#delete row"),
            action: async () => {
                const groupsContainer = this._getGroupsContainer()
                groupsContainer.deleteItem(groupId)
                await this.saveConfig()
                this._render()
            }
        })
    }

    /**
     * Move a group up in the dashboard
     * 
     * @param {string} groupId 
     * @returns this
     */
    moveGroupUp(groupId) {
        const group = $(groupId)
        if (!group) return
    
        const previousGroup = group.previousElementSibling
        if (!previousGroup || !previousGroup.classList.contains("dashboard-group")) return // Already at the top
    
        previousGroup.before(group)
        this.saveConfig()
        return this
    }

    /**
     * Move a group down in the dashboard
     * 
     * @param {string} groupId 
     * @returns this
     */
    moveGroupDown(groupId) {
        const group = $(groupId)
        if (!group) return
    
        const nextGroup = group.nextElementSibling
        if (!nextGroup || !nextGroup.classList.contains("dashboard-group")) return // Already at the bottom
    
        nextGroup.after(group)
        this.saveConfig()
        return this
    }

    /**
     * Checks whether a group can be moved up or down.
     *
     * @param {string} groupId - The ID of the group to check.
     * @returns {object} - An object with `up` and `down` properties indicating the possible moves.
     */
    checkGroupMove(groupId) {
        const group = $(groupId)
        if (!group) return false

        const previousGroup = group.previousElementSibling;
        const nextGroup = group.nextElementSibling;

        return {
            up: !!(previousGroup && previousGroup.classList.contains("dashboard-group")),
            down: !!(nextGroup && nextGroup.classList.contains("dashboard-group")),
        }
    }

    /**
     * Get the groups of the dashboard
     * 
     * @returns {HTMLElement[]} - The groups of charts (1 group = 1 row in the dashboard)
     */
    getGroups() {
        return Array.from(this.querySelectorAll(".dashboard-group"))
    }

    /**
     * Get the number of groups in the dashboard
     * 
     * @returns {number} - The number of groups
     */
    getGroupCount() {
        return this.getGroups().length
    }

    /**
     * Get the charts of a group
     * 
     * @param {string} groupId 
     * @returns {HTMLElement[]} - The charts of the group
     */
    getGroupCharts(groupId) {
        return Array.from($(groupId).querySelectorAll(".a-chartview"))
    }

    /**
     * Get a chart from its ID
     * 
     * @param {string} chartId
     * @returns {HTMLElement} - The chart element
     */
    getChart(chartId) {
        const dashboardGroups = $("dashboard-groups:" + this.id)
        let chart = null

        for (const dashboardGroup of dashboardGroups.firstChild.items) {
            chart = dashboardGroup.items.find(item => item.id == chartId)
            if (chart) break
        }
        return chart
    }

    /**
     * Add a chart to a group of charts
     * 
     * @async
     * @param {string} groupId 
     * @returns this
     */
    async addChart(groupId) {
        const viewId = uid()
        const dashboardGroup = $(groupId)

        dashboardGroup.insertItem({
            id: viewId,
            type: "chartview",
            chartType: "bar",
            dashboard: true,
            name: txtTitleCase("untitled"),
            color: this.color,
            collection: this.collection.clone("memory"),
            useCDN: this.useCDN
        }, dashboardGroup.items.length - 1)

        this._manageDashboardButtons()
        this._enableDragAndDrop()
        
        await this.saveConfig()
        return this
    }

    /**
     * Move a chart to another position in the dashboard
     * 
     * @param {string} chartId 
     * @param {string} targetChartId
     * @returns this
     */
    moveChart(chartId, targetChartId) {
        const sourceChart = $(chartId)
        const targetChart = $(targetChartId)
        const sourceGroup = $(chartId).closest(".dashboard-group")
        const targetGroup = $(targetChartId).closest(".dashboard-group")
        
        // Case 1: same group
        if (sourceGroup.id == targetGroup.id) {
            const sourceIndex = this.getChartIndexInGroup(chartId)
            const targetIndex = this.getChartIndexInGroup(targetChartId)

            if (sourceIndex < targetIndex) {
                targetChart.after(sourceChart)
            } else {
                targetChart.before(sourceChart)
            }

            this.saveConfig()
            return this
        }

        // Case 2: different groups
        const chartsInTargetGroup = targetGroup.querySelectorAll(".a-chartview").length

        if (chartsInTargetGroup >= 4) {
            return createNotification(txtTitleCase("this row is full"))
        }

        const targetIndex = [...targetGroup.children].indexOf(targetChart)

        if (targetIndex !== -1) {
            targetChart.before(sourceChart)
        } else {
            targetGroup.appendChild(sourceChart)
        }

        // Recompute the size of the charts
        sourceChart.updateSize()
        this._updateGroupLayout(sourceGroup)

        // If the source group is empty, remove it
        if (sourceGroup.children.length == 1) {
            sourceGroup.deepDelete()
        }

        this.saveConfig()
        return this
    }    

    /**
     * Remove a chart from the dashboard
     * 
     * @async
     * @param {string} chartId - The chart ID to remove
     * @returns this
     */
    async deleteChart(chartId) {
        const dashboardGroup = $(chartId).closest(".dashboard-group")
        dashboardGroup.deleteItem(chartId)
        this._manageDashboardButtons()
        await this.saveConfig()
        return this
    }

    /**
     * Get the index of a chart in a group
     * 
     * @param {string} chartId 
     * @returns {number} - The chart index in the group
     */
    getChartIndexInGroup(chartId) {
        const sourceChart = $(chartId)
        const sourceGroup = $(chartId).closest(".dashboard-group")
        return [...sourceGroup.children].indexOf(sourceChart)
    }    

    /**
     * Get the current dashboard configuration
     * 
     * @returns {object[]} - The dashboard configuration, as an array of groups of charts
     */
    getConfig() {
        const dashboardGroupsContainer = $("dashboard-groups:" + this.id)
        let dashboardGroups = []

        const dashboardGroupElements = Array.from(dashboardGroupsContainer.firstChild.children)
        dashboardGroupElements.forEach(dashboardGroup => {
            let group = {
                id: dashboardGroup.id,
                height: dashboardGroup.config.height,
                class: "dashboard-group",
            }

            let items = []
            const groupItems = [...dashboardGroup.children]
            groupItems.forEach(item => {
                if (item.type == "chartview") {
                    let newItem = {
                        type: "chartview",
                        dashboard: true,
                        id: item.id,
                        name: item.name,
                        modelId: item.model.id,
                        chartType: item.chartType,
                        sort: item.sort,
                        filter: item.filter,
                        isTimeSeries: item.isTimeSeries,
                        categoryField: item.categoryField,
                        timeField: item.timeField,
                        timePeriod: item.timePeriod,
                        operationType: item.operationType,
                        summaryOperation: item.summaryOperation,
                        valueField: item.valueField,
                        startAtZero: item.startAtZero,
                        showLegend: item.showLegend,
                        legendPosition: item.legendPosition,
                        showValues: item.showValues,
                        showLabels: item.showLabels,
                        centerLabels: item.centerLabels,
                        labelColor: item.labelColor,
                        precision: item.precision,
                        unit: item.unit,
                        useCDN: this.useCDN
                    }
                    items.push(newItem)
                }
            })
            group.items = items
            dashboardGroups.push(group)
        })
        return dashboardGroups
    }
    
    /**
     * Save the current dashboard configuration
     * 
     * @async
     * @returns this
     */
    async saveConfig() {
        const config = this.getConfig()
        await this.record.update({
            config
        })
        return this
    }
    
    /**
     * Update the layout of the dashboard
     * 
     * @returns this
     */
    updateLayout() {
        return this
    }

    /**
     * Print the dashboard - Work in progress
     */
    async print() {
        // const charts = document.querySelectorAll("a-chart")
        // charts.forEach((chartElement) => {
        //     const chartInstance = chartElement.chart
        //     if (chartInstance) {
        //         const img = document.createElement("img")
        //         img.src = chartInstance.canvas.toDataURL()
        //         img.style.display = "block"
        //         img.style.width = "100%"
        //         img.style.maxWidth = "fit-content"
        //         img.style.height = "100%"
        //         img.style.maxHeight = "fit-content"
        //         chartElement.replaceChild(img, chartInstance.canvas)
        //     }
        // })
        // setTimeout(() => window.print(), 500)
    }

    /**
     * Initialize click events
     * 
     * @private
     * @ignore
     */
    _initClickEvents() {
        if (!this.canEditView) return

        // this.onclick = (e) => {
        //     const target = e.target
        //     log("---------")
        //     log(target)
        //     const chartButton = target.closest(".a-button")

        //     if (!chartButton && target.closest(".dashboard-group")) {
        //         const groupId = target.closest(".dashboard-group").id
        //         this._showGroupSetup(groupId, e)
        //     }
        // }
    }

    /**
     * Initialize subscriptions to PubSub
     * 
     * @private
     * @ignore
     * @returns this
     */
    _initSubscriptions() {
        // React to database mutations
        this.subscriptions = this.subscriptions.concat([
            subscribe("EVT_DB_UPDATE:VIEW", (msgData) => this._updateTitle(msgData)),

            // React to events coming from individual charts
            subscribe("EVT_DASHBOARD_SETUP", (chartId) => {
                if (!this.isConnected) return

                const chartIds = this._getChartIds()
                if (chartIds.length == 0) return

                if (chartIds.includes(chartId)) {
                    this.saveConfig()
                }
            }),

            // React when one of the charts is deleted
            subscribe("EVT_DASHBOARD_CHART_DELETED", (chartId) => {
                if (!this.isConnected) return

                const chartIds = this._getChartIds()
                if (chartIds.length == 0) return

                if (chartIds.includes(chartId)) {
                    this.deleteChart(chartId)
                }
            }),

            // React when one of the charts is clicked
            subscribe("EVT_CHART_CLICKED", (msg) => {
                if (!this.isConnected) return

                const chartIds = this._getChartIds()
                if (chartIds.length == 0) return

                if (chartIds.includes(msg.chartId)) {
                    const group = this._getChartGroup(msg.chartId)
                    this._showGroupSetup(group.id, msg.event)
                }
            })
        ])

        return this
    }

    /**
     * Initialize the charts drag and drop
     * 
     * @private
     * @ignore
     */
    _enableDragAndDrop() {
        if (!this.canEditView) return
        
        // Drag and drop helpers
        const getCharts = () => this.querySelectorAll(".a-chartview")
        const resetCharts = () => getCharts().forEach(chart => chart.classList.remove("chartview-highlight"))
        const resetChart = (chart) => chart.classList.remove("chartview-highlight")
        const highlightChart = (chart) => chart.classList.add("chartview-highlight")

        // Drag and drop events
        const dndEvents = {
            ondragstart: (event) => {
                const chart = event.target.closest(".a-chartview")
                kiss.context.chartId = chart.id
            },

            ondragover: (event) => {
                event.preventDefault()
                resetCharts()
                const chart = event.target.closest(".a-chartview")
                if (!chart) return
                highlightChart(chart)
            },

            ondrop: (event) => {
                event.preventDefault()
                resetCharts()
                const chart = event.target.closest(".a-chartview")
                if (!chart) return

                const chartId = kiss.context.chartId
                this.moveChart(chartId, chart.id)
            },

            ondragleave: (event) => {
                const chart = event.target.closest(".a-chartview")
                if (!chart) return
                resetChart(chart)
            }
        }

        this.querySelectorAll(".a-chartview").forEach(chart => {
            chart.draggable = true
            chart.ondragstart = dndEvents.ondragstart
            Object.assign(chart, dndEvents)
        })
    }

    /**
     * Show the menu to setup a group of charts
     * 
     * @param {string} groupId 
     * @param {object} event - The click event
     */
    _showGroupSetup(groupId, event) {
        const _this = this
        const group = $(groupId)
        const groupMoves = this.checkGroupMove(groupId)

        createMenu({
            items: [
                // SIZE
                `<h3>${txtTitleCase("row height")}</h3>`,
                "-",
                {
                    text: "20%",
                    icon: "fas fa-circle",
                    iconSize: "0.2rem",
                    action: async function() {
                        group.config.height = group.style.height = `calc(calc(calc(100vh - ${_this.CHART_TOP_SPACE}rem) * 0.2) - 1px)`
                        await _this.saveConfig()
                    }
                },                
                {
                    text: "25%",
                    icon: "fas fa-circle",
                    iconSize: "0.5rem",
                    action: async function() {
                        group.config.height = group.style.height = `calc(calc(calc(100vh - ${_this.CHART_TOP_SPACE}rem) * 0.25) - 1px)`
                        await _this.saveConfig()
                    }
                },
                {
                    text: "33%",
                    icon: "fas fa-circle",
                    iconSize: "0.66rem",
                    action: async function() {
                        group.config.height = group.style.height = `calc(calc(calc(100vh - ${_this.CHART_TOP_SPACE}rem) * 0.33) - 1px)`
                        await _this.saveConfig()
                    }
                },
                {
                    text: "40%",
                    icon: "fas fa-circle",
                    iconSize: "0.8rem",
                    action: async function() {
                        group.config.height = group.style.height = `calc(calc(calc(100vh - ${_this.CHART_TOP_SPACE}rem) * 0.40) - 1px)`
                        await _this.saveConfig()
                    }
                },                
                {
                    text: "50%",
                    icon: "fas fa-circle",
                    iconSize: "1rem",
                    action: async function() {
                        group.config.height = group.style.height = `calc(calc(calc(100vh - ${_this.CHART_TOP_SPACE}rem) * 0.5) - 1px)`
                        await _this.saveConfig()
                    }
                },
                {
                    text: "66%",
                    icon: "fas fa-circle",
                    iconSize: "1.3rem",
                    action: async function() {
                        group.config.height = group.style.height = `calc(calc(calc(100vh - ${_this.CHART_TOP_SPACE}rem) * 0.66) - 1px)`
                        await _this.saveConfig()
                    }
                },
                {
                    text: "75%",
                    icon: "fas fa-circle",
                    iconSize: "1.5rem",
                    action: async function() {
                        group.config.height = group.style.height = `calc(calc(calc(100vh - ${_this.CHART_TOP_SPACE}rem) * 0.75) - 1px)`
                        await _this.saveConfig()
                    }
                },          
                {
                    text: "100%",
                    icon: "fas fa-circle",
                    iconSize: "2rem",
                    action: async function() {
                        group.config.height = group.style.height = `calc(100vh - ${_this.CHART_TOP_SPACE}rem)`
                        await _this.saveConfig()
                    }
                },
                "-",
                // MOVE UP
                {
                    hidden: groupMoves.up ? false : true,
                    text: txtTitleCase("move up"),
                    icon: "fas fa-arrow-up",
                    action: async () => this.moveGroupUp(groupId)
                },
                // MOVE DOWN
                {
                    hidden: groupMoves.down ? false : true,
                    text: txtTitleCase("move down"),
                    icon: "fas fa-arrow-down",
                    action: async () => this.moveGroupDown(groupId)
                },
                // ADD A ROW OF CHARTS
                {
                    text: txtTitleCase("add row"),
                    icon: "fas fa-plus",
                    action: async () => this.addGroup()
                },
                // DELETE
                "-",
                {
                    text: txtTitleCase("delete this row"),
                    icon: "fas fa-trash",
                    iconColor: "var(--red)",
                    action: async () => this.deleteGroup(groupId)
                }
            ]
        }).render().showAt(event.clientX - 20, event.clientY - 20)
    }

    /**
     * Show quick tips for the 1st created chart
     * 
     * @private
     * @ignore
     */
    _showQuickTips() {
        const dashboardGroup = this.getGroups()[0]
        setTimeout(() => {
            const buttonSetup = $("actions:" + dashboardGroup.items[0].id)
            const buttonAdd = dashboardGroup.items[dashboardGroup.items.length - 1]

            kiss.tools.highlightElements([
                {
                    element: buttonSetup,
                    text: txtTitleCase("#help setup chart"),
                    position: "left"
                },
                {
                    element: buttonAdd,
                    text: txtTitleCase("#help add chart"),
                    position: "left"
                }
            ])
        }, 1000)
    }

    /**
     * Manage the dashboard buttons
     * 
     * @private
     * @ignore
     */
    _manageDashboardButtons() {
        if (!this.canEditView) return

        setTimeout(() => {
            const dashboardGroupsContainer = $("dashboard-groups:" + this.id)
            const dashboardGroupElements = Array.from(dashboardGroupsContainer.firstChild.children)

            // Remove all buttons from the dashboard
            dashboardGroupElements.forEach(dashboardGroup => {
                dashboardGroup.items.forEach((item, index) => {
                    if (item.type == "button") {
                        dashboardGroup.deleteItem(item.id)
                    }
                })
            })

            // Add a button to add a chart to each group
            dashboardGroupElements.forEach(dashboardGroup => {
                if (dashboardGroup.items.length > 3) return

                let buttonConfig = {
                    type: "button",
                    icon: "fas fa-plus",
                    width: "3.2rem",
                    height: "3.2rem",
                    margin: "0 0 0 -2.5rem",
                    tip: txtTitleCase("add chart"),
                    action: () => this.addChart(dashboardGroup.id)
                }

                dashboardGroup.addItem(buttonConfig)
            })
        }, 0)
    }
 
    /**
     * Update the dashboard title
     * 
     * @private
     * @ignore
     */
    _updateTitle(msgData) {
        if (!this.record) return
        if (msgData.id == this.record.id && msgData.data.name) {
            this.setTitle(msgData.data.name)
        }
    }

    /**
     * Get the charts of the dashboard
     * 
     * @returns {HTMLElement[]} - The charts
     */
    _getCharts() {
        return Array.from(this.querySelectorAll(".a-chartview"))
    }

    /**
     * Get the chart IDs from the dashboard
     * 
     * @private
     * @ignore
     * @returns {string[]} - The chart IDs
     */
    _getChartIds() {
        return this._getCharts().map(chart => chart.id)
    }

    /**
     * Get the group of a chart
     * 
     * @private
     * @ignore
     * @param {string} chartId 
     * @returns {HTMLElement} - The group element
     */
    _getChartGroup(chartId) {
        return $(chartId).closest(".dashboard-group")
    }

    /**
     * Get the index of a group in the dashboard
     * 
     * @private
     * @ignore
     * @param {string} groupId 
     * @returns {number} - The group index, or -1 if not found
     */
    _getGroupIndex(groupId) {
        const dashboardGroups = $("dashboard-groups:" + this.id)
        return dashboardGroups.firstChild.items.findIndex(group => group.id == groupId)
    }

    /**
     * Get the groups container
     * 
     * @private
     * @ignore
     * @returns {HTMLElement} - The groups container
     */
    _getGroupsContainer() {
        return $("dashboard-groups-container:" + this.id)
    }

    /**
     * Refresh the sizes of the charts in a group
     * 
     * @private
     * @ignore
     * @param {HTMLElement} group - The group element
     */
    _updateGroupLayout(group) {
        group.querySelectorAll(".a-chartview").forEach(chart => chart.updateSize())
    }    
}

// Create a Custom Element and add a shortcut to create it
customElements.define("a-dashboard", kiss.ui.Dashboard)

/**
 * Shorthand to create a new Dashboard. See [kiss.ui.Dashboard](kiss.ui.Dashboard.html)
 * 
 * @param {object} config
 * @returns HTMLElement
 */
const createDashboard = (config) => document.createElement("a-dashboard").init(config)

;