diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index e645baca1..bc480c33f 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -81,6 +81,7 @@ export default ({ mode }) => { collapsed: false, items: [ { text: 'ui-button', link: '/nodes/widgets/ui-button' }, + { text: 'ui-control', link: '/nodes/widgets/ui-control' }, { text: 'ui-chart', link: '/nodes/widgets/ui-chart' }, { text: 'ui-dropdown', link: '/nodes/widgets/ui-dropdown' }, { text: 'ui-event', link: '/nodes/widgets/ui-event' }, diff --git a/docs/components/AddedIn.vue b/docs/components/AddedIn.vue new file mode 100644 index 000000000..401cba14b --- /dev/null +++ b/docs/components/AddedIn.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/docs/contributing/widgets/third-party.md b/docs/contributing/widgets/third-party.md index bffdd60f8..c266259ef 100644 --- a/docs/contributing/widgets/third-party.md +++ b/docs/contributing/widgets/third-party.md @@ -1,4 +1,8 @@ -# Building Third Party Widgets + + +# Building Third Party Widgets If you have an idea for a widget that you'd like to build in Dashboard 2.0 we are open to Pull Requests and ideas for additions to the [core collection](../../nodes/widgets.md) of Widgets. diff --git a/docs/layouts/notebook.md b/docs/layouts/notebook.md index 84b6e62d5..4e1ee118f 100644 --- a/docs/layouts/notebook.md +++ b/docs/layouts/notebook.md @@ -1,4 +1,8 @@ -# Layout: Notebook + + +# Layout: Notebook This layout mimics a traditional Jupyter Notebook, where the layout will stretch to 100% width, up to a maximum width of 1024px, and will centrally align. diff --git a/docs/nodes/widgets/ui-control.md b/docs/nodes/widgets/ui-control.md new file mode 100644 index 000000000..27e536e45 --- /dev/null +++ b/docs/nodes/widgets/ui-control.md @@ -0,0 +1,157 @@ +--- +{ + "events": [ + { + "event": "$pageview", + "payload": "{ page }", + "description": "Sent whenever a user views a given page on the Dashboard" + }, + { + "event": "$pageleave", + "payload": "{ page }", + "description": "Sent whenever a user leaves a given page on the Dashboard" + } + ] +} +--- + + + +# Control `ui-control` + +This widget doesn't render any content into your Dashboard. Instead, it provides an interface for you to control the behaviour of your Dashboard from within the Node-RED Editor. + +Functionality is generally divided into two main features: + +- **Navigation**: Force the user to move to a new page +- **Display**: Show/Hide groups and pages +- **Disability**: Enable/Disable groups and pages, this still shows them, but prevents interaction + +## Controls List + +Currently, we support the following controls: + +### Navigation + +You can programmaticaly force navigation with the following payloads with `ui-control`: + +#### Change Page + +Explicitely choose the page you want to navigate to: + +```js +// String +msg.payload = '' + +// Object +msg.payload = { + page: '', +} +``` + +#### Next/Previous + +Navigate to the next or previous page in the list: + +```js +// Next Page +msg.payload = "+1" + +// Previous Page +msg.payload = "-1" +``` + +#### Refresh + +You can force a refresh of the current view by sending a blank string payload: + +```js +msg.payload = "" +``` + +### Show/Hide + +You can programmaticaly show/hide groups and pages with the following payload into `ui-control`: + +```js +msg.payload = { + pages: { + show: ['', ''], + hide: [''] + } + groups: { + show: ['', ''], + hide: [''] + } +} +``` + +_Note:_ `pages` can be subbed with `tabs` as per Dashboard 1.0 and `groups` can also be subbed with `group` as per Dashboard 1.0. + +### Enable/Disable + +You can programmaticaly disable/enable groups and pages with the following payload into `ui-control`: + +```js +msg.payload = { + pages: { + enable: ['', ''], + disable: [''] + } + groups: { + enable: ['', ''], + disable: [''] + } +} +``` + +_Note:_ `pages` can be subbed with `tabs` as per Dashboard 1.0 and `groups` can also be subbed with `group` as per Dashboard 1.0. + +## Events List + +In addition to `ui-control` taking input to _control_ the UI, we have also maintained support for all events emitted by `ui-control` from Dashboard 1.0 here too. + +### Connection Status + +We follow the Dashboard 1.0 convention for emitting socket-based events from the `ui-control` node. + +#### .on('connection') + +When a new Dashboard client connects to Node-RED, the `ui-control` node will emit: + +```js +msg = { + payload: 'connect', + socketid: '', + socketip: '' +} +``` + +#### .on('disconnect') + +When a Dashboard client disconnects from Node-RED, the `ui-control` node will emit: + +```js +msg = { + payload: 'lost', + socketid: '', + socketip: '' +} +``` + +### Change Tab/Page + +When a user changes the active tab or page, the `ui-control` node will emit: + +```js +msg = { + payload: 'change', + socketid: '', + socketip: '', + tab: '', + name: '' +} +``` \ No newline at end of file diff --git a/docs/nodes/widgets/ui-event.md b/docs/nodes/widgets/ui-event.md index a3f28b7d7..12a261ce6 100644 --- a/docs/nodes/widgets/ui-event.md +++ b/docs/nodes/widgets/ui-event.md @@ -17,9 +17,10 @@ -# Event `ui-event` +# Event `ui-event` This widget doesn't render any content into your Dashboard. Instead, it listens for user-driven behaviour and events in your Dashboard and emits accordingly into the Node-RED Editor when those events have taken place. @@ -36,6 +37,8 @@ Each time a user views a page, the `ui-event` node will emit: ```js msg = { topic: '$pageview', + socketid: '1234', + socketip: '127.0.0.1' payload: { page: { name: 'Page Name', diff --git a/docs/nodes/widgets/ui-markdown.md b/docs/nodes/widgets/ui-markdown.md index 276b226ce..8d07f82b4 100644 --- a/docs/nodes/widgets/ui-markdown.md +++ b/docs/nodes/widgets/ui-markdown.md @@ -5,6 +5,7 @@ props: --- # Markdown Viewer `ui-markdown` @@ -70,7 +71,7 @@ function () { | `msg.payload` | {{ msg.payload || 'Placeholder' }} | ```` -## Mermaid Charts +## Mermaid Charts The `ui-markdown` widget also supports the injection of [Mermaid](https://mermaid.js.org/intro/). To do so, you can include a mermaid chart definition inside a Markdown fenced code block, defined with the `mermaid` type: diff --git a/docs/nodes/widgets/ui-notification.md b/docs/nodes/widgets/ui-notification.md index 53b1cdb47..893629ea2 100644 --- a/docs/nodes/widgets/ui-notification.md +++ b/docs/nodes/widgets/ui-notification.md @@ -10,7 +10,11 @@ props: Class: Appends CSS classes to the widget --- -# Notification `ui-notification` + + +# Notification `ui-notification` Known in Dashboard 1.0 as a "Toast", this widget displays text/HTML in a small window that will appear on the screen for a defined duration of time (`timeout`) and at a defined location on the screen (`position`). diff --git a/docs/nodes/widgets/ui-table.md b/docs/nodes/widgets/ui-table.md index 586ee6d3d..541cdf20e 100644 --- a/docs/nodes/widgets/ui-table.md +++ b/docs/nodes/widgets/ui-table.md @@ -9,9 +9,10 @@ props: --- -# Data Table `ui-table` +# Data Table `ui-table` Renders a set of data in a tabular format. Expects an input (`msg.payload`) in the format of: diff --git a/docs/user/migration.md b/docs/user/migration.md index 5c8b8ecf1..b423f611e 100644 --- a/docs/user/migration.md +++ b/docs/user/migration.md @@ -4,6 +4,7 @@ import uiButton from './migration/ui_button.json' import uiChart from './migration/ui_chart.json' + import uiControl from './migration/ui_control.json' import uiDropdown from './migration/ui_dropdown.json' import uiForm from './migration/ui_form.json' import uiSlider from './migration/ui_slider.json' @@ -16,6 +17,7 @@ const widgets = ref({ 'ui_button': uiButton, 'ui_chart': uiChart, + 'ui_control': uiControl, 'ui_dropdown': uiDropdown, 'ui_form': uiForm, 'ui_slider': uiSlider, @@ -140,6 +142,16 @@ is currently _not_ supported. If this is of particular importance, please do voi Whilst there is currently not an explicit `ui_colour_picker` widget, the `ui_text_input` widget can be used to achieve the same result, by setting _"type"_ to _"color"_ +### `ui_control` + + + +#### Controls List + +All Dashboard 1.0 controls are supported in Dashboard 2.0, with the exception of the `open/close` control for a group, which is currently not supported. + +You can see detailed documentation of the available controls, and emitted events [here](/nodes/widgets/ui-control.html). + ### `ui_date_picker` Whilst there is currently not an explicit `ui_date_picker` widget, the `ui_text_input` widget can be used to achieve the same result, by setting _"type"_ to _"date"_, _"time"_, _"week"_ or _"month"_. @@ -198,13 +210,6 @@ You can track progress of this development effort here: [Issue #41](https://gith -### `ui_control` - - - -Track progress, and input ideas here: [UI Control #258](https://github.com/FlowFuse/node-red-dashboard/issues/258). - - ## Theming We have tried to make theming in Dashboard 2.0 more low-code friendly, by providing a number of exposed properties, and a wrapping `ui-theme` config node which is assigned at the `ui-page` level. diff --git a/docs/user/migration/ui_control.json b/docs/user/migration/ui_control.json index b5213bfe1..f57f0e7ae 100644 --- a/docs/user/migration/ui_control.json +++ b/docs/user/migration/ui_control.json @@ -1,11 +1,16 @@ { "node": "ui_button", - "description": "Button", + "description": "Send events to Dashboard in order to show/hide content, disable/enable content and navigate between pages", "properties": [ { - "property": "label", - "changes": "label", - "notes": "label" + "property": "name", + "changes": null, + "notes": null + }, + { + "property": "events", + "changes": null, + "notes": "All events supported except for the group open/close events. Track #406 progress here." } ] } \ No newline at end of file diff --git a/nodes/config/ui_base.js b/nodes/config/ui_base.js index e1b9f42f9..b0530b4c8 100644 --- a/nodes/config/ui_base.js +++ b/nodes/config/ui_base.js @@ -99,12 +99,12 @@ module.exports = function (RED) { uiShared.app.use(config.path, uiShared.httpMiddleware, express.static(path.join(__dirname, '../../dist'))) // debugging endpoints - uiShared.app.get(config.path + '/_debug/datastore/:widgetid', uiShared.httpMiddleware, (req, res) => { - return res.json(datastore.get(req.params.widgetid)) + uiShared.app.get(config.path + '/_debug/datastore/:itemid', uiShared.httpMiddleware, (req, res) => { + return res.json(datastore.get(req.params.itemid)) }) - uiShared.app.get(config.path + '/_debug/statestore/:widgetid', uiShared.httpMiddleware, (req, res) => { - return res.json(statestore.getAll(req.params.widgetid)) + uiShared.app.get(config.path + '/_debug/statestore/:itemid', uiShared.httpMiddleware, (req, res) => { + return res.json(statestore.getAll(req.params.itemid)) }) // serve dashboard @@ -259,9 +259,8 @@ module.exports = function (RED) { * @param {Socket} socket - socket.io socket connecting to the server */ function emitConfig (socket) { - const widgets = node.ui.widgets // loop over widgets - check statestore if we've had any dynamic properties set - for (const [id, widget] of widgets) { + for (const [id, widget] of node.ui.widgets) { const state = statestore.getAll(id) if (state) { // merge the statestore with our props to account for dynamically set properties: @@ -269,6 +268,24 @@ module.exports = function (RED) { } } + // loop over pages - check statestore if we've had any dynamic properties set + for (const [id, page] of node.ui.pages) { + const state = statestore.getAll(id) + if (state) { + // merge the statestore with our props to account for dynamically set properties: + node.ui.pages.set(id, { ...page, ...state }) + } + } + + // loop over groups - check statestore if we've had any dynamic properties set + for (const [id, group] of node.ui.groups) { + const state = statestore.getAll(id) + if (state) { + // merge the statestore with our props to account for dynamically set properties: + node.ui.groups.set(id, { ...group, ...state }) + } + } + // pass the connected UI the UI config socket.emit('ui-config', node.id, { dashboards: Object.fromEntries(node.ui.dashboards), @@ -308,7 +325,7 @@ module.exports = function (RED) { }) } - function setupEventHandlers (socket) { + function setupEventHandlers (socket, onConnection) { socket.on('widget-action', onAction.bind(null, socket)) socket.on('widget-change', onChange.bind(null, socket)) socket.on('widget-load', onLoad.bind(null, socket)) @@ -318,12 +335,19 @@ module.exports = function (RED) { const registered = [] // track which widget types we've already subscribed for node.ui?.widgets?.forEach((widget) => { if (widget.hooks?.onSocket) { - for (const [eventName, handler] of Object.entries(widget.hooks.onSocket)) { - // we only need add the listener for a given event type the once - if (registered.indexOf(widget.type) === -1) { - socket.on(eventName, handler.bind(null, socket)) - registered.push(widget.type) + if (registered.indexOf(widget.type) === -1) { + for (const [eventName, handler] of Object.entries(widget.hooks.onSocket)) { + // we only need add the listener for a given event type the once + if (eventName === 'connection') { + if (onConnection) { + // these handlers are setup as part of an onConnection event, so trigegr these now + handler(socket) + } + } else { + socket.on(eventName, handler.bind(null, socket)) + } } + registered.push(widget.type) } } }) @@ -341,16 +365,19 @@ module.exports = function (RED) { * @param {Socket} socket socket.io socket connecting to the server */ function onConnection (socket) { + console.log('new connection', socket.id) // record mapping from connection to he ui-base node socket._baseId = node.id // node.connections[socket.id] = socket // store the connection for later use uiShared.connections[socket.id] = socket // store the connection for later use + emitConfig(socket) // clean up then re-register listeners - cleanupEventHandlers(socket) - setupEventHandlers(socket) + // cleanupEventHandlers(socket) + // setup connections, and fire any 'on('connection')' events + setupEventHandlers(socket, true) } /** * Handles a widget-action event from the UI @@ -537,6 +564,11 @@ module.exports = function (RED) { }, 300) } + /** + * Allow for any child node to emit to all connected UIs + */ + node.emit = emit + /** * Register allows for pages, widgets, groups, etc. to register themselves with the Base UI Node * @param {*} page diff --git a/nodes/config/ui_group.html b/nodes/config/ui_group.html index 81b7f56f1..0260c9309 100644 --- a/nodes/config/ui_group.html +++ b/nodes/config/ui_group.html @@ -10,7 +10,7 @@ width: { value: 6 }, height: { value: 1 }, order: { value: -1 }, - disp: { value: true }, // show title on group card + showTitle: { value: true }, // show title on group card className: { value: '' } }, label: function () { @@ -18,6 +18,9 @@ return `${this.name} [${page}]` || 'UI Group' }, oneditprepare: function () { + if (this.disp) { + this.showTitle = this.disp + } $('#node-config-input-size').elementSizer({ width: '#node-config-input-width', height: '#node-config-input-height', @@ -43,7 +46,8 @@
- + +
diff --git a/nodes/config/ui_group.js b/nodes/config/ui_group.js index 46d3ab9d6..17de7d30a 100644 --- a/nodes/config/ui_group.js +++ b/nodes/config/ui_group.js @@ -7,6 +7,12 @@ module.exports = function (RED) { RED.nodes.createNode(this, config) const node = this + if (!('showTitle' in config)) { + // migration backwards compatibility + // we now use showTitle, not disp, but older flows still may have disp + config.showTitle = config.disp || true + } + node.on('close', function (removed, done) { node.deregister() // deregister self done() diff --git a/nodes/widgets/locales/en-US/ui_control.html b/nodes/widgets/locales/en-US/ui_control.html new file mode 100644 index 000000000..442a93865 --- /dev/null +++ b/nodes/widgets/locales/en-US/ui_control.html @@ -0,0 +1,57 @@ + + \ No newline at end of file diff --git a/nodes/widgets/locales/en-US/ui_control.json b/nodes/widgets/locales/en-US/ui_control.json new file mode 100644 index 000000000..9407e712f --- /dev/null +++ b/nodes/widgets/locales/en-US/ui_control.json @@ -0,0 +1,13 @@ +{ + "ui-control": { + "label": { + "name": "Name", + "output": "Output" + }, + "events": { + "all": "All Events", + "change": "Page/Tab Change Events Only", + "connect": "Connection Events Only" + } + } + } \ No newline at end of file diff --git a/nodes/widgets/locales/en-US/ui_event.html b/nodes/widgets/locales/en-US/ui_event.html index 91d75ea25..5a1ca80d3 100644 --- a/nodes/widgets/locales/en-US/ui_event.html +++ b/nodes/widgets/locales/en-US/ui_event.html @@ -17,4 +17,20 @@ $pageleave - Emits whenever a user leaves a page. +

An example output msg is as follows:

+
msg = {
+    topic: '$pageview',
+    socketid: '1234',
+    socketip: '127.0.0.1'
+    payload: {
+        page: {
+            name: 'Page Name',
+            path: '/page/path'
+            id: '1234',
+            theme: 'dark',
+            layout: 'default',
+            _groups: []
+        }
+    }
+}
\ No newline at end of file diff --git a/nodes/widgets/ui_control.html b/nodes/widgets/ui_control.html new file mode 100644 index 000000000..99763559b --- /dev/null +++ b/nodes/widgets/ui_control.html @@ -0,0 +1,51 @@ + + + \ No newline at end of file diff --git a/nodes/widgets/ui_control.js b/nodes/widgets/ui_control.js new file mode 100644 index 000000000..536a2b7f9 --- /dev/null +++ b/nodes/widgets/ui_control.js @@ -0,0 +1,208 @@ +const statestore = require('../store/state.js') + +module.exports = function (RED) { + function UiControlNode (config) { + const node = this + RED.nodes.createNode(this, config) + + // which ui does this widget belong to + const ui = RED.nodes.getNode(config.ui) + + function updateStore (all, items, prop, value) { + items.forEach(function (item) { + const i = all[item] + // update the state store for each page + statestore.set(i.id, prop, value) + }) + } + + function emit (payload) { + ui.emit('ui-control:' + node.id, payload) + } + + const evts = { + onInput: function (msg, send, done) { + const wNode = RED.nodes.getNode(node.id) + // handle the logic here of what to do when input is received + + if (typeof msg.payload !== 'object') { + msg.payload = { page: msg.payload } + } + + // switch to tab name (or number) + if ('tab' in msg.payload || 'page' in msg.payload) { + let page = msg.payload.page || msg.payload.tab + + page = (page === undefined) ? '' : page + + if (page === '-1' || page === '+1' || typeof (page) === 'number' || page === '') { + // special case for -1 and +1 to switch to previous/next tab + // number to pick specific index + // "" to refresh the page + emit({ page }) + } else { + let pageFound = false + // check we have a valid tab/page name + RED.nodes.eachNode(function (n) { + if (n.type === 'ui-page') { + if (n.name === page) { + // send a message to the ui to switch to this tab + emit({ page }) + pageFound = true + } + } + }) + if (!pageFound) { + node.error("No page with the name '" + page + "' found") + } + } + } + + // show/hide or enable/disable tabs + if ('tabs' in msg.payload || 'pages' in msg.payload) { + const pages = msg.payload.pages || msg.payload.tabs + // get a map of page name to page object + const allPages = {} + RED.nodes.eachNode((n) => { + if (n.type === 'ui-page') { + allPages[n.name] = n + } + }) + + // const pMap = RED.nodes.forEach + if ('show' in pages) { + updateStore(allPages, pages.show, 'visible', true) + } + if ('hide' in pages) { + updateStore(allPages, pages.hide, 'visible', false) + } + if ('enable' in pages) { + updateStore(allPages, pages.enable, 'disabled', false) + } + if ('disable' in pages) { + updateStore(allPages, pages.disable, 'disabled', false) + } + + // send to front end in order to action there too + emit({ pages }) + } + + // show or hide ui groups + if ('groups' in msg.payload || 'group' in msg.payload) { + const groups = msg.payload.groups || msg.payload.group + // get a map of group name to group object + const allGroups = {} + RED.nodes.eachNode((n) => { + if (n.type === 'ui-group') { + allGroups[n.name] = n + } + }) + if ('show' in groups) { + updateStore(allGroups, groups.show, 'visible', true) + } + if ('hide' in groups) { + updateStore(allGroups, groups.hide, 'visible', false) + } + if ('enable' in groups) { + updateStore(allGroups, groups.enable, 'disabled', false) + } + if ('disable' in groups) { + updateStore(allGroups, groups.disable, 'disabled', true) + } + emit({ groups }) + } + + // send specific visible/hidden commands via SocketIO here, + // so all logic stays server-side + + wNode.send({ payload: 'input' }) + }, + onSocket: { + connection: function (conn) { + if (config.events === 'all' || config.events === 'connect') { + const wNode = RED.nodes.getNode(node.id) + wNode.send({ + payload: 'connect', + socketid: conn.id, + socketip: conn.client.conn.remoteAddress + }) + } + }, + disconnect: function (conn) { + if (config.events === 'all' || config.events === 'connect') { + const wNode = RED.nodes.getNode(node.id) + wNode.send({ + payload: 'lost', + socketid: conn.id, + socketip: conn.client.conn.remoteAddress + }) + } + }, + 'ui-control': function (conn, id, evt, payload) { + console.log('ui-control', id, evt, payload, id, node.id) + if (id === node.id && (config.events === 'all' || config.events === 'change')) { + // this message was sent by this particular node + if (evt === 'change') { + const wNode = RED.nodes.getNode(node.id) + wNode.send({ + payload: 'change', + tab: payload.page, // index of tab + name: payload.name, // page name + socketid: conn.id, + socketip: conn.client.conn.remoteAddress + }) + } + } + } + } + } + + // inform the dashboard UI that we are adding this node + ui.register(null, null, node, config, evts) + + // this.events = config.events || 'all' + + // const sendconnect = function (id, ip) { + // node.send({ payload: 'connect', socketid: id, socketip: ip }) + // } + + // const sendlost = function (id, ip) { + // node.send({ payload: 'lost', socketid: id, socketip: ip }) + // } + + // const sendchange = function (index, name, id, ip, p) { + // node.send({ payload: 'change', tab: index, name, socketid: id, socketip: ip, params: p }) + // } + + // const sendcollapse = function (group, state, id, ip) { + // node.send({ payload: 'group', group, open: state, socketid: id, socketip: ip }) + // } + + // if (node.events === 'connect') { + // ui.ev.on('newsocket', sendconnect) + // } else if (node.events === 'change') { + // ui.ev.on('changetab', sendchange) + // ui.ev.on('collapse', sendcollapse) + // } else { + // ui.ev.on('newsocket', sendconnect) + // ui.ev.on('changetab', sendchange) + // ui.ev.on('collapse', sendcollapse) + // ui.ev.on('endsocket', sendlost) + // } + + // this.on('close', function () { + // if (node.events === 'connect') { + // ui.ev.removeListener('newsocket', sendconnect) + // } else if (node.events === 'change') { + // ui.ev.removeListener('changetab', sendchange) + // ui.ev.removeListener('collapse', sendcollapse) + // } else { + // ui.ev.removeListener('newsocket', sendconnect) + // ui.ev.removeListener('changetab', sendchange) + // ui.ev.removeListener('collapse', sendcollapse) + // ui.ev.removeListener('endsocket', sendlost) + // } + // }) + } + RED.nodes.registerType('ui-control', UiControlNode) +} diff --git a/nodes/widgets/ui_event.js b/nodes/widgets/ui_event.js index f5e6088ed..f424f9ded 100644 --- a/nodes/widgets/ui_event.js +++ b/nodes/widgets/ui_event.js @@ -9,11 +9,17 @@ module.exports = function (RED) { const evts = { onSocket: { - 'ui-event': function (conn, evt, payload) { - node.send({ - topic: evt, - payload - }) + 'ui-event': function (conn, id, evt, payload) { + const wNode = RED.nodes.getNode(node.id) + if (id === node.id) { + // this was sent by this particular node + wNode.send({ + topic: evt, + payload, + socketid: conn.id, + socketip: conn.client.conn.remoteAddress + }) + } } } } diff --git a/package.json b/package.json index b18b58884..221756543 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,8 @@ "ui-notification": "nodes/widgets/ui_notification.js", "ui-markdown": "nodes/widgets/ui_markdown.js", "ui-template": "nodes/widgets/ui_template.js", - "ui-event": "nodes/widgets/ui_event.js" + "ui-event": "nodes/widgets/ui_event.js", + "ui-control": "nodes/widgets/ui_control.js" } }, "overrides": { diff --git a/ui/src/debug/Debug.vue b/ui/src/debug/Debug.vue index d4840a864..f8ec43c4b 100644 --- a/ui/src/debug/Debug.vue +++ b/ui/src/debug/Debug.vue @@ -33,10 +33,32 @@ {{ filter.key }}: {{ filter.value }}
- + +