How to Display Topology Above Table in Node-RED Dashboard?

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."

1 Like

Is there a reason for your template being UI Scoped? When set that way, the `page and group-scoped stuff will always get priority and render first per the ordering in the Dashboard sidebar

Yes, in the template node, I am using Teleport to display the combobox in the app bar title across all pages. That's why I set the template node as UI scoped. However, when selecting a list item in the combobox, the topology appears below the table. I am using Tabulator for the table, which is group scoped. How can I modify it so that the table stays below and the topology appears in front? How can I fix this?

Can you build them as two separate ui-template nodes? The UI Scoped combo box in one node, and the topology in another?

but the data from the combbox only i m getting. then how can i get the data from one template node to other templates or pages

You can wire the templates together still.

There is a built-in send function (docs) which you could emit the selection from the combobox, pass that into the topology and it'll treat it as a new message coming in as with any other message in Node-RED

thanks that is a great idea