1999 lines
84 KiB
HTML
1999 lines
84 KiB
HTML
<style>
|
|
.red-ui-editor {
|
|
--nrdb-node-light: rgb(160, 230, 236);
|
|
--nrdb-node-medium: rgb(90, 210, 220);
|
|
--nrdb-node-dark: rgb(39, 183, 195);
|
|
--nrdb-node-darkest: rgb(32 160 170);
|
|
}
|
|
.red-ui-editor .form-row-flex {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 4px;
|
|
}
|
|
.red-ui-editor .form-row-flex input,
|
|
.red-ui-editor .form-row-flex label:not(:first-child) {
|
|
margin: 0;
|
|
width: auto;
|
|
}
|
|
.nrdb2-helptext {
|
|
font-size: 0.75rem;
|
|
line-height: 0.825rem;
|
|
color: var(--red-ui-tertiary-text-color);
|
|
font-style: italic;
|
|
}
|
|
.w-16 {
|
|
width: 16px;
|
|
}
|
|
#ff-node-red-dashboard {
|
|
--ff-grey-50: #F9FAFB;
|
|
--ff-grey-100: #F3F4F6;
|
|
--ff-grey-200: #E5E7EB;
|
|
position: absolute;
|
|
top: 1px;
|
|
bottom: 2px;
|
|
left: 1px;
|
|
right: 1px;
|
|
overflow-y: auto;
|
|
}
|
|
#ff-node-red-dashboard .red-ui-sidebar-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
}
|
|
#ff-node-red-dashboard .red-ui-sidebar-header label {
|
|
margin-bottom: 0;
|
|
}
|
|
#ff-node-red-dashboard .red-ui-sidebar-header-actions {
|
|
gap: 4px;
|
|
display: flex;
|
|
}
|
|
#ff-node-red-dashboard .red-ui-editableList-container {
|
|
padding: 0;
|
|
}
|
|
/* don't show border for nexted editable lists */
|
|
.red-ui-editableList-border .red-ui-editableList-border {
|
|
border: 0;
|
|
}
|
|
#node-config-client-constraints-nodes .red-ui-treeList-label.selected {
|
|
background-color: #e3f2fd;
|
|
}
|
|
/* Dashboard 2.0 Sidebar */
|
|
.nrdb2-sb-list-button-group {
|
|
display: flex;
|
|
gap: 4px;
|
|
}
|
|
.nrdb2-sidebar-tab-content {
|
|
padding: 8px 10px;
|
|
height: 100%;
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
#dashboard-2-client-constraints a {
|
|
color: blue;
|
|
}
|
|
#dashboard-2-client-constraints a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
.nrdb2-layout-helptext {
|
|
padding: 0 0 9px;
|
|
font-style: italic;
|
|
color: #a2a2a2;
|
|
font-size: 8pt;
|
|
line-height: 12pt;
|
|
}
|
|
.nrdb2-sidebar-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 9px;
|
|
}
|
|
.nrdb2-sb-pages-list li {
|
|
padding: 0;
|
|
border-bottom: 0;
|
|
}
|
|
.nrdb2-sb-unattached-groups-list li {
|
|
padding: 0;
|
|
border-bottom: 0;
|
|
background-color: #ffefef;
|
|
}
|
|
.nrdb2-sb-list-header {
|
|
display: flex;
|
|
gap: 6px;
|
|
align-items: center;
|
|
padding: 9px 6px;
|
|
cursor: pointer;
|
|
}
|
|
.nrdb2-sb-list-header.nrdb2-sb-pages-list-header {
|
|
border-top: 1px solid var(--red-ui-primary-border-color, --ff-grey-200);
|
|
border-bottom: 1px solid var(--red-ui-primary-border-color, --ff-grey-200);
|
|
}
|
|
.nrdb2-sb-list-header .nrdb2-sb-title {
|
|
text-overflow: ellipsis;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
}
|
|
.nrdb2-sb-list-header .nrdb2-sb-info {
|
|
font-size: 0.75rem;
|
|
color: var(--red-ui-tertiary-text-color);
|
|
}
|
|
.nrdb2-sb-list-header .nrdb2-sb-palette {
|
|
display: flex;
|
|
gap: 2px;
|
|
}
|
|
.nrdb2-sb-list-header .nrdb2-sb-palette-color {
|
|
width: 12px;
|
|
height: 12px;
|
|
background-color: black;
|
|
border: 1px solid #d4d4d4;
|
|
border-radius: 2px;
|
|
}
|
|
.nrdb2-sb-list-header-actions {
|
|
position: absolute;
|
|
gap: 4px;
|
|
right: 1rem;
|
|
display: flex;
|
|
gap: 4px;
|
|
}
|
|
.nrdb2-sb-list-header-actions a {
|
|
cursor: pointer;
|
|
}
|
|
.nrdb2-sb-list-header-state-options {
|
|
display: flex;
|
|
gap: 4px;
|
|
}
|
|
.nrdb2-sb-list-header-button-group {
|
|
right: 1rem;
|
|
display: flex;
|
|
gap: 4px;
|
|
}
|
|
.nrdb2-sb-list-header-button-group,
|
|
.nrdb2-sb-list-handle {
|
|
opacity: 0;
|
|
transition: 0.15s opacity;
|
|
}
|
|
.nrdb2-sb-list-header:hover {
|
|
background-color: var(--red-ui-secondary-background-hover, --ff-grey-100);
|
|
}
|
|
.nrdb2-sb-list-header:hover .nrdb2-sb-list-handle,
|
|
.nrdb2-sb-list-header:hover .nrdb2-sb-list-header-button-group {
|
|
opacity: 1;
|
|
}
|
|
.nrdb2-sb-item-hidden .nrdb2-sb-title {
|
|
text-decoration: line-through;
|
|
}
|
|
.nrdb2-sb-item-disabled .nrdb2-sb-title,
|
|
.nrdb2-sb-item-disabled .nrdb2-sb-info {
|
|
opacity: 0.5;
|
|
}
|
|
/* indent the groups */
|
|
.nrdb2-sb-groups-list-header .nrdb2-sb-list-chevron {
|
|
margin-left: 1.5rem;
|
|
}
|
|
/* indent the widgets */
|
|
.nrdb2-sb-widgets-list-header .nrdb2-sb-widget-icon {
|
|
margin-left: 3.5rem;
|
|
display: inline-block;
|
|
height: 14px;
|
|
width: 14px;
|
|
text-align: center;
|
|
}
|
|
/* common styles for images */
|
|
.nrdb2-sb-widgets-list-header .nrdb2-sb-widget-icon.nrdb2-sb-widget-icon-img {
|
|
background-color: currentColor;
|
|
display: inline-block;
|
|
mask-size: contain;
|
|
mask-position: center;
|
|
mask-repeat: no-repeat;
|
|
height: 18px;
|
|
width: 18px;
|
|
margin-right: -0.15rem;
|
|
margin-left: 3.35rem;
|
|
}
|
|
#nrdb2-sb-client-data-providers {
|
|
padding-left: 24px;
|
|
}
|
|
#nrdb2-sb-client-data-providers label {
|
|
cursor: default;
|
|
}
|
|
#nrdb2-sb-client-data-providers-list {
|
|
list-style: none;
|
|
margin: 0;
|
|
margin-bottom: 12px;
|
|
border-radius: 4px;
|
|
padding: 4px;
|
|
border: 1px solid var(--red-ui-form-input-border-color);
|
|
}
|
|
#nrdb2-sb-client-data-providers-list li {
|
|
display: flex;
|
|
gap: 6px;
|
|
align-items: center;
|
|
}
|
|
#nrdb2-sb-client-data-providers-list li:not(:first-child){
|
|
border-top: 1px solid #e6e6e6;
|
|
}
|
|
#nrdb2-sb-client-data-providers-list li .fa {
|
|
color: var(--red-ui-tertiary-text-color);
|
|
font-size: 0.75rem;
|
|
}
|
|
#nrdb2-sb-client-data-providers-list li label {
|
|
margin: 0;
|
|
white-space: nowrap;
|
|
}
|
|
.nrdb2-sb-client-data-provider-package {
|
|
color: var(--red-ui-tertiary-text-color);
|
|
font-size: 0.75rem;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
}
|
|
#node-config-client-constraints-nodes {
|
|
flex-grow: 1;
|
|
width: 100%;
|
|
}
|
|
#node-config-client-constraints-nodes-container {
|
|
flex-grow: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
</style>
|
|
|
|
|
|
<script type="text/javascript">
|
|
// #region typedefs for intellisense and better code completion/DX
|
|
|
|
/**
|
|
* @typedef {Object} DashboardItem - A widget/group/page/subflow item
|
|
* @property {String} itemType - The type of item (e.g. 'widget', 'group', 'page', 'link')
|
|
* @property {String} id - The unique id of the item
|
|
* @property {String} name - The name of the item
|
|
* @property {String} type - The type of the item (e.g. 'ui-button', 'ui-template', 'ui-group', 'ui-page')
|
|
* @property {Number} order
|
|
* @property {String} label
|
|
* @property {String} color
|
|
* @property {String} [group] - The group id that this widget belongs to
|
|
* @property {String} [page] - The page id that this group belongs to
|
|
* @property {String} [theme] - The theme id that this page belongs to
|
|
* @property {Boolean} [isSubflowInstance] - Whether or not this item is a subflow instance
|
|
* @property {String} [subflowName] - The name give to the subflow template (only applicable when `isSubflowInstance` is `true`)
|
|
* @property {Object} node - The actual node or subflow instance that this DashboardItem represents
|
|
* @global
|
|
*/
|
|
|
|
/** @typedef {Object<string, Array<DashboardItem>>} DashboardItemLookup */
|
|
// #endregion
|
|
|
|
(function () {
|
|
const sidebarContainer = '<div style="position: relative; height: 100%;"></div>'
|
|
const sidebarContentTemplate = $('<div id="ff-node-red-dashboard"></div>').appendTo(sidebarContainer)
|
|
const sidebar = $(sidebarContentTemplate)
|
|
|
|
// convert to i18 text
|
|
function c_ (x) {
|
|
return RED._('@flowfuse/node-red-dashboard/ui-base:ui-base.' + x)
|
|
}
|
|
|
|
function hasProperty (obj, prop) {
|
|
return Object.prototype.hasOwnProperty.call(obj, prop)
|
|
}
|
|
function debounce (func, wait, immediate) {
|
|
let timeout
|
|
return function () {
|
|
const context = this; const args = arguments
|
|
const later = function () {
|
|
timeout = null
|
|
if (!immediate) func.apply(context, args)
|
|
}
|
|
const callNow = immediate && !timeout
|
|
clearTimeout(timeout)
|
|
timeout = setTimeout(later, wait)
|
|
if (callNow) func.apply(context, args)
|
|
}
|
|
}
|
|
|
|
RED.nodes.registerType('ui-base', {
|
|
category: 'config',
|
|
defaults: {
|
|
name: {
|
|
value: c_('label.uiName'),
|
|
required: true
|
|
},
|
|
path: {
|
|
value: '/dashboard',
|
|
required: true
|
|
},
|
|
includeClientData: {
|
|
value: true
|
|
},
|
|
acceptsClientConfig: {
|
|
value: ['ui-notification', 'ui-control']
|
|
},
|
|
showPathInSidebar: {
|
|
value: false
|
|
},
|
|
showPageTitle: {
|
|
value: true
|
|
},
|
|
navigationStyle: {
|
|
value: 'default'
|
|
},
|
|
titleBarStyle: {
|
|
value: 'default'
|
|
}
|
|
},
|
|
label: function () {
|
|
return `${this.name} [${this.path}]` || 'UI Config'
|
|
},
|
|
oneditprepare: function () {
|
|
// backward compatibility for navigation style
|
|
if (!this.titleBarStyle) {
|
|
// set to default
|
|
this.titleBarStyle = 'default'
|
|
// update the jquery dropdown
|
|
$('#node-config-input-titleBarStyle').val('default')
|
|
}
|
|
|
|
// backward compatibility for including page name
|
|
if (this.showPageTitle === undefined) {
|
|
// set to true
|
|
this.showPageTitle = true
|
|
// update the jquery checkbox
|
|
$('#node-config-input-showPageTitle').prop('checked', true)
|
|
}
|
|
},
|
|
onpaletteadd: function () {
|
|
// add the Dashboard 2.0 sidebar
|
|
if (RED._db2debug) { console.log('dashboard 2: ui_base.html: onpaletteadd ()') }
|
|
addSidebar()
|
|
}
|
|
})
|
|
|
|
/**
|
|
* Utility function to convert a dashboard node to a DashboardItem
|
|
* Provides a better DX with type checking and intellisense
|
|
* @param {Object} node - The node to convert
|
|
* @returns {DashboardItem}
|
|
*/
|
|
function toDashboardItem (node) {
|
|
if (RED._db2debug) { console.log('dashboard 2: ui_base.html: toDashboardItem (node)', node) }
|
|
/** @type {DashboardItem} */
|
|
const item = {
|
|
itemType: 'widget',
|
|
id: node.id,
|
|
name: node.name,
|
|
type: node.type,
|
|
order: node.order,
|
|
label: null,
|
|
icon: null,
|
|
color: null,
|
|
isSubflowInstance: false,
|
|
node
|
|
}
|
|
if (hasProperty(node, 'group')) { item.group = node.group }
|
|
if (hasProperty(node, 'page')) { item.page = node.page }
|
|
if (hasProperty(node, 'link')) { item.link = node.link }
|
|
if (hasProperty(node, 'theme')) { item.theme = node.theme }
|
|
if (hasProperty(node, 'env') && Array.isArray(node.env) && /subflow:.+/.test(node.type)) {
|
|
const envOrder = node.env.find(e => e.key === 'DB2_SF_ORDER')
|
|
if (envOrder) {
|
|
item.order = envOrder.value
|
|
}
|
|
}
|
|
switch (node.type) {
|
|
case 'ui-page':
|
|
case 'ui-link':
|
|
case 'ui-group':
|
|
case 'ui-theme':
|
|
case 'ui-base':
|
|
item.itemType = node.type.replace('ui-', '')
|
|
break
|
|
default:
|
|
item.itemType = 'widget'
|
|
break
|
|
}
|
|
try {
|
|
item.order = parseInt(item.order)
|
|
} finally {
|
|
item.order = isNaN(item.order) ? 0 : item.order
|
|
}
|
|
try {
|
|
item.label = RED.utils.getNodeLabel(node)
|
|
} finally {
|
|
item.label = item.label || node.type || node.id
|
|
}
|
|
try {
|
|
item.color = RED.utils.getNodeColor(node)
|
|
} catch (_err) { }
|
|
return item
|
|
}
|
|
|
|
/**
|
|
* Add Custom Dashboard Side Menu
|
|
* */
|
|
|
|
function dashboardLink (id, name, path) {
|
|
const base = RED.settings.httpNodeRoot || '/'
|
|
const basePart = base.endsWith('/') ? base : `${base}/`
|
|
const dashPart = path.startsWith('/') ? path.slice(1) : path
|
|
const fullPath = `${basePart}${dashPart}`
|
|
|
|
const header = $('<div class="red-ui-sidebar-header"></div>')
|
|
const label = $('<label></label>').text(name)
|
|
|
|
const actions = $('<div class="red-ui-sidebar-header-actions"></div>')
|
|
|
|
const editSettingsButton = $('<a id="edit-ui-base" class="editor-button editor-button-small nr-db-sb-list-header-button">' +
|
|
c_('label.editSettings') + ' <i style="margin-left: 3px;" class="fa fa-cog"></i></a>')
|
|
|
|
editSettingsButton.on('click', function () {
|
|
RED.editor.editConfig('', 'ui-base', id)
|
|
})
|
|
|
|
const openDashboardButton = $(`<a id="open-dashboard" href="${fullPath}" target="nr-dashboard" class="editor-button editor-button-small nr-db-sb-list-header-button">` +
|
|
c_('label.openDashboard') + ' <i style="margin-left: 3px;" class="fa fa-external-link"></i></a>')
|
|
|
|
label.appendTo(header)
|
|
editSettingsButton.appendTo(actions)
|
|
openDashboardButton.appendTo(actions)
|
|
actions.appendTo(header)
|
|
return header
|
|
}
|
|
|
|
/**
|
|
* Add an editor to control the ordering of groups & widgets
|
|
*/
|
|
function updateItemOrder (items, events) {
|
|
if (RED._db2debug) { console.log('dashboard 2: ui_base.html: updateItemOrder (items, events)', items, events) }
|
|
items.each((i, el) => {
|
|
/** @type {DashboardItem} */
|
|
const dbItem = el.data('data')
|
|
const node = dbItem?.node || {}
|
|
const setNodeOrder = (newOrder) => {
|
|
dbItem.order = newOrder
|
|
if (dbItem.isSubflowInstance) {
|
|
node.env = node.env || []
|
|
const envOrder = node.env.find(e => e.key === 'DB2_SF_ORDER')
|
|
if (envOrder) {
|
|
envOrder.value = newOrder
|
|
} else {
|
|
node.env.push({ key: 'DB2_SF_ORDER', value: '' + newOrder, type: 'str' }) // db2 sort order
|
|
}
|
|
} else {
|
|
node.order = newOrder
|
|
}
|
|
}
|
|
const getNodeOrder = () => {
|
|
let order = dbItem.order // node.order
|
|
if (dbItem.isSubflowInstance && node.env) {
|
|
const envOrder = node.env.find(e => e.key === 'DB2_SF_ORDER')
|
|
if (envOrder) { order = envOrder.value }
|
|
}
|
|
if (typeof order === 'string') { order = parseInt(order) }
|
|
if (isNaN(order)) { order = 0 }
|
|
return order || 0
|
|
}
|
|
const oldOrder = getNodeOrder()
|
|
if (oldOrder !== i + 1) {
|
|
const wasDirty = node.dirty
|
|
const wasChanged = node.changed
|
|
// update Node-RED node properties
|
|
setNodeOrder(i + 1)
|
|
node.dirty = true
|
|
node.changed = true
|
|
// generate a history event
|
|
const hev = {
|
|
t: 'edit',
|
|
node,
|
|
changes: {
|
|
order: oldOrder
|
|
},
|
|
dirty: wasDirty,
|
|
changed: wasChanged
|
|
}
|
|
events.push(hev)
|
|
}
|
|
})
|
|
}
|
|
|
|
// toggle slide tab group content
|
|
const titleToggle = function (id, content, chevron) {
|
|
return function (evt) {
|
|
if (content.is(':visible')) {
|
|
content.slideUp()
|
|
chevron.css({ transform: 'rotate(-90deg)' })
|
|
content.addClass('nr-db-sb-collapsed')
|
|
} else {
|
|
content.slideDown()
|
|
chevron.css({ transform: '' })
|
|
content.removeClass('nr-db-sb-collapsed')
|
|
}
|
|
}
|
|
}
|
|
|
|
// Utility function to store events in NR history, trigger a redraw, and detect if a re-deploy is necessary
|
|
function recordEvents (events) {
|
|
if (events.length === 0) { return } // nothing to record
|
|
|
|
// note the state of the editor before pushing to history
|
|
const isDirty = RED.nodes.dirty()
|
|
if (RED._db2debug) { console.log('dashboard 2: recordEvents ()', isDirty, events) }
|
|
|
|
// add our changes to NR history and trigger whether or not we need to redeploy
|
|
RED.history.push({
|
|
t: 'multi',
|
|
events,
|
|
dirty: isDirty
|
|
})
|
|
RED.nodes.dirty(true)
|
|
RED.view.redraw()
|
|
}
|
|
|
|
function checkDuplicateUiBases () {
|
|
// check how many ui-bases we have and trim if already too many
|
|
const bases = []
|
|
const pages = []
|
|
RED.nodes.eachConfig((n) => {
|
|
if (n.type === 'ui-base') { bases.push(n) }
|
|
if (n.type === 'ui-page') { pages.push(n) }
|
|
})
|
|
|
|
// the eachConfig is in creation order, so we can remove from the end
|
|
// and keep the oldest ones
|
|
while (bases.length > 1) {
|
|
const n = bases.pop()
|
|
if (RED._db2debug) { console.log('ui-base removed', n) }
|
|
RED.nodes.remove(n.id)
|
|
RED.nodes.dirty(true)
|
|
}
|
|
|
|
if (bases.length > 0) {
|
|
const baseId = bases[0].id
|
|
// loop over pages and re-map the ui-base to the only ui-base available
|
|
pages.forEach((page) => {
|
|
page.ui = baseId
|
|
})
|
|
|
|
RED.nodes.eachNode((node) => {
|
|
if (node.type.startsWith('ui-')) {
|
|
// check if the widgets are ui-scoped, and have a ui set to the removed ui-base nodes
|
|
if (node.ui && node.ui !== '' && node.ui !== baseId) {
|
|
node.ui = baseId
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
function addConfigNode (node) {
|
|
if (!node.users) {
|
|
node.users = []
|
|
}
|
|
node.dirty = true
|
|
RED.nodes.add(node)
|
|
RED.editor.validateNode(node)
|
|
RED.history.push({
|
|
t: 'add',
|
|
nodes: [node.id],
|
|
dirty: RED.nodes.dirty()
|
|
})
|
|
RED.nodes.dirty(true)
|
|
}
|
|
|
|
function mapDefaults (defaults) {
|
|
const values = {}
|
|
for (const key in defaults) {
|
|
if (Object.prototype.hasOwnProperty.call(defaults, key)) {
|
|
values[key] = defaults[key].value
|
|
}
|
|
}
|
|
return values
|
|
}
|
|
|
|
function getConfigNodesByType (type) {
|
|
const nodes = []
|
|
RED.nodes.eachConfig((n) => {
|
|
if ((type instanceof String) && type === n.type) {
|
|
nodes.push(n)
|
|
} else {
|
|
// we have an array of types
|
|
if (type.includes(n.type)) {
|
|
nodes.push(n)
|
|
}
|
|
}
|
|
})
|
|
return nodes
|
|
}
|
|
|
|
function addDefaultPage (baseId, themeId) {
|
|
const page = RED.nodes.getType('ui-page')
|
|
// get all pages
|
|
const entries = getConfigNodesByType(['ui-page', 'ui-link'])
|
|
const pageNumber = entries.length + 1
|
|
const pageNode = {
|
|
_def: page,
|
|
id: RED.nodes.id(),
|
|
type: 'ui-page',
|
|
...mapDefaults(page.defaults),
|
|
path: `/page${pageNumber}`, // TODO: generate a unique path
|
|
name: `Page ${pageNumber}`,
|
|
ui: baseId,
|
|
theme: themeId,
|
|
layout: 'grid',
|
|
order: pageNumber
|
|
}
|
|
|
|
addConfigNode(pageNode)
|
|
return pageNode
|
|
}
|
|
|
|
function addDefaultLink (baseId) {
|
|
const link = RED.nodes.getType('ui-link')
|
|
const linkNode = {
|
|
_def: link,
|
|
id: RED.nodes.id(),
|
|
type: 'ui-link',
|
|
...mapDefaults(link.defaults),
|
|
path: '/',
|
|
name: 'Link',
|
|
ui: baseId
|
|
}
|
|
|
|
addConfigNode(linkNode)
|
|
return linkNode
|
|
}
|
|
|
|
function addDefaultGroup (pageId) {
|
|
const group = RED.nodes.getType('ui-group')
|
|
const groupNode = {
|
|
_def: group,
|
|
id: RED.nodes.id(),
|
|
type: 'ui-group',
|
|
...mapDefaults(group.defaults),
|
|
name: 'My Group',
|
|
page: pageId
|
|
}
|
|
|
|
addConfigNode(groupNode)
|
|
return groupNode
|
|
}
|
|
|
|
function addDefaultTheme () {
|
|
const theme = RED.nodes.getType('ui-theme')
|
|
const themeNode = {
|
|
_def: theme,
|
|
id: RED.nodes.id(),
|
|
type: 'ui-theme',
|
|
...mapDefaults(theme.defaults),
|
|
name: 'Default Theme'
|
|
}
|
|
|
|
addConfigNode(themeNode)
|
|
return themeNode
|
|
}
|
|
|
|
function addLayoutsDefaults () {
|
|
const cNodes = ['ui-base', 'ui-page', 'ui-group', 'ui-theme']
|
|
let exists = false
|
|
RED.nodes.eachConfig((n) => {
|
|
if (cNodes.includes(n.type)) {
|
|
exists = true
|
|
}
|
|
})
|
|
|
|
// check if we haven't got any of these yet
|
|
if (!exists) {
|
|
// Add Single Base Node
|
|
const base = RED.nodes.getType('ui-base')
|
|
const baseNode = {
|
|
_def: base,
|
|
id: RED.nodes.id(),
|
|
type: 'ui-base',
|
|
...mapDefaults(base.defaults),
|
|
name: 'My Dashboard'
|
|
}
|
|
|
|
addConfigNode(baseNode)
|
|
|
|
const theme = addDefaultTheme()
|
|
const page = addDefaultPage(baseNode.id, theme.id)
|
|
const group = addDefaultGroup(page.id)
|
|
|
|
// update existing `ui-` nodes to use the new base/page/theme/group
|
|
RED.nodes.eachNode((node) => {
|
|
if (node.type.startsWith('ui-')) {
|
|
// if node has a group property
|
|
if (hasProperty(node._def.defaults, 'group') && !node.group) {
|
|
// group-scoped widgets - which is most of them
|
|
node.group = group.id
|
|
} else if (hasProperty(node._def.defaults, 'page') && !node.page) {
|
|
// page-scoped widgets
|
|
node.page = page.id
|
|
} else if (hasProperty(node._def.defaults, 'ui') && !node.ui) {
|
|
// base-scoped widgets, e.g. ui-notification/control
|
|
node.ui = baseNode.id
|
|
}
|
|
RED.editor.validateNode(node)
|
|
}
|
|
})
|
|
|
|
RED.view.redraw()
|
|
RED.sidebar.config.refresh()
|
|
}
|
|
}
|
|
|
|
// watch for nodes changed, added, removed - use this to refresh the sidebar
|
|
let refreshBusy = false // this is set/reset inside refreshSidebarEditors
|
|
const refreshSidebarEditorDebounced = debounce(refreshSidebarEditors, 300)
|
|
/**
|
|
* Conditional refresh of the sidebar editor
|
|
* The refresh is only triggered if the event is a ui- node or a subflow that contains a ui- node
|
|
* Calls are debounced to prevent multiple calls to refreshSidebarEditors
|
|
* @param {Object} node - The node that was changed/added/removed
|
|
* @param {String} eventName - The name of the event that was fired
|
|
*/
|
|
const conditionalSidebarRefresh = function (node, eventName) {
|
|
// if the layout editor is not in view, don't refresh
|
|
if ($('#ff-node-red-dashboard').parent().css('display') === 'none') { return }
|
|
// if a refresh is in progress, don't refresh
|
|
if (refreshBusy) { return }
|
|
if (RED._db2debug) { console.log('dashboard 2: conditionalSidebarRefresh (node, eventName)', node, eventName) }
|
|
|
|
// first, check if the node.dirty flag is set or if the event is a nodes:remove (also ensure the node has a type property)
|
|
if ((node.dirty || eventName === 'nodes:remove') && node.type) {
|
|
// check if the node is a ui- node
|
|
let refresh = node.type.startsWith('ui-')
|
|
// if this is not a ui- node, it is perhaps a subflow?
|
|
if (!refresh && node.type.startsWith('subflow:')) {
|
|
// lets see if it has env vars linked to a `ui-` config or it has a DB2_SF_ORDER sort order key?
|
|
const subflowId = node.type.split(':')[1]
|
|
if (node.env && node.env.find(e => e.type?.startsWith('ui-') || node.env.find(e => e.key === 'DB2_SF_ORDER'))) {
|
|
refresh = true
|
|
}
|
|
if (!refresh) {
|
|
// check if the subflow definition contains any ui- nodes
|
|
const subflowChildren = RED.nodes.filterNodes({ z: subflowId })
|
|
if (subflowChildren.some(n => n.type.startsWith('ui-'))) {
|
|
refresh = true
|
|
}
|
|
}
|
|
}
|
|
if (refresh) {
|
|
if (RED._db2debug) { console.log(`dashboard 2: ${eventName} - this is a ui- node! queuing a call to refreshSidebarEditors`) }
|
|
// debounce the call to refreshSidebarEditors as multiple events can be fired in quick succession
|
|
refreshSidebarEditorDebounced()
|
|
}
|
|
}
|
|
}
|
|
|
|
RED.events.on('nodes:change', function (event) {
|
|
conditionalSidebarRefresh(event, 'nodes:change')
|
|
})
|
|
|
|
const addLayoutsDefaultsDebounced = debounce(addLayoutsDefaults, 25)
|
|
const checkDuplicateUiBasesDebounced = debounce(checkDuplicateUiBases, 25)
|
|
RED.events.on('nodes:add', function (node) {
|
|
if (RED._db2debug) { console.log('nodes:add', node) }
|
|
if (node.dirty && node.type && node.type.startsWith('ui-')) {
|
|
if (RED._db2debug) { console.log('nodes:add - this is a ui- node! queuing a call to refreshSidebarEditors') }
|
|
// debounce the call to refreshSidebarEditors as multiple events can be fired in quick succession
|
|
refreshSidebarEditorDebounced()
|
|
}
|
|
|
|
// if we're adding a ui-base
|
|
if (node.type.startsWith('ui-')) {
|
|
// action on all ui- elements to ensure we've remapped (now) missing ui-base nodes
|
|
checkDuplicateUiBasesDebounced()
|
|
addLayoutsDefaultsDebounced()
|
|
}
|
|
})
|
|
|
|
RED.events.on('nodes:remove', function (event) {
|
|
conditionalSidebarRefresh(event, 'nodes:remove')
|
|
})
|
|
|
|
/**
|
|
* Add group of actions to the right-side of a row in the sidebar editable list.
|
|
* @param {Object} parent - jQuery object to add this button group as a child element to
|
|
* @param {DashboardItem} item - The page/group/widget that these actions are bound to
|
|
*/
|
|
function addRowActions (parent, item, list) {
|
|
const configNodes = ['ui-base', 'ui-page', 'ui-link', 'ui-group', 'ui-theme']
|
|
const btnGroup = $('<div>', { class: 'nrdb2-sb-list-header-button-group', id: item.id }).appendTo(parent)
|
|
if (!configNodes.includes(item.type)) {
|
|
const focusButton = $('<a href="#" class="nr-db-sb-tab-focus-button editor-button editor-button-small nr-db-sb-list-header-button"><i class="fa fa-bullseye"></i> ' + c_('layout.focus') + '</a>').appendTo(btnGroup)
|
|
focusButton.on('click', function (evt) {
|
|
RED.view.reveal(item.id)
|
|
evt.stopPropagation()
|
|
evt.preventDefault()
|
|
})
|
|
}
|
|
const editButton = $('<a href="#" class="nr-db-sb-tab-edit-button editor-button editor-button-small nr-db-sb-list-header-button"><i class="fa fa-pencil"></i> ' + c_('layout.edit') + '</a>').appendTo(btnGroup)
|
|
editButton.on('click', function (evt) {
|
|
if (configNodes.includes(item.type)) {
|
|
RED.editor.editConfig('', item.type, item.id)
|
|
} else {
|
|
RED.editor.edit(item?.node)
|
|
}
|
|
evt.stopPropagation()
|
|
evt.preventDefault()
|
|
})
|
|
if (item.type === 'ui-page') {
|
|
// add the "+ group" button
|
|
$('<a href="#" class="editor-button editor-button-small nr-db-sb-list-header-button"><i class="fa fa-plus"></i> ' + c_('layout.group') + '</a>')
|
|
.click(function (evt) {
|
|
list.editableList('addItem')
|
|
evt.preventDefault()
|
|
})
|
|
.appendTo(btnGroup)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add group of actions to the right-side of a row in the sidebar editable list.
|
|
* @param {Object} parent - jQuery object to add this button group as a child element to
|
|
* @param {DashboardItem} dashboardItem - The page/group/widget that these actions are bound to
|
|
*/
|
|
function addRowStateOptions (parent, dashboardItem) {
|
|
const item = dashboardItem.node
|
|
const nodes = ['ui-page', 'ui-link', 'ui-group']
|
|
const titleRow = parent.closest('.nrdb2-sb-list-header')
|
|
const btnGroup = $('<div>', { class: 'nrdb2-sb-list-header-state-options', id: item.id }).appendTo(parent)
|
|
if (nodes.includes(item.type)) {
|
|
const visibleIcon = (item.visible === 'false' || item.visible === false) ? 'fa-eye-slash' : 'fa-eye'
|
|
const visibleBtn = $('<a href="#" title="Hide" class="nr-db-sb-tab-visible-button editor-button editor-button-small nr-db-sb-list-header-button"><i class="fa ' + visibleIcon + '"></i></a>').appendTo(btnGroup)
|
|
visibleBtn.on('click', function (evt) {
|
|
const events = []
|
|
|
|
evt.stopPropagation()
|
|
evt.preventDefault()
|
|
|
|
if (item.visible === 'true' || item.visible === true || item.visible === undefined) {
|
|
// toggle to hidden
|
|
item.visible = false
|
|
titleRow.addClass('nrdb2-sb-item-hidden')
|
|
$(this).prop('title', 'Show')
|
|
$(this).find('i').addClass('fa-eye-slash').removeClass('fa-eye')
|
|
events.push({
|
|
t: 'edit',
|
|
node: item,
|
|
changes: {
|
|
visible: true
|
|
},
|
|
dirty: item.dirty,
|
|
changed: item.changed
|
|
})
|
|
} else {
|
|
// toggle to shown
|
|
item.visible = true
|
|
titleRow.removeClass('nrdb2-sb-item-hidden')
|
|
$(this).prop('title', 'Hide')
|
|
$(this).find('i').addClass('fa-eye').removeClass('fa-eye-slash')
|
|
events.push({
|
|
t: 'edit',
|
|
node: item,
|
|
changes: {
|
|
visible: false
|
|
},
|
|
dirty: item.dirty,
|
|
changed: item.changed
|
|
})
|
|
}
|
|
recordEvents(events)
|
|
})
|
|
const disabledIcon = (item.disabled === 'true' || item.disabled === true) ? 'fa-lock' : 'fa-unlock'
|
|
const disabledBtn = $('<a href="#" class="nr-db-sb-tab-visible-button editor-button editor-button-small nr-db-sb-list-header-button"><i class="fa ' + disabledIcon + '"></i></a>').appendTo(btnGroup)
|
|
disabledBtn.on('click', function (evt) {
|
|
const events = []
|
|
|
|
evt.stopPropagation()
|
|
evt.preventDefault()
|
|
|
|
if (item.disabled === 'true' || item.disabled === true) {
|
|
// toggle to hidden
|
|
item.disabled = false
|
|
titleRow.removeClass('nrdb2-sb-item-disabled')
|
|
$(this).prop('title', 'Disable')
|
|
$(this).find('i').addClass('fa-unlock').removeClass('fa-lock')
|
|
events.push({
|
|
t: 'edit',
|
|
node: item,
|
|
changes: {
|
|
disabled: true
|
|
},
|
|
dirty: item.dirty,
|
|
changed: item.changed
|
|
})
|
|
} else {
|
|
// toggle to shown
|
|
item.disabled = true
|
|
titleRow.addClass('nrdb2-sb-item-disabled')
|
|
$(this).prop('title', 'Enable')
|
|
$(this).find('i').addClass('fa-lock').removeClass('fa-unlock')
|
|
events.push({
|
|
t: 'edit',
|
|
node: item,
|
|
changes: {
|
|
disabled: false
|
|
},
|
|
dirty: item.dirty,
|
|
changed: item.changed
|
|
})
|
|
}
|
|
recordEvents(events)
|
|
})
|
|
}
|
|
}
|
|
|
|
function setStateClasses (item, row) {
|
|
if (!item.node.visible === 'true' || item.node.visible === false) {
|
|
row.addClass('nrdb2-sb-item-hidden')
|
|
}
|
|
if (item.node.disabled === 'true' || item.node.disabled === true) {
|
|
row.addClass('nrdb2-sb-item-disabled')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds child list of groups for a given page
|
|
* @param {String} pageId - The id of the page that these groups belong to
|
|
* @param {Object} container - The jQuery object to append the groups list to
|
|
* @param {Object[]} groups - The list of groups to add to the list
|
|
* @param {DashboardItemLookup} widgetsByGroup - The lookup of widgets by group
|
|
*/
|
|
function addGroupOrderingList (pageId, container, groups, widgetsByGroup) {
|
|
// sort groups by order
|
|
groups.sort((a, b) => a.order - b.order)
|
|
|
|
// ordered list of groups to live within a container (e.g. page list item)
|
|
const groupsOL = $('<ol>', { class: 'nrdb2-sb-group-list' }).appendTo(container).editableList({
|
|
sortable: '.nrdb2-sb-groups-list-header',
|
|
addButton: false,
|
|
height: 'auto',
|
|
connectWith: '.nrdb2-sb-group-list',
|
|
addItem: function (container, i, group) {
|
|
if (!group || !group.id) {
|
|
// this is a new page that's been added and we need to setup the basics
|
|
group = addDefaultGroup(pageId)
|
|
RED.editor.editConfig('', group.type, group.id)
|
|
}
|
|
|
|
const widgets = widgetsByGroup[group.id] || []
|
|
|
|
// sort widgets by order
|
|
widgets.sort((a, b) => a.order - b.order)
|
|
|
|
const titleRow = $('<div>', { class: 'nrdb2-sb-list-header nrdb2-sb-groups-list-header' }).appendTo(container)
|
|
$('<i class="nrdb2-sb-list-handle nrdb2-sb-group-list-handle fa fa-bars"></i>').appendTo(titleRow)
|
|
const chevron = $('<i class="fa fa-angle-down nrdb2-sb-list-chevron">', { style: 'width:10px;' }).appendTo(titleRow)
|
|
const groupicon = 'fa-table'
|
|
$('<i>', { class: 'nrdb2-sb-icon nrdb2-sb-group-icon fa ' + groupicon }).appendTo(titleRow)
|
|
$('<span>', { class: 'nrdb2-sb-title' }).text(group.name || group.id).appendTo(titleRow)
|
|
$('<span>', { class: 'nrdb2-sb-info' }).text(`${widgets.length} Widgets`).appendTo(titleRow)
|
|
|
|
const actions = $('<div>', { class: 'nrdb2-sb-list-header-actions' }).appendTo(titleRow)
|
|
addRowActions(actions, group)
|
|
addRowStateOptions(actions, group)
|
|
|
|
// adds widgets within this group
|
|
const widgetsList = $('<div>', { class: 'nrdb2-sb-widget-list-container' }).appendTo(container)
|
|
|
|
// add chevron/list toggle
|
|
titleRow.click(titleToggle(group.id, widgetsList, chevron))
|
|
|
|
addWidgetToList(group.id, widgetsList, widgets)
|
|
|
|
const events = []
|
|
updateItemOrder(groupsOL.editableList('items'), events)
|
|
// add our changes to NR history and trigger whether or not we need to redeploy
|
|
recordEvents(events)
|
|
},
|
|
sortItems: function (items) {
|
|
// track any changes
|
|
const events = []
|
|
|
|
// check if we have any new widgets added to this list
|
|
items.each((i, el) => {
|
|
const dbItem = el.data('data')
|
|
const widget = dbItem?.node || {}
|
|
if (widget.page !== pageId) {
|
|
const oldPageId = widget.page
|
|
widget.page = pageId
|
|
events.push({
|
|
t: 'edit',
|
|
node: widget,
|
|
changes: {
|
|
page: oldPageId
|
|
},
|
|
dirty: widget.changed,
|
|
changed: widget.dirty
|
|
})
|
|
}
|
|
})
|
|
|
|
updateItemOrder(items, events)
|
|
|
|
// add our changes to NR history and trigger whether or not we need to redeploy
|
|
recordEvents(events)
|
|
},
|
|
sort: function (a, b) {
|
|
return Number(a.order) - Number(b.order)
|
|
}
|
|
})
|
|
|
|
groups.forEach(function (group) {
|
|
if (RED._db2debug) { if (RED._db2debug) { console.log('dashboard 2: ui_base.html: addGroupOrderingList: adding group', group) } }
|
|
groupsOL.editableList('addItem', group)
|
|
})
|
|
|
|
return groupsOL
|
|
}
|
|
|
|
/**
|
|
* Adds list of widgets underneath a group
|
|
* @param {String} groupId - The id of the group that these widgets belong to
|
|
* @param {JQuery} container - The jQuery object to append the widgets list to
|
|
* @param {DashboardItem[]} widgets - The list of widgets to add to the list
|
|
*/
|
|
function addWidgetToList (groupId, container, widgets) {
|
|
// ordered list of groups to live within a container (e.g. page list item)
|
|
const widgetsOL = $('<ol>', { class: 'nrdb2-sb-widget-list' }).appendTo(container).editableList({
|
|
sortable: '.nrdb2-sb-widgets-list-header',
|
|
addButton: false,
|
|
height: 'auto',
|
|
connectWith: '.nrdb2-sb-widget-list',
|
|
addItem: function (container, i, /** @type {DashboardItem} */ widget) {
|
|
const titleRow = $('<div>', { class: 'nrdb2-sb-list-header nrdb2-sb-widgets-list-header' }).appendTo(container)
|
|
$('<i class="nrdb2-sb-list-handle nrdb2-sb-widget-list-handle fa fa-bars"></i>').appendTo(titleRow)
|
|
|
|
// Set the icon
|
|
const ico = $('<i>', { class: 'nrdb2-sb-icon nrdb2-sb-widget-icon' }).appendTo(titleRow)
|
|
let widgetIcon = RED.utils.getNodeIcon(widget.node?._def, widget.node) || 'fa-question'
|
|
if (widgetIcon.startsWith('font-awesome/')) {
|
|
widgetIcon = widgetIcon.replace(/^font-awesome\//, 'fa ')
|
|
}
|
|
if (widget.isSubflowInstance) {
|
|
// In this MVP, subflow instances are constrained to stay within own group.
|
|
// This is achieved using the class `red-ui-editableList-item-constrained`
|
|
container.parent().addClass('red-ui-editableList-item-constrained')
|
|
titleRow.attr('title', `${(widget.subflowName || '').trim()} (subflow instance)`.trim())
|
|
}
|
|
if (/.*\.(png|gif|jpg|jpeg|svg)$/.test(widgetIcon)) {
|
|
ico.css('mask-image', `url("${widgetIcon}")`)
|
|
ico.addClass('nrdb2-sb-widget-icon-img')
|
|
} else if (widgetIcon.startsWith('fa fa-')) {
|
|
ico.addClass(widgetIcon)
|
|
}
|
|
|
|
$('<span>', { class: 'nrdb2-sb-title' }).text(widget.label?.trim() || widget.id).appendTo(titleRow)
|
|
const actions = $('<div>', { class: 'nrdb2-sb-list-header-actions' }).appendTo(titleRow)
|
|
addRowActions(actions, widget)
|
|
|
|
const events = []
|
|
updateItemOrder(widgetsOL.editableList('items'), events)
|
|
// add our changes to NR history and trigger whether or not we need to redeploy
|
|
recordEvents(events)
|
|
},
|
|
sortItems: function (items) {
|
|
// track any changes
|
|
const events = []
|
|
|
|
// check if we have any new widgets added to this list
|
|
items.each((i, el) => {
|
|
const dbItem = el.data('data')
|
|
const widget = dbItem?.node || {}
|
|
if (widget.group !== groupId) {
|
|
const oldGroupId = widget.group
|
|
widget.group = groupId
|
|
events.push({
|
|
t: 'edit',
|
|
node: widget,
|
|
changes: {
|
|
group: oldGroupId
|
|
},
|
|
dirty: widget.dirty,
|
|
changed: widget.changed
|
|
})
|
|
}
|
|
})
|
|
|
|
updateItemOrder(items, events)
|
|
|
|
// add our changes to NR history and trigger whether or not we need to redeploy
|
|
recordEvents(events)
|
|
},
|
|
sort: function (a, b) {
|
|
return Number(a.order) - Number(b.order)
|
|
}
|
|
})
|
|
|
|
widgets.forEach(function (w) {
|
|
widgetsOL.editableList('addItem', w)
|
|
})
|
|
}
|
|
|
|
// expand / collapse buttons
|
|
let layoutDisplayLevel = 2 // all open by default
|
|
const getGroupsInLayout = function () {
|
|
const content = $('.nrdb2-layout-order-editor > .red-ui-editableList .nrdb2-sb-widget-list-container')
|
|
return {
|
|
content,
|
|
chevrons: content.parent().find('div.nrdb2-sb-list-header > .nrdb2-sb-list-chevron')
|
|
}
|
|
}
|
|
const getPagesInLayout = function () {
|
|
const content = $('.nrdb2-layout-order-editor > .red-ui-editableList .nrdb2-sb-group-list-container')
|
|
return {
|
|
content,
|
|
chevrons: content.parent().find('div.nrdb2-sb-pages-list-header > .nrdb2-sb-list-chevron')
|
|
}
|
|
}
|
|
const collapseLayoutItems = function ({ chevrons, content }) {
|
|
chevrons.css({ transform: 'rotate(-90deg)' })
|
|
content.slideUp()
|
|
content.addClass('nr-db-sb-collapsed')
|
|
}
|
|
const expandLayoutItems = function ({ chevrons, content }) {
|
|
chevrons.css({ transform: '' })
|
|
content.slideDown()
|
|
content.removeClass('nr-db-sb-collapsed')
|
|
}
|
|
/**
|
|
* Update the visibility of the layout editor expandable lists
|
|
* @param {0|1|2} level - 0 = collapse all, 1 = expand pages (groups collapsed), 2 = expand pages and groups (to expose widgets)
|
|
*/
|
|
const updateLayoutVisibility = function (level) {
|
|
if (RED._db2debug) { console.log('dashboard 2: ui_base.html: updateLayoutVisibility(level)', level) }
|
|
if (level === 2) {
|
|
expandLayoutItems(getGroupsInLayout())
|
|
expandLayoutItems(getPagesInLayout())
|
|
} else if (level === 1) {
|
|
expandLayoutItems(getPagesInLayout())
|
|
collapseLayoutItems(getGroupsInLayout())
|
|
} else {
|
|
collapseLayoutItems(getGroupsInLayout())
|
|
collapseLayoutItems(getPagesInLayout())
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sidebar Functions
|
|
*/
|
|
|
|
function buildLayoutOrderEditor (parent) {
|
|
if (RED._db2debug) { console.log('dashboard 2: ui_base.html: buildLayoutOrderEditor (parent)', parent) }
|
|
|
|
// layout/order editor
|
|
const divTabs = $('.nrdb2-layout-order-editor').length ? $('.nrdb2-layout-order-editor') : $('<div>', { class: 'nrdb2-layout-order-editor nrdb2-sidebar-tab-content' }).appendTo(parent)
|
|
|
|
// section header - Pages
|
|
const pagesHeader = $('<div>', { class: 'nrdb2-sidebar-header' }).appendTo(divTabs)
|
|
$('<b>').html(c_('layout.pages')).appendTo(pagesHeader)
|
|
|
|
// toggle "all" buttons
|
|
const buttonGroup = $('<div>', { class: 'nrdb2-sb-list-button-group' }).appendTo(pagesHeader)
|
|
|
|
const buttonCollapse = $('<a href="#" class="editor-button editor-button-small nrdb2-sb-list-header-button"><i class="fa fa-angle-double-up"></i></a>')
|
|
.click(function (evt) {
|
|
evt.preventDefault()
|
|
if (--layoutDisplayLevel < 0) { layoutDisplayLevel = 0 }
|
|
updateLayoutVisibility(layoutDisplayLevel)
|
|
})
|
|
.appendTo(buttonGroup)
|
|
RED.popover.tooltip(buttonCollapse, c_('layout.collapse'))
|
|
|
|
// expand button
|
|
const buttonExpand = $('<a href="#" class="editor-button editor-button-small nrdb2-sb-list-header-button"><i class="fa fa-angle-double-down"></i></a>')
|
|
.click(function (evt) {
|
|
if (++layoutDisplayLevel > 2) { layoutDisplayLevel = 2 }
|
|
updateLayoutVisibility(layoutDisplayLevel)
|
|
}).appendTo(buttonGroup)
|
|
.appendTo(buttonGroup)
|
|
RED.popover.tooltip(buttonExpand, c_('layout.expand'))
|
|
|
|
// add link button
|
|
$('<a href="#" class="editor-button editor-button-small nr-db-sb-list-header-button"><i class="fa fa-plus"></i> ' + c_('layout.link') + '</a>')
|
|
.click(function (evt) {
|
|
pagesOL.editableList('addItem', { type: 'ui-link' })
|
|
evt.preventDefault()
|
|
})
|
|
.appendTo(buttonGroup)
|
|
|
|
// add page button
|
|
$('<a href="#" class="editor-button editor-button-small nr-db-sb-list-header-button"><i class="fa fa-plus"></i> ' + c_('layout.page') + '</a>')
|
|
.click(function (evt) {
|
|
pagesOL.editableList('addItem')
|
|
evt.preventDefault()
|
|
})
|
|
.appendTo(buttonGroup)
|
|
|
|
divTabs.append('<div class="nrdb2-layout-helptext">' + c_('label.layoutMessage') + '</div>')
|
|
|
|
/** @type {DashboardItemLookup} */
|
|
const pages = {}
|
|
const links = {}
|
|
/** @type {DashboardItemLookup} */
|
|
const groupsByPage = {}
|
|
const unattachedGroups = []
|
|
/** @type {DashboardItemLookup} */
|
|
const widgetsByGroup = {}
|
|
const subflowDefinitions = new Map()
|
|
// get all pages & all groups
|
|
RED.nodes.eachConfig(function (n) {
|
|
if (n.type === 'ui-page' && !!n.ui) {
|
|
pages[n.id] = toDashboardItem(n)
|
|
} else if (n.type === 'ui-link') {
|
|
links[n.id] = toDashboardItem(n)
|
|
} else if (n.type === 'ui-group') {
|
|
const p = n.page
|
|
if (!p) {
|
|
unattachedGroups.push(toDashboardItem(n))
|
|
} else {
|
|
if (!groupsByPage[p]) {
|
|
groupsByPage[p] = []
|
|
}
|
|
groupsByPage[p].push(toDashboardItem(n))
|
|
}
|
|
}
|
|
})
|
|
|
|
// get all widgets
|
|
const uiNodesWithGroupProp = []
|
|
RED.nodes.eachNode(function (n) {
|
|
if (/^ui-/.test(n.type) && n.group) {
|
|
uiNodesWithGroupProp.push(n)
|
|
if (!widgetsByGroup[n.group]) {
|
|
widgetsByGroup[n.group] = []
|
|
}
|
|
widgetsByGroup[n.group].push(toDashboardItem(n))
|
|
if (n.z) {
|
|
const subflowDef = RED.nodes.subflow(n.z)
|
|
if (subflowDef) {
|
|
subflowDefinitions.set('subflow:' + subflowDef.id, subflowDef)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
// update `widgetsByGroup` for subflow instances where its env has an entry which
|
|
// is a ui-group
|
|
RED.nodes.eachNode(function (n) {
|
|
if (subflowDefinitions.has(n.type)) {
|
|
const subflowDef = subflowDefinitions.get(n.type)
|
|
const subflowInstance = n
|
|
if (subflowDef && subflowDef.env && subflowDef.env.length > 0) {
|
|
/** @type {{name:String, type:String, value:String}[]} */
|
|
const envDefsWithUIGroup = subflowDef.env.filter(env => env.type === 'ui-group' && env.ui?.type === 'conf-types')
|
|
for (const envDef of envDefsWithUIGroup) {
|
|
const groupNameAsEnv = '${' + envDef.name + '}'
|
|
|
|
if (widgetsByGroup[groupNameAsEnv]) {
|
|
// at this point, we know that the widgets in widgetsByGroup[groupNameAsEnv] belong to the group defined by the env var of the subflow instance
|
|
// get the actual group id from the subflow instance env vars where the name matches the envDef.name
|
|
if (subflowInstance.env) {
|
|
const groupEnvVar = subflowInstance.env.find(env => env.name === envDef.name)
|
|
if (groupEnvVar) {
|
|
const groupId = groupEnvVar.value
|
|
if (!widgetsByGroup[groupId]) {
|
|
widgetsByGroup[groupId] = []
|
|
}
|
|
const sfItem = toDashboardItem(n)
|
|
sfItem.isSubflowInstance = true
|
|
sfItem.subflowName = RED.utils.getNodeLabel(subflowDef)
|
|
if (!sfItem.label?.trim() || sfItem.label === sfItem.id) {
|
|
// label was defaulted to id, so we should use the label from the subflowDef
|
|
sfItem.label = sfItem.subflowName
|
|
}
|
|
widgetsByGroup[groupId].push(sfItem)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
const pagesOL = $('<ol>', { class: 'nrdb2-sb-pages-list' }).appendTo(divTabs).editableList({
|
|
sortable: '.nrdb2-sb-pages-list-header',
|
|
addButton: false,
|
|
addItem: function (container, i, item) {
|
|
if (item && item.type === 'ui-link') {
|
|
// want to create a new link
|
|
if (!item || !item.id) {
|
|
// create a default link
|
|
item = addDefaultLink()
|
|
RED.editor.editConfig('', item.type, item.id)
|
|
}
|
|
// add it to the list of pages/links
|
|
container.addClass('nrdb2-sb-pages-list-item')
|
|
const titleRow = $('<div>', { class: 'nrdb2-sb-list-header nrdb2-sb-pages-list-header' }).appendTo(container)
|
|
setStateClasses(item, titleRow)
|
|
|
|
// build title row
|
|
$('<i class="nrdb2-sb-list-handle nrdb2-sb-page-list-handle fa fa-bars"></i>').appendTo(titleRow)
|
|
const linkIcon = 'fa-link'
|
|
$('<i>', { class: 'nrdb2-sb-icon nrdb2-sb-tab-icon fa ' + linkIcon }).appendTo(titleRow)
|
|
$('<span>', { class: 'nrdb2-sb-title' }).text(item.name || item.id).appendTo(titleRow)
|
|
|
|
// link - actions
|
|
const actions = $('<div>', { class: 'nrdb2-sb-list-header-actions' }).appendTo(titleRow)
|
|
// add "Edit" and "Focus" buttons
|
|
addRowActions(actions, item)
|
|
// Add visibility/disabled options
|
|
addRowStateOptions(actions, item)
|
|
} else {
|
|
// is a page, with groups and widgets inside
|
|
if (!item || !item.id) {
|
|
// this is a new page that's been added and we need to setup the basics
|
|
item = addDefaultPage()
|
|
RED.editor.editConfig('', item.type, item.id)
|
|
}
|
|
const groups = groupsByPage[item.id] || []
|
|
|
|
container.addClass('nrdb2-sb-pages-list-item')
|
|
|
|
const titleRow = $('<div>', { class: 'nrdb2-sb-list-header nrdb2-sb-pages-list-header' }).appendTo(container)
|
|
setStateClasses(item, titleRow)
|
|
|
|
const groupsList = $('<div>', { class: 'nrdb2-sb-group-list-container' }).appendTo(container)
|
|
|
|
// build title row
|
|
$('<i class="nrdb2-sb-list-handle nrdb2-sb-page-list-handle fa fa-bars"></i>').appendTo(titleRow)
|
|
const chevron = $('<i class="fa fa-angle-down nrdb2-sb-list-chevron">', { style: 'width:10px;' }).appendTo(titleRow)
|
|
const tabicon = 'fa-object-group'
|
|
$('<i>', { class: 'nrdb2-sb-icon nrdb2-sb-tab-icon fa ' + tabicon }).appendTo(titleRow)
|
|
$('<span>', { class: 'nrdb2-sb-title' }).text(item.name || item.id).appendTo(titleRow)
|
|
$('<span>', { class: 'nrdb2-sb-info' }).text(`${groups.length} Groups`).appendTo(titleRow)
|
|
|
|
// adds groups within this page
|
|
titleRow.click(titleToggle(item.id, groupsList, chevron))
|
|
const groupsOL = addGroupOrderingList(item.id, groupsList, groups, widgetsByGroup)
|
|
|
|
// page - actions
|
|
const actions = $('<div>', { class: 'nrdb2-sb-list-header-actions' }).appendTo(titleRow)
|
|
addRowActions(actions, item, groupsOL)
|
|
addRowStateOptions(actions, item)
|
|
}
|
|
// ensure all pages/links have the correct order value
|
|
const events = []
|
|
updateItemOrder(pagesOL.editableList('items'), events)
|
|
// add our changes to NR history and trigger whether or not we need to redeploy
|
|
recordEvents(events)
|
|
},
|
|
sortItems: function (items) {
|
|
// runs when an item is explicitly moved in the order
|
|
// track any changes
|
|
const events = []
|
|
updateItemOrder(items, events)
|
|
|
|
// add our changes to NR history and trigger whether or not we need to redeploy
|
|
recordEvents(events)
|
|
},
|
|
sort: function (a, b) {
|
|
return Number(a.order) - Number(b.order)
|
|
}
|
|
})
|
|
|
|
const items = {
|
|
...pages,
|
|
...links
|
|
}
|
|
|
|
Object.values(items).sort((a, b) => a.order - b.order).forEach(function (item) {
|
|
let groups = []
|
|
if (item.type === 'ui-page' && item.id) {
|
|
if (RED._db2debug) { console.log('dashboard 2: ui_base.html: buildLayoutOrderEditor: adding groups', groups) }
|
|
groups = groupsByPage[item.id] || []
|
|
}
|
|
if (item) {
|
|
pagesOL.editableList('addItem', item)
|
|
}
|
|
})
|
|
|
|
// add Unattached Groups to the bottom
|
|
if (unattachedGroups.length > 0) {
|
|
const unattachedGroupsSection = $('<div>', { class: 'nrdb2-sidebar-header', style: 'padding-top: 12px;' }).appendTo(divTabs)
|
|
$('<b>').html(c_('layout.unattachedGroups')).appendTo(unattachedGroupsSection)
|
|
divTabs.append('<div class="nrdb2-layout-helptext">' + c_('label.unattachedMessage') + '</div>')
|
|
|
|
// we have some groups bound to a page that no longer exists
|
|
const unattachedGroupsOL = $('<ol>', { class: 'nrdb2-sb-unattached-groups-list' }).appendTo(divTabs).editableList({
|
|
sortable: '.nrdb2-sb-unattached-groups-list-header',
|
|
addButton: false,
|
|
addItem: function (container, i, group) {
|
|
if (!group || !group.id) {
|
|
// this is a new group that's been added and we need to setup the basics
|
|
group = addDefaultGroup()
|
|
RED.editor.editConfig('', group.type, group.id)
|
|
}
|
|
container.addClass('nrdb2-sb-unattached-groups-list-item')
|
|
|
|
const titleRow = $('<div>', { class: 'nrdb2-sb-list-header nrdb2-sb-unattached-groups-list-header' }).appendTo(container)
|
|
const tabicon = 'fa-table'
|
|
$('<i>', { class: 'nrdb2-sb-icon nrdb2-sb-tab-icon fa ' + tabicon }).appendTo(titleRow)
|
|
$('<span>', { class: 'nrdb2-sb-title' }).text(group.name || group.id).appendTo(titleRow)
|
|
$('<span>', { class: 'nrdb2-sb-info' }).text('No Page Assigned').appendTo(titleRow)
|
|
|
|
// group - actions
|
|
const actions = $('<div>', { class: 'nrdb2-sb-list-header-actions' }).appendTo(titleRow)
|
|
addRowActions(actions, group)
|
|
}
|
|
})
|
|
|
|
unattachedGroups.forEach(function (group) {
|
|
unattachedGroupsOL.editableList('addItem', group)
|
|
})
|
|
}
|
|
|
|
// call updateLayoutVisibility to sync display level
|
|
updateLayoutVisibility(layoutDisplayLevel)
|
|
}
|
|
|
|
function buildThemesEditor (parent) {
|
|
// layout/order editor
|
|
const divTabs = $('.nrdb2-themes-editor').length ? $('.nrdb2-themes-editor') : $('<div>', { class: 'nrdb2-themes-editor nrdb2-sidebar-tab-content' }).appendTo(parent)
|
|
|
|
// section header - Pages
|
|
const themeHeader = $('<div>', { class: 'nrdb2-sidebar-header' }).appendTo(divTabs)
|
|
$('<b>').html(c_('themes.header')).appendTo(themeHeader)
|
|
|
|
// button group
|
|
const buttonGroup = $('<div>', { class: 'nrdb2-sb-list-button-group' }).appendTo(themeHeader)
|
|
|
|
// add theme button
|
|
$('<a href="#" class="editor-button editor-button-small nr-db-sb-list-header-button"><i class="fa fa-plus"></i> ' + c_('themes.theme') + '</a>')
|
|
.click(function (evt) {
|
|
themesOL.editableList('addItem')
|
|
evt.preventDefault()
|
|
})
|
|
.appendTo(buttonGroup)
|
|
|
|
divTabs.append('<div class="nrdb2-layout-helptext">' + c_('label.themingMessage') + '</div>')
|
|
|
|
const themes = {}
|
|
|
|
// get all themes
|
|
RED.nodes.eachConfig(function (n) {
|
|
if (n.type === 'ui-theme') {
|
|
themes[n.id] = n
|
|
}
|
|
})
|
|
|
|
const themesOL = $('<ol>', { class: 'nrdb2-sb-pages-list' }).appendTo(divTabs).editableList({
|
|
sortable: '.nrdb2-sb-pages-list-header',
|
|
addButton: false,
|
|
addItem: function (container, i, theme) {
|
|
if (!theme || !theme.id) {
|
|
// this is a new theme that's been added and we need to setup the basics
|
|
theme = addDefaultTheme()
|
|
RED.editor.editConfig('', theme.type, theme.id)
|
|
}
|
|
container.addClass('nrdb2-sb-pages-list-item')
|
|
|
|
const titleRow = $('<div>', { class: 'nrdb2-sb-list-header nrdb2-sb-themes-list-header' }).appendTo(container)
|
|
const tabicon = 'fa-paint-brush'
|
|
$('<i>', { class: 'nrdb2-sb-icon nrdb2-sb-tab-icon fa ' + tabicon }).appendTo(titleRow)
|
|
$('<span>', { class: 'nrdb2-sb-title' }).text(theme.name || theme.id).appendTo(titleRow)
|
|
$('<span>', { class: 'nrdb2-sb-info' }).text(theme.users.length + ' ' + (theme.users.length > 1 ? c_('label.pages') : c_('label.page'))).appendTo(titleRow)
|
|
|
|
const palette = $('<div>', { class: 'nrdb2-sb-palette' }).appendTo(titleRow)
|
|
const colors = theme.colors
|
|
|
|
palette.append($('<div>', { class: 'nrdb2-sb-palette-color', style: `background-color: ${colors.surface}` }))
|
|
palette.append($('<div>', { class: 'nrdb2-sb-palette-color', style: `background-color: ${colors.primary}` }))
|
|
palette.append($('<div>', { class: 'nrdb2-sb-palette-color', style: `background-color: ${colors.bgPage}` }))
|
|
palette.append($('<div>', { class: 'nrdb2-sb-palette-color', style: `background-color: ${colors.groupBg}` }))
|
|
palette.append($('<div>', { class: 'nrdb2-sb-palette-color', style: `background-color: ${colors.groupOutline}` }))
|
|
|
|
// theme - actions
|
|
const actions = $('<div>', { class: 'nrdb2-sb-list-header-actions' }).appendTo(titleRow)
|
|
addRowActions(actions, theme)
|
|
}
|
|
})
|
|
|
|
Object.values(themes).forEach(function (theme) {
|
|
themesOL.editableList('addItem', theme)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Builds the client constraints editor
|
|
* @param {Object} base - The ui-base node we are representing
|
|
* @param {Object} parent - The parent element to append the client constraints editor to
|
|
*/
|
|
function buildClientConstraintsEditor (base, parent) {
|
|
const html = `<div class="nrdb2-sidebar-tab-content">
|
|
<div style="margin-bottom: 9px;border-bottom: 1px solid #ddd;">
|
|
<p>
|
|
This tab allows you to control whether or not client-specific data is included in messages,
|
|
and which nodes accept it in order to constrain communication to specific clients.
|
|
You can read more about it <a href="https://dashboard.flowfuse.com/user/sidebar.html#client-data" target="_blank">here</a>
|
|
</p>
|
|
</div>
|
|
<div class="form-row form-row-flex">
|
|
<input type="checkbox" id="node-config-input-includeClientData">
|
|
<label style="width:auto" for="node-config-input-includeClientData"><i class="fa fa-id-card-o" style="margin-right:4px;"></i>Include Client Data</label>
|
|
</div>
|
|
<p class="nrdb2-helptext" style="margin-top: -12px;">
|
|
<i class="fa fa-info-circle"></i> This option includes client data in all messages transmitted
|
|
by Dashboard 2.0 (e.g. Socket ID and information about any logged in user)
|
|
</p>
|
|
<div id="nrdb2-sb-client-data-providers">
|
|
<label>Client Data Providers:</label>
|
|
<ul id="nrdb2-sb-client-data-providers-list"></ul>
|
|
</div>
|
|
<div id="node-config-client-constraints-nodes-container" class="form-row">
|
|
<label style="display: block; width: auto;"><i class="fa fa-id-card-o" style="margin-right:4px;"></i>Accept Client Data</label>
|
|
<p class="nrdb2-helptext" style="margin-top: -4px;">
|
|
<i class="fa fa-info-circle"></i> Defines whether the respective node type will use client data
|
|
(e.g. socketid), and constrain communications to just that client.
|
|
</p>
|
|
<div id="node-config-client-constraints-nodes"></div>
|
|
</div></div>`
|
|
|
|
$(html).appendTo(parent)
|
|
|
|
if (base.includeClientData === undefined) {
|
|
// older ui-base nodes may not have this property
|
|
base.includeClientData = true
|
|
const events = [{
|
|
t: 'edit',
|
|
node: base,
|
|
changes: {
|
|
includeClientData: undefined
|
|
},
|
|
dirty: true,
|
|
changed: true
|
|
}]
|
|
recordEvents(events)
|
|
}
|
|
|
|
// set initial state
|
|
$('#node-config-input-includeClientData')
|
|
.prop('checked', base.includeClientData)
|
|
|
|
// add event handler to our checkbox for including client data
|
|
$('#node-config-input-includeClientData')
|
|
.on('change', function (event) {
|
|
const value = event.target.checked
|
|
base.includeClientData = value
|
|
// store hte previous value in history
|
|
const events = [{
|
|
t: 'edit',
|
|
node: base,
|
|
changes: {
|
|
includeClientData: !value
|
|
},
|
|
dirty: true,
|
|
changed: true
|
|
}]
|
|
recordEvents(events)
|
|
})
|
|
|
|
// add core as a data provider as it provides socketId
|
|
const coreLi = $('<li>').appendTo('#nrdb2-sb-client-data-providers-list')
|
|
$('<i class="fa fa-bar-chart"></i>').appendTo(coreLi)
|
|
$('<label></label>').text('Socket ID').appendTo(coreLi)
|
|
$('<span class="nrdb2-sb-client-data-provider-package"></span>').text('@flowfuse/node-red-dashboard').appendTo(coreLi)
|
|
|
|
// detail any auth providers we have
|
|
// check for any third-party tab definitions
|
|
RED.plugins.getPluginsByType('node-red-dashboard-2').forEach(plugin => {
|
|
if (plugin.auth) {
|
|
const li = $('<li>').appendTo('#nrdb2-sb-client-data-providers-list')
|
|
$('<i class="fa fa-plug"></i>').appendTo(li)
|
|
$('<label></label>').text(plugin.description).appendTo(li)
|
|
$('<span class="nrdb2-sb-client-data-provider-package"></span>').text(plugin.module).appendTo(li)
|
|
$('<li>').text(plugin.description).appendTo('#node-config-client-data-providers-list')
|
|
}
|
|
})
|
|
|
|
// build list of node types
|
|
const widgets = RED.nodes.registry.getNodeTypes().filter((nodeType) => {
|
|
const type = RED.nodes.getType(nodeType)
|
|
return /^ui-/.test(nodeType) && type.category !== 'config' && type.inputs > 0
|
|
})
|
|
|
|
if (base.acceptsClientConfig === undefined) {
|
|
// older ui-base nodes may not have this property
|
|
base.acceptsClientConfig = ['ui-control', 'ui-notification']
|
|
const events = [{
|
|
t: 'edit',
|
|
node: base,
|
|
changes: {
|
|
acceptsClientConfig: undefined
|
|
},
|
|
dirty: true,
|
|
changed: true
|
|
}]
|
|
recordEvents(events)
|
|
}
|
|
|
|
$('#node-config-client-constraints-nodes')
|
|
.treeList({
|
|
data: widgets.map((w) => {
|
|
const isSelected = base.acceptsClientConfig?.includes(w)
|
|
return {
|
|
label: w,
|
|
checkbox: true,
|
|
selected: isSelected
|
|
}
|
|
})
|
|
})
|
|
.on('treelistselect', function (event, node) {
|
|
const events = [{
|
|
t: 'edit',
|
|
node: base,
|
|
changes: {
|
|
acceptsClientConfig: [...base.acceptsClientConfig]
|
|
},
|
|
dirty: true,
|
|
changed: true
|
|
}]
|
|
let changed = false
|
|
if (!node.selected && base.acceptsClientConfig?.includes(node.label)) {
|
|
// remove it from the list
|
|
base.acceptsClientConfig = base.acceptsClientConfig.filter((w) => w !== node.label)
|
|
changed = true
|
|
} else if (node.selected && !base.acceptsClientConfig?.includes(node.label)) {
|
|
// add it to the list
|
|
base.acceptsClientConfig.push(node.label)
|
|
changed = true
|
|
}
|
|
if (changed) {
|
|
recordEvents(events)
|
|
}
|
|
})
|
|
}
|
|
|
|
function refreshSidebarEditors () {
|
|
try {
|
|
refreshBusy = true
|
|
if (RED._db2debug) { console.log('dashboard 2: ui_base.html: refreshSidebarEditors ()') }
|
|
const layoutOrderDiv = $('.nrdb2-layout-order-editor')
|
|
const themesDiv = $('.nrdb2-themes-editor')
|
|
// empty the list if any items exist
|
|
if (layoutOrderDiv.length) {
|
|
layoutOrderDiv.empty()
|
|
}
|
|
if (themesDiv.length) {
|
|
themesDiv.empty()
|
|
}
|
|
// now rebuild any sidebars that are dependent upon other nodes
|
|
buildLayoutOrderEditor()
|
|
buildThemesEditor()
|
|
|
|
// finally, restore previous state of expanded/collapsed items
|
|
// TODO: expand/collapse any items that were expanded before
|
|
// for now, we will just re-sync the display level
|
|
updateLayoutVisibility(layoutDisplayLevel)
|
|
} finally {
|
|
refreshBusy = false
|
|
}
|
|
}
|
|
|
|
function addSidebarTab (label, id, tabs, sidebar) {
|
|
$(`<li id="dashboard-2-tab-${id}" class="red-ui-tab" style="min-width: 90px; width: fit-content"><a class="red-ui-tab-label" style="padding-inline: 12px" title="Layout"><span>${label}</span></a></li>`).appendTo(tabs)
|
|
|
|
// Add in Tab Content
|
|
const content = $(`<div id="dashboard-2-${id}" class="red-ui-tab-content" style="height: calc(100% - 72px);"></div>`).appendTo(sidebar)
|
|
return content
|
|
}
|
|
|
|
function addSidebar () {
|
|
if (RED._db2debug) { console.log('dashboard 2: ui_base.html: addSidebar ()') }
|
|
RED.sidebar.addTab({
|
|
id: 'dashboard-2.0',
|
|
label: c_('label.dashboard2'),
|
|
name: c_('label.dashboard2'),
|
|
content: sidebar,
|
|
closeable: true,
|
|
pinned: true,
|
|
disableOnEdit: true,
|
|
iconClass: 'fa fa-bar-chart',
|
|
action: '@flowfuse/node-red-dashboard:show-dashboard-2.0-tab',
|
|
onchange: () => {
|
|
sidebar.empty()
|
|
|
|
// UI Base Header
|
|
RED.nodes.eachConfig(function (n) {
|
|
if (n.type === 'ui-base') {
|
|
const base = n
|
|
sidebar.append(dashboardLink(base.id, base.name, base.path))
|
|
const divTab = $('<div class="red-ui-tabs">').appendTo(sidebar)
|
|
|
|
const ulDashboardTabs = $('<ul id="dashboard-tabs-list"></ul>').appendTo(divTab)
|
|
|
|
// Add in Tabs
|
|
// Tab - Pages
|
|
const pagesContent = addSidebarTab(c_('label.layout'), 'pages', ulDashboardTabs, sidebar)
|
|
const themesContent = addSidebarTab(c_('label.theming'), 'themes', ulDashboardTabs, sidebar)
|
|
const cConstraintsContent = addSidebarTab(c_('label.constraints'), 'client-constraints', ulDashboardTabs, sidebar)
|
|
|
|
// check for any third-party tab definitions
|
|
RED.plugins.getPluginsByType('node-red-dashboard-2').forEach(plugin => {
|
|
if (plugin.tabs) {
|
|
plugin.tabs.forEach(tab => {
|
|
// add tab to sidebar
|
|
const container = addSidebarTab(tab.label, tab.id, ulDashboardTabs, sidebar)
|
|
container.hide()
|
|
tab.init(base, container)
|
|
})
|
|
}
|
|
})
|
|
|
|
// on tab click, show the tab content, and hide the others
|
|
ulDashboardTabs.children().on('click', function (evt) {
|
|
const tab = $(this)
|
|
const tabContent = $('#' + tab.attr('id').replace('-tab', ''))
|
|
ulDashboardTabs.children().removeClass('active')
|
|
tab.addClass('active')
|
|
$('.red-ui-tab-content').hide()
|
|
tabContent.show()
|
|
evt.preventDefault()
|
|
})
|
|
|
|
// default to first tab
|
|
ulDashboardTabs.children().first().trigger('click')
|
|
|
|
// add page/layout editor
|
|
buildLayoutOrderEditor(pagesContent)
|
|
// add Themes View
|
|
buildThemesEditor(themesContent)
|
|
// add Themes View
|
|
buildClientConstraintsEditor(base, cConstraintsContent)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
RED.actions.add('@flowfuse/node-red-dashboard:show-dashboard-2.0-tab', function () {
|
|
RED.sidebar.show('flowfuse-nr-tools')
|
|
})
|
|
}
|
|
|
|
/**
|
|
* jQuery widget to provide a selector for the sizing (width & height) of a widget & group
|
|
*/
|
|
$.widget('nodereddashboard.elementSizer', {
|
|
_create: function () {
|
|
// convert to i18 text
|
|
function c_ (x) {
|
|
return RED._(`@flowfuse/node-red-dashboard/ui-base:ui-base.${x}`)
|
|
}
|
|
|
|
const thisWidget = this
|
|
let gridWidth = 6
|
|
const width = parseInt($(this.options.width).val() || 0)
|
|
const height = parseInt(hasProperty(this.options, 'height') ? $(this.options.height).val() : '1') || 0
|
|
const hasAuto = (!hasProperty(this.options, 'auto') || this.options.auto)
|
|
|
|
this.element.css({
|
|
minWidth: this.element.height() + 4
|
|
})
|
|
const autoText = c_('auto')
|
|
const sizeLabel = (width === 0 && height === 0) ? autoText : width + (hasProperty(this.options, 'height') ? ' x ' + height : '')
|
|
this.element.text(sizeLabel).on('mousedown', function (evt) {
|
|
evt.stopPropagation()
|
|
evt.preventDefault()
|
|
|
|
const width = parseInt($(thisWidget.options.width).val() || 0)
|
|
const height = parseInt(hasProperty(thisWidget.options, 'height') ? $(thisWidget.options.height).val() : '1') || 0
|
|
let maxWidth = 0
|
|
let maxHeight
|
|
let fixedWidth = false
|
|
const fixedHeight = false
|
|
const group = $(thisWidget.options.group).val()
|
|
if (group) {
|
|
const groupNode = RED.nodes.node(group)
|
|
if (groupNode) {
|
|
gridWidth = Math.max(6, groupNode.width, +width)
|
|
maxWidth = groupNode.width || gridWidth
|
|
fixedWidth = true
|
|
}
|
|
maxHeight = Math.max(6, +height + 1)
|
|
} else {
|
|
gridWidth = Math.max(12, +width)
|
|
maxWidth = gridWidth
|
|
maxHeight = height + 1
|
|
// fixedHeight = false;
|
|
}
|
|
|
|
const pos = $(this).offset()
|
|
const container = $('<div>').css({
|
|
position: 'absolute',
|
|
background: 'var(--red-ui-secondary-background, white)',
|
|
padding: '5px 10px 10px 10px',
|
|
border: '1px solid var(--red-ui-primary-border-color, grey)',
|
|
zIndex: '20',
|
|
borderRadius: '4px',
|
|
display: 'none'
|
|
}).appendTo(document.body)
|
|
|
|
let closeTimer
|
|
container.on('mouseleave', function (evt) {
|
|
closeTimer = setTimeout(function () {
|
|
container.fadeOut(200, function () { $(this).remove() })
|
|
}, 100)
|
|
})
|
|
container.on('mouseenter', function () {
|
|
clearTimeout(closeTimer)
|
|
})
|
|
|
|
const label = $('<div>').css({
|
|
fontSize: '13px',
|
|
color: 'var(--red-ui-tertiary-text-color, #aaa)',
|
|
float: 'left',
|
|
paddingTop: '1px'
|
|
}).appendTo(container).text((width === 0 && height === 0) ? autoText : (width + (hasProperty(thisWidget.options, 'height') ? ' x ' + height : '')))
|
|
label.hover(function () {
|
|
$(this).css('text-decoration', 'underline')
|
|
}, function () {
|
|
$(this).css('text-decoration', 'none')
|
|
})
|
|
|
|
label.click(function (e) {
|
|
const group = $(thisWidget.options.group).val()
|
|
let groupNode = null
|
|
if (group) {
|
|
groupNode = RED.nodes.node(group)
|
|
if (groupNode === null) {
|
|
return
|
|
}
|
|
}
|
|
$(thisWidget).elementSizerByNum({
|
|
width: thisWidget.options.width,
|
|
height: thisWidget.options.height,
|
|
groupNode,
|
|
pos,
|
|
label: thisWidget.element,
|
|
has_height: hasProperty(thisWidget.options, 'height')
|
|
})
|
|
closeTimer = setTimeout(function () {
|
|
container.fadeOut(200, function () {
|
|
$(this).remove()
|
|
})
|
|
}, 100)
|
|
})
|
|
|
|
const buttonRow = $('<div>', { style: 'text-align:right; height:25px;' }).appendTo(container)
|
|
|
|
if (hasAuto) {
|
|
$('<a>', { href: '#', class: 'editor-button editor-button-small', style: 'margin-bottom:5px' })
|
|
.text(autoText)
|
|
.appendTo(buttonRow)
|
|
.on('mouseup', function (evt) {
|
|
thisWidget.element.text(autoText)
|
|
$(thisWidget.options.width).val(0).change()
|
|
$(thisWidget.options.height).val(0).change()
|
|
evt.preventDefault()
|
|
container.fadeOut(200, function () { $(this).remove() })
|
|
})
|
|
}
|
|
|
|
const cellBorder = '1px dashed var(--red-ui-secondary-border-color, lightGray)'
|
|
const cellBorderExisting = '1px solid gray'
|
|
const cellBorderHighlight = '1px dashed var(--red-ui-primary-border-color, black)'
|
|
const rows = []
|
|
const cells = []
|
|
|
|
function addRow (i) {
|
|
const row = $('<div>').css({ padding: 0, margin: 0, height: '25px', 'box-sizing': 'border-box' }).appendTo(container)
|
|
rows.push(row)
|
|
cells.push([])
|
|
for (let j = 0; j < gridWidth; j++) {
|
|
addCell(i, j)
|
|
}
|
|
}
|
|
|
|
function addCell (i, j) {
|
|
const row = rows[i]
|
|
const cell = $('<div>').css({
|
|
display: 'inline-block',
|
|
width: '25px',
|
|
height: '25px',
|
|
borderRight: (j === (width - 1) && i < height) ? cellBorderExisting : cellBorder,
|
|
borderBottom: (i === (height - 1) && j < width) ? cellBorderExisting : cellBorder,
|
|
boxSizing: 'border-box',
|
|
cursor: 'pointer',
|
|
background: (j < maxWidth) ? 'var(--red-ui-secondary-background, #fff)' : 'var(--red-ui-node-background-placeholder, #eee)'
|
|
}).appendTo(row)
|
|
cells[i].push(cell)
|
|
if (j === 0) {
|
|
cell.css({ borderLeft: ((i <= height - 1) ? cellBorderExisting : cellBorder) })
|
|
}
|
|
if (i === 0) {
|
|
cell.css({ borderTop: ((j <= width - 1) ? cellBorderExisting : cellBorder) })
|
|
}
|
|
if (j < maxWidth) {
|
|
cell.data('w', j)
|
|
cell.data('h', i)
|
|
cell.on('mouseup', function () {
|
|
thisWidget.element.text(($(this).data('w') + 1) + (hasProperty(thisWidget.options, 'height') ? ' x ' + ($(this).data('h') + 1) : ''))
|
|
$(thisWidget.options.width).val($(this).data('w') + 1).change()
|
|
$(thisWidget.options.height).val($(this).data('h') + 1).change()
|
|
container.fadeOut(200, function () { $(this).remove() })
|
|
})
|
|
cell.on('mouseover', function () {
|
|
const w = $(this).data('w')
|
|
const h = $(this).data('h')
|
|
label.text((w + 1) + (hasProperty(thisWidget.options, 'height') ? ' x ' + (h + 1) : ''))
|
|
for (let y = 0; y < maxHeight; y++) {
|
|
for (let x = 0; x < maxWidth; x++) {
|
|
cells[y][x].css({
|
|
background: (y <= h && x <= w) ? 'var(--red-ui-secondary-background-selected, #ddd)' : 'var(--red-ui-secondary-background, #fff)',
|
|
borderLeft: (x === 0 && y <= h) ? cellBorderHighlight : (x === 0) ? ((y <= height - 1) ? cellBorderExisting : cellBorder) : '',
|
|
borderTop: (y === 0 && x <= w) ? cellBorderHighlight : (y === 0) ? ((x <= width - 1) ? cellBorderExisting : cellBorder) : '',
|
|
borderRight: (x === w && y <= h) ? cellBorderHighlight : ((x === width - 1 && y <= height - 1) ? cellBorderExisting : cellBorder),
|
|
borderBottom: (y === h && x <= w) ? cellBorderHighlight : ((y === height - 1 && x <= width - 1) ? cellBorderExisting : cellBorder)
|
|
})
|
|
}
|
|
}
|
|
if (!fixedHeight && h === maxHeight - 1) {
|
|
addRow(maxHeight++)
|
|
}
|
|
if (!fixedWidth && w === maxWidth - 1) {
|
|
maxWidth++
|
|
gridWidth++
|
|
for (let r = 0; r < maxHeight; r++) {
|
|
addCell(r, maxWidth - 1)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
for (let i = 0; i < maxHeight; i++) {
|
|
addRow(i)
|
|
}
|
|
container.css({
|
|
top: (pos.top) + 'px',
|
|
left: (pos.left) + 'px'
|
|
})
|
|
container.fadeIn(200)
|
|
})
|
|
}
|
|
})
|
|
})()
|
|
</script>
|
|
|
|
<script type="text/html" data-template-name="ui-base">
|
|
<div class="form-row">
|
|
<label for="node-config-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></label>
|
|
<input type="text" id="node-config-input-name" data-i18n="[placeholder]node-red:common.label.name">
|
|
</div>
|
|
<div class="form-row">
|
|
<label for="node-config-input-path"><i class="fa fa-bookmark"></i> <span data-i18n="ui-base.label.path"></label>
|
|
<input type="text" id="node-config-input-path" disabled>
|
|
<span style="display: block; margin-left: 105px; margin-top: 0px; font-style: italic; color: #bbb; font-size: 8pt;">This option is currently disabled and still in-development.</span>
|
|
</div>
|
|
<div class="form-row" style="margin-bottom: 0;">
|
|
<label style="font-weight: 600; width: auto;" data-i18n="ui-base.label.header"></label>
|
|
</div>
|
|
<div class="form-row" style="align-items: center;">
|
|
<label style="margin-right: 5px; margin-bottom: 0px;" for="node-config-input-titleBarStyle"><span data-i18n="ui-base.label.titleBarStyle"></span></label>
|
|
<select id="node-config-input-titleBarStyle">
|
|
<option value="default" data-i18n="ui-base.label.titleBarStyleDefault"></option>
|
|
<option value="hidden" data-i18n="ui-base.label.titleBarStyleHidden"></option>
|
|
<option value="fixed" data-i18n="ui-base.label.titleBarStyleFixed"></option>
|
|
</select>
|
|
</div>
|
|
<div class="form-row form-row-flex" style="align-items: center;">
|
|
<input style="margin: 8px 0 10px 16px; width:20px;" type="checkbox" id="node-config-input-showPageTitle">
|
|
<label style="width:auto" for="node-config-input-showPageTitle"><span data-i18n="ui-base.label.showPageTitle"></span></label>
|
|
</div>
|
|
<div class="form-row" style="margin-bottom: 0;">
|
|
<label style="font-weight: 600; width: auto;" data-i18n="ui-base.label.sidebar"></label>
|
|
</div>
|
|
<div class="form-row" style="align-items: center;">
|
|
<label style="margin-right: 5px; margin-bottom: 0px;" for="node-config-input-navigationStyle"><span data-i18n="ui-base.label.navigationStyle"></span></label>
|
|
<select id="node-config-input-navigationStyle">
|
|
<option value="default" data-i18n="ui-base.label.navigationStyleDefault"></option>
|
|
<option value="fixed" data-i18n="ui-base.label.navigationStyleFixed"></option>
|
|
<option value="icon" data-i18n="ui-base.label.navigationStyleIcon"></option>
|
|
<option value="temporary" data-i18n="ui-base.label.navigationStyleTemporary"></option>
|
|
<option value="none" data-i18n="ui-base.label.navigationStyleNone"></option>
|
|
</select>
|
|
</div>
|
|
<div class="form-row form-row-flex" style="align-items: center;">
|
|
<input style="margin: 8px 0 10px 16px; width:20px;" type="checkbox" id="node-config-input-showPathInSidebar">
|
|
<label style="width:auto" for="node-config-input-showPathInSidebar"><span data-i18n="ui-base.label.showPath"></span></label>
|
|
</div>
|
|
</script>
|