Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supporting typedInputs and simplified dynamic property setup #1237

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ lerna-debug.log*
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Custom
node_modules
dist
dist-ssr
*.local
Expand Down Expand Up @@ -44,9 +43,11 @@ bower_components
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
# Ignore dependency directories
node_modules/
jspm_packages/
# except for the test fixtures node_modules
!test/**/fixtures/**/node_modules/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
Expand Down
82 changes: 67 additions & 15 deletions nodes/config/ui_base.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const axios = require('axios')
const v = require('../../package.json').version
const datastore = require('../store/data.js')
const statestore = require('../store/state.js')
const { appendTopic, addConnectionCredentials, getThirdPartyWidgets } = require('../utils/index.js')
const { appendTopic, addConnectionCredentials, getThirdPartyWidgets, evaluateTypedInputs, applyUpdates } = require('../utils/index.js')

// from: https://stackoverflow.com/a/28592528/3016654
function join (...paths) {
Expand Down Expand Up @@ -410,14 +410,35 @@ module.exports = function (RED) {
* @param {Socket} socket - socket.io socket connecting to the server
*/
function emitConfig (socket) {
const promises = []
// loop over widgets - check statestore if we've had any dynamic properties set
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:
widget.props = { ...widget.props, ...state }
// merge the statestore:
widget.state = { ...widget.state, ...state }
}
// if we have typedInputs, evaluate them and update props.
// This is for initial evaluation e.g. for things set to use msg/flow/global/JSONata
try {
const { typedInputs } = widget
if (typedInputs) {
const n = RED.nodes.getNode(id)
const { config } = n.getWidgetRegistration && n.getWidgetRegistration()
const msg = datastore.get(id) || {}
const p = evaluateTypedInputs(RED, config, n, msg, typedInputs).then((result) => {
if (result?.count > 0) {
widget.props = { ...widget.props, ...result?.updates }
}
return result
}).catch((_err) => {
// do nothing
})
promises.push(p)
}
} catch (_err) {
// do nothing
}
}

// loop over pages - check statestore if we've had any dynamic properties set
Expand Down Expand Up @@ -446,14 +467,19 @@ module.exports = function (RED) {
delete meta.wysiwyg
}
// pass the connected UI the UI config
socket.emit('ui-config', node.id, {
meta,
dashboards: Object.fromEntries(node.ui.dashboards),
heads: Object.fromEntries(node.ui.heads),
pages: Object.fromEntries(node.ui.pages),
themes: Object.fromEntries(node.ui.themes),
groups: Object.fromEntries(node.ui.groups),
widgets: Object.fromEntries(node.ui.widgets)
// eslint-disable-next-line promise/always-return
Promise.all(promises).then(() => {
socket.emit('ui-config', node.id, {
meta,
dashboards: Object.fromEntries(node.ui.dashboards),
heads: Object.fromEntries(node.ui.heads),
pages: Object.fromEntries(node.ui.pages),
themes: Object.fromEntries(node.ui.themes),
groups: Object.fromEntries(node.ui.groups),
widgets: Object.fromEntries(node.ui.widgets)
})
}).catch((_err) => {
// do nothing
})
}

Expand Down Expand Up @@ -856,10 +882,16 @@ module.exports = function (RED) {

/**
* Register allows for pages, widgets, groups, etc. to register themselves with the Base UI Node
* @param {*} page
* @param {*} widget
* @param {*} page - the page node we are registering to
* @param {*} group - the group node we are registering to
* @param {*} widgetNode - the node we are registering
* @param {*} widgetConfig - the nodes' configuration object
* @param {*} widgetEvents - the widget event hooks
* @param {Object} [widgetOptions] - additional configuration options for dynamic features the widget
* @param {import('../utils/index.js').NodeDynamicProperties} [widgetOptions.dynamicProperties] - dynamic properties that the node will support
* @param {import('../utils/index.js').NodeTypedInputs} [widgetOptions.typedInputs] - typed inputs that the node will support
*/
node.register = function (page, group, widgetNode, widgetConfig, widgetEvents) {
node.register = function (page, group, widgetNode, widgetConfig, widgetEvents, widgetOptions) {
// console.log('dashboard 2.0, UIBaseNode: node.register(...)', page, group, widgetNode, widgetConfig, widgetEvents)
/**
* Build UI Config
Expand All @@ -870,7 +902,9 @@ module.exports = function (RED) {
// store our UI state properties under the .state key too

let widget = null

if (!widgetOptions || typeof widgetOptions !== 'object') {
widgetOptions = {} // ensure we have an object to work with
}
if (widgetNode && widgetConfig) {
// default states
if (statestore.getProperty(widgetConfig.id, 'enabled') === undefined) {
Expand All @@ -893,6 +927,8 @@ module.exports = function (RED) {
height: widgetConfig.height || 1,
order: widgetConfig.order || 0
},
typedInputs: widgetOptions.typedInputs,
dynamicProperties: widgetOptions.dynamicProperties,
state: statestore.getAll(widgetConfig.id),
hooks: widgetEvents,
src: uiShared.contribs[widgetConfig.type]
Expand Down Expand Up @@ -984,6 +1020,19 @@ module.exports = function (RED) {
widgetNode.getState = function () {
return datastore.get(widgetNode.id)
}
/** Helper function for accessing node setup */
widgetNode.getWidgetRegistration = function () {
return {
base: node,
page,
group,
node: widgetNode,
config: widgetConfig,
events: widgetEvents,
options: widgetOptions,
statestore
}
}

/**
* Event Handlers
Expand Down Expand Up @@ -1017,6 +1066,8 @@ module.exports = function (RED) {
// pre-process the msg before running our onInput function
if (widgetEvents?.beforeSend) {
msg = await widgetEvents.beforeSend(msg)
} else {
msg = await applyUpdates(RED, widgetNode, msg)
}

// standard dynamic property handlers
Expand All @@ -1043,6 +1094,7 @@ module.exports = function (RED) {
if (widgetConfig.topic || widgetConfig.topicType) {
msg = await appendTopic(RED, widgetConfig, wNode, msg)
}

if (hasProperty(widgetConfig, 'passthru')) {
if (widgetConfig.passthru) {
send(msg)
Expand Down
11 changes: 8 additions & 3 deletions nodes/config/ui_group.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,16 @@ module.exports = function (RED) {
* Function for widgets to register themselves with this page
* Calls the parent UI Base "register" function and registers this page,
* along with the widget
* @param {*} widget
* @param {*} widgetNode - the node we are registering
* @param {*} widgetConfig - the nodes' configuration object
* @param {*} widgetEvents - the widget event hooks
* @param {Object} [widgetOptions] - additional configuration options for dynamic features the widget
* @param {Object} [widgetOptions.dynamicProperties] - dynamic properties that the node will support
* @param {import('../utils/index.js').NodeTypedInputs} [widgetOptions.typedInputs] - typed inputs that the node will support
*/
node.register = function (widgetNode, widgetConfig, widgetEvents) {
node.register = function (widgetNode, widgetConfig, widgetEvents, widgetOptions) {
const group = config
page.register(group, widgetNode, widgetConfig, widgetEvents)
page.register(group, widgetNode, widgetConfig, widgetEvents, widgetOptions)
}

node.deregister = function (widgetNode) {
Expand Down
12 changes: 9 additions & 3 deletions nodes/config/ui_page.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,18 @@ module.exports = function (RED) {
* Function for widgets to register themselves with this page
* Calls the parent UI Base "register" function and registers this page,
* along with the widget
* @param {*} widget
* @param {*} group - the group we are registering
* @param {*} widgetNode - the node we are registering
* @param {*} widgetConfig - the nodes' configuration object
* @param {*} widgetEvents - the widget event hooks
* @param {Object} [widgetOptions] - additional configuration options for dynamic features the widget
* @param {Object} [widgetOptions.dynamicProperties] - dynamic properties that the node will support
* @param {import('../utils/index.js').NodeTypedInputs} [widgetOptions.typedInputs] - typed inputs that the node will support
*/
node.register = function (group, widgetNode, widgetConfig, widgetEvents) {
node.register = function (group, widgetNode, widgetConfig, widgetEvents, widgetOptions) {
const page = config
if (ui) {
ui.register(page, group, widgetNode, widgetConfig, widgetEvents)
ui.register(page, group, widgetNode, widgetConfig, widgetEvents, widgetOptions)
} else {
node.error(`Error registering Widget - ${widgetNode.name || widgetNode.id}. No parent ui-base node found for ui-page node: ${(page.name || page.id)}`)
}
Expand Down
7 changes: 7 additions & 0 deletions nodes/store/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,19 @@ const setters = {
// remove data associated to a given widget
reset (id) {
delete state[id]
},
// delete property
deleteProperty (id, prop) {
if (state[id]) {
delete state[id][prop]
}
}
}

module.exports = {
getAll: getters.all,
getProperty: getters.property,
deleteProperty: setters.deleteProperty,
RED: getters.RED,
setConfig: setters.setConfig,
set: setters.set,
Expand Down
Loading
Loading