I am working on a Node-RED dashboard using Vue.js and Vuetify. My flow fetches topology data and displays it using nx.graphic.Topology
. Below the topology, I want to display a table. However, the table is currently appearing above the topology.
How can I ensure that the topology appears at the top of the page and the table comes below it?
Here’s a snippet of my flow and ui-template
configuration:
[
{
"id": "203308fe43af6d1d",
"type": "function",
"z": "7c02ada792f0342a",
"name": "function 14",
"func": "const global_DBUtils = global.get('global_DBUtils'); // Retrieve the function\nif (!global_DBUtils) {\n node.error(\"getTopologies function not found in global context\");\n return null;\n}\n\n// Define an async wrapper function to handle the async call\n(async () => {\n try {\n // Call the getTopologies function\n const topologies = await global_DBUtils.getTopologies();\n console.log(\"Topologies data:\", topologies);\nnode.log(\"----------------->Path: \" + msg.payload.page.path);\n\n // Attach the topologies data to the message\n msg.payload = topologies;\n\n // Send the updated message\n node.send(msg);\n } catch (err) {\n // Handle any errors\n node.error(\"Error fetching topologies: \" + err.message);\n }\n})();\n\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 430,
"y": 200,
"wires": [
[
"9253ed63bf19cecf"
]
]
},
{
"id": "9253ed63bf19cecf",
"type": "ui-template",
"z": "7c02ada792f0342a",
"group": "",
"page": "",
"ui": "64fc71361e24a0d0",
"name": "topo_trial",
"order": 1,
"width": "10",
"height": "6",
"head": "",
"format": "<template>\n <div>\n <router-view :key=\"$route.fullPath\"></router-view>\n <div class=\"msg\">\n <Teleport v-if=\"mounted\" to=\"#app-bar-title\">\n <div style=\"order: 2;\">\n <div style=\"display: flex; justify-content: center;\">\n\n <v-combobox label=\"Topology\" :items=\"topologies.map(topology => topology.name)\" variant=\"outlined\"\n style=\"width: 300px; height: 50px; margin-top: 60px; border-radius: 25px 25px 0 0;\"\n prepend-inner-icon=\"mdi-magnify\" v-model=\"selectedTopology\"></v-combobox>\n\n </div>\n\n\n </div>\n\n </Teleport>\n\n <div id=\"topology-container\" v-if=\"showContainer\">\n <div class=\"right-corner\">\n <v-btn class=\"topology_button mb-2\" prepend-icon=\"mdi-play\" min-height=\"auto\" width=\"100%\" height=\"35px\">\n Deploy</v-btn>\n <v-btn class=\"topology_button mb-2\" prepend-icon=\"mdi-delete\" min-height=\"auto\" width=\"100%\" height=\"35px\">\n Delete</v-btn>\n </div>\n </div>\n </div>\n <link rel=\"alternate icon\" href=\"./favicon.ico\" type=\"image/png\" sizes=\"16x16\">\n <link rel=\"stylesheet\" href=\"./assets/nextjs/next.css\">\n </div>\n\n <!-- context menu -->\n <v-menu v-model=\"contextMenuVisible\" :style=\"{ left: `${menuX}px`, top: `${menuY}px`, position: 'absolute' }\">\n <v-list>\n <v-list-item v-for=\"(list, i) in lists\" :key=\"i\" @click=\"handleMenuClick(list.title)\">\n <v-list-item-title>{{ list.title }}</v-list-item-title>\n </v-list-item>\n </v-list>\n </v-menu>\n</template>\n\n<script src=\"./assets/nextjs/next.js\"></script>\n<script>\n export default {\n data() {\n return {\n mounted: false,\n topologies: [], // Array of topology objects\n selectedTopology: null,\n topologyApp: null,\n topologyInstance: null,\n value:[],\n labelText: \"\",\n \n };\n },\n computed: {\n // Computed property to determine if the container should be visible\n showContainer() {\n const visibleRoutes = ['/dashboard/topology']; // Specify the paths where the container should appear\n return visibleRoutes.includes(this.$route.path);\n },\n },\n watch: {\n selectedTopology(newValue) {\n console.log('Selected topology changed:', newValue);\n this.onTopologyChange(newValue);\n },\n },\n \n mounted() {\n this.mounted = true;\n // Check if `nx` is loaded\n if (typeof nx === 'undefined') {\n // Dynamically load the script if `nx` is not defined\n const script = document.createElement('script');\n script.src = './assets/nextjs/next.js'; // Replace with actual path to the script\n script.onload = () => {\n console.log('nx library loaded');\n this.initializeTopology(); // Proceed with initialization\n };\n document.head.appendChild(script);\n } else {\n // If already loaded, proceed with initialization\n this.initializeTopology();\n }\n\n // Listen for incoming messages to populate topologies\n this.$watch('msg.payload', (newPayload) => {\n if (Array.isArray(newPayload)) {\n this.topologies = newPayload;\n console.log(\"Topologies updated:\", this.topologies);\n // // Set the default topology if there are any topologies available\n if (this.topologies.length > 0) {\n const defaultTopology = this.topologies[0]; // Selecting the first topology\n this.selectedTopology = defaultTopology.name; // Set the default selected topology\n console.log(\"-------->defaultTopology\", defaultTopology);\n console.log(\"-------->selectedTopology\", this.selectedTopology);\n \n // Initialize the topology in the container\n // this.initializeTopology(defaultTopology.data);\n\n setTimeout(() => {\n this.initializeTopology(defaultTopology.data);\n }, 500);\n }\n }\n });\n \n \n },\n methods: {\n selectTopology(topology) {\n console.log(\"Selected topology:\", this.selectedTopology);\n this.selectedTopology = topology.name;\n this.changeTopology(topology.data);\n },\n \n onTopologyChange(selected) {\n console.log(\"Topology selected in combobox:\", selected);\n const topology = this.topologies.find(topology => topology.name === selected);\n if (topology) {\n console.log(\"Topology found:\", topology);\n this.selectTopology(topology);\n } else {\n console.error(\"Selected topology not found in list\");\n }\n },\n \n initializeTopology(topologyData) {\n console.log(\"Initializing topology after delay...\");\n \n // Add a delay before initializing the topology\n setTimeout(() => {\n console.log(\"Initializing topology:\", topologyData);\n console.log(\"-------not topology app\", !this.topologyApp);\n \n if (!this.topologyApp) {\n const app = new nx.ui.Application();\n const topologyConfig = {\n width: window.innerWidth,\n height: window.innerHeight,\n nodeConfig: {\n label: \"model.name\",\n iconType: \"model.device_type\",\n color: \"model.color\",\n },\n linkConfig: {\n linkType: \"straight\",\n color: \"model.color\",\n },\n showIcon: true,\n };\n \n const topology = new nx.graphic.Topology(topologyConfig);\n topology.attach(app);\n app.container(document.getElementById(\"topology-container\"));\n \n const parentElement = document.querySelector(\"#topology-container\");\n console.log(\"Parent element:\", parentElement);\n const container = parentElement?.querySelector(\".nrdb-layout-group--grid\");\n console.log(\"Container element:\", container);\n \n const topo = document.querySelector(\".n-topology-blue\");\n if (topo) {\n parentElement.appendChild(topo);\n }\n \n this.topologyApp = app;\n this.topologyInstance = topology;\n }\n \n // Apply topology data after delay\n this.topologyInstance.data(topologyData);\n }, 500); // 2000ms = 2 seconds\n },\n \n changeTopology(topologyData) {\n console.log(\"Changing topology to:\", topologyData);\n \n if (this.topologyInstance) {\n console.log(\"Updating existing topology instance after delay...\");\n setTimeout(() => {\n this.topologyInstance.data(topologyData);\n }, 500); // 2-second delay\n } else {\n console.log(\"Topology instance not found, initializing new topology after delay...\");\n setTimeout(() => {\n this.initializeTopology(topologyData);\n }, 500); // 2-second delay\n }\n },\n },\n \n unmounted() {\n if (this.topologyApp) {\n this.topologyApp.destroy();\n this.topologyApp = null;\n this.topologyInstance = null;\n }\n },\n \n};\n\n</script>\n\n<style>\n #topology-container {\n width: 100%;\n height: 100%;\n background-color: white;\n overflow: hidden;\n \n }\n\n body {\n background-color: white;\n }\n\n .v-application__wrap {\n min-height: unset !important;\n }\n\n #app-bar-title {\n overflow: visible;\n position: relative;\n /* display:block; */\n /* z-index: 1000; */\n\n }\n\n /* v-combobox {\n display: block !important; \n } */\n\n .v-menu {\n position: absolute !important;\n z-index: 9999;\n }\n\n\n .bg-primary {\n background-color: #795548 !important;\n color: white !important;\n }\n\n /* .v-btn {\n height: 48px;\n margin-top: 60px;\n} */\n\n /* Button 1 */\n .delete {\n\n position: absolute;\n left: 1029px;\n width: 120px;\n height: 46px;\n padding: 0 20px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-family: Inter;\n font-size: 21px;\n line-height: 40px;\n font-weight: 600;\n background: linear-gradient(180deg, #22CCB2FF 0%, #235BCDFF 100%);\n opacity: 1;\n border: none;\n border-radius: 10px;\n /* border-l */\n }\n\n .delete .text {\n background: linear-gradient(180deg, #FFFFFFFF 0%, #CCCCCCFF 100%);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n }\n\n /* Hover */\n .delete:hover {\n color: #FFFFFFFF;\n /* white */\n background: #6D3EDCFF;\n /* tertiary2-550 */\n }\n\n /* Pressed */\n .delete:hover:active {\n color: #FFFFFFFF;\n /* white */\n background: #5B27D5FF;\n /* tertiary2-600 */\n }\n\n /* Disabled */\n .delete:disabled {\n opacity: 0.4;\n }\n\n /* Button 1 */\n .deploy {\n position: absolute;\n left: 882px;\n width: 180px;\n height: 46px;\n padding: 0 20px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-family: Inter;\n font-size: 21px;\n line-height: 40px;\n font-weight: 600;\n background: linear-gradient(180deg, #22CCB2FF 0%, #235BCDFF 100%);\n opacity: 1;\n border: none;\n border-radius: 10px;\n /* border-l */\n }\n\n .deploy .text {\n background: linear-gradient(180deg, #FFFFFFFF 0%, #CCCCCCFF 100%);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n }\n\n /* Hover */\n .deploy:hover {\n color: #FFFFFFFF;\n /* white */\n background: #6D3EDCFF;\n /* tertiary2-550 */\n }\n\n /* Pressed */\n .deploy:hover:active {\n color: #FFFFFFFF;\n /* white */\n background: #5B27D5FF;\n /* tertiary2-600 */\n }\n\n /* Disabled */\n .deploy:disabled {\n opacity: 0.4;\n }\n\n\n .v-application__wrap {\n min-height: unset !important;\n /* Override the min-height */\n }\n\n .topology_button {\n background-color: #007BFF;\n font-size: 14px;\n font-family: Helvetica, Arial, sans-serif;\n\n }\n\n .right-corner {\n position: absolute;\n width: 30%;\n /* top: 10px; */\n /* right: 10px; */\n padding: 20px;\n right: 0;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n align-items: center;\n }\n\n .tabulator .tabulator-header {\n background-color: #fff !important;\n border-bottom: none !important;\n }\n\n .tabulator .tabulator-header .tabulator-col {\n border-right: none !important;\n }\n\n .tabulator {\n /* border:none!important; */\n height: auto !important;\n border-radius: 8px !important;\n }\n\n .tabulator-row {\n border-bottom: 1px solid #ccc !important;\n }\n\n .tabulator-row .tabulator-cell {\n border: none !important;\n padding: 15px !important;\n }\n\n .tabulator-row.tabulator-row-even {\n background-color: #fff !important;\n }\n\n .tabulator-cell[tabulator-field=\"time\"] {\n color: blue !important;\n /* Change the font color */\n }\n\n .tabulator-cell[tabulator-field=\"status\"][data-status=\"Success\"] {\n color: red !important;\n }\n\n .tabulator .tabulator-header .tabulator-col {\n border-bottom: 1px solid #aaa !important;\n background: white !important;\n text-align: left !important;\n }\n</style>",
"storeOutMessages": true,
"passthru": true,
"resendOnRefresh": true,
"templateScope": "widget:ui",
"className": "nextjs",
"x": 600,
"y": 200,
"wires": [
[]
]
},
{
"id": "3cf86986518b3ea4",
"type": "ui-event",
"z": "7c02ada792f0342a",
"ui": "64fc71361e24a0d0",
"name": "",
"x": 150,
"y": 200,
"wires": [
[
"4548e9aac21fc6bd"
]
]
},
{
"id": "4548e9aac21fc6bd",
"type": "switch",
"z": "7c02ada792f0342a",
"name": "",
"property": "payload.page.path",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "/topology",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 270,
"y": 200,
"wires": [
[
"203308fe43af6d1d"
]
]
},
{
"id": "98e5aaef82696c03",
"type": "ui-tabulator",
"z": "7c02ada792f0342a",
"name": "",
"group": "edd44d776a4e95a7",
"initObj": "{\n \"height\": 200,\n \"layout\": \"fitColumns\",\n \"columns\": [\n {\"title\":\"S.No\",\"field\":\"sno\",\"hozAlign\":\"left\"},\n {\"title\":\"Event type\",\"field\":\"event type\",\"hozAlign\":\"left\"},\n {\"title\":\"Status\",\"field\":\"status\",\"hozAlign\":\"left\"},\n {\"title\":\"Time\",\"field\":\"time\",\"hozAlign\":\"left\"}\n\n\n ],\n \"data\": [\n {\"sno\":1,\"event type\":\"Login\", \"status\":\"Success\",\"time\":\"10:30\"},\n {\"sno\":2,\"event type\":\"File Upload\", \"status\":\"Failed\",\"time\":\"11:30\"},\n {\"sno\":3,\"event type\":\"Profile Update\", \"status\":\"Success\",\"time\":\"12:30\"},\n\n {\"sno\":4,\"event type\":\"Password Change\", \"status\":\"Success\",\"time\":\"1:30\"},\n\n {\"sno\":5,\"event type\":\"Logout\", \"status\":\"Failed\",\"time\":\"2:30\"},\n {\"sno\":6,\"event type\":\"Logout\", \"status\":\"success\",\"time\":\"3:30\"}\n\n\n\n \n ]\n}",
"maxWidth": "",
"events": "",
"order": 1,
"multiUser": false,
"validateRowIds": false,
"themeCSS": "",
"themeFile": "",
"tblDivId": "",
"printToLog": false,
"width": 0,
"height": 0,
"x": 590,
"y": 240,
"wires": [
[]
]
},
{
"id": "64fc71361e24a0d0",
"type": "ui-base",
"name": "",
"path": "/dashboard",
"appIcon": "",
"includeClientData": true,
"acceptsClientConfig": [
"ui-iframe",
"ui-control",
"ui-template",
"ui-gauge",
"ui-chart",
"ui-slider",
"ui-text-input",
"ui-number-input",
"ui-file-input",
"ui-button",
"ui-button-group",
"ui-dropdown",
"ui-radio-group",
"ui-switch",
"ui-text",
"ui-chart",
"ui-number-input",
"ui-switch",
"ui-table",
"ui-gauge",
"ui-markdown",
"ui-iframe",
"ui-tabulator",
"ui-radio-group",
"ui-dropdown",
"ui-button-group",
"ui-file-input",
"ui-form"
],
"showPathInSidebar": false,
"showPageTitle": false,
"navigationStyle": "icon",
"titleBarStyle": "fixed"
},
{
"id": "edd44d776a4e95a7",
"type": "ui-group",
"name": "topology table",
"page": "dc3b0354cb683966",
"width": "27",
"height": "1",
"order": 1,
"showTitle": false,
"className": "",
"visible": "true",
"disabled": "false",
"groupType": "default"
},
{
"id": "dc3b0354cb683966",
"type": "ui-page",
"name": "topology",
"ui": "64fc71361e24a0d0",
"path": "/topology",
"icon": "home",
"layout": "grid",
"theme": "",
"breakpoints": [
{
"name": "Default",
"px": "0",
"cols": "3"
},
{
"name": "Tablet",
"px": "576",
"cols": "6"
},
{
"name": "Small Desktop",
"px": "768",
"cols": "9"
},
{
"name": "Desktop",
"px": "1024",
"cols": "12"
}
],
"order": 1,
"className": "",
"visible": true,
"disabled": false
}
]
"How can I position the table from the Node-RED Tabulator node under the topology when using a UI-scoped Template node? The Tabulator node is group-scoped, and the Template node is UI-scoped. I want both elements to be displayed within the same layout properly."