@flowfuse/node-red-dashboard 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +53 -0
  3. package/dist/css/app.d047b42b.css +1 -0
  4. package/dist/css/chunk-vendors.2378ce49.css +24 -0
  5. package/dist/fonts/materialdesignicons-webfont.3de8526e.woff +0 -0
  6. package/dist/fonts/materialdesignicons-webfont.477c6ab0.woff2 +0 -0
  7. package/dist/fonts/materialdesignicons-webfont.48a1ce0c.eot +0 -0
  8. package/dist/fonts/materialdesignicons-webfont.dfd403cf.ttf +0 -0
  9. package/dist/index.html +1 -0
  10. package/dist/js/app.854a8cd5.js +2 -0
  11. package/dist/js/app.854a8cd5.js.map +1 -0
  12. package/dist/js/chunk-vendors.174e8921.js +43 -0
  13. package/dist/js/chunk-vendors.174e8921.js.map +1 -0
  14. package/nodes/config/locales/en-US/ui_base.json +19 -0
  15. package/nodes/config/locales/en-US/ui_group.html +4 -0
  16. package/nodes/config/locales/en-US/ui_group.json +16 -0
  17. package/nodes/config/ui_base.html +807 -0
  18. package/nodes/config/ui_base.js +678 -0
  19. package/nodes/config/ui_group.html +55 -0
  20. package/nodes/config/ui_group.js +34 -0
  21. package/nodes/config/ui_page.html +84 -0
  22. package/nodes/config/ui_page.js +33 -0
  23. package/nodes/config/ui_theme.html +101 -0
  24. package/nodes/config/ui_theme.js +15 -0
  25. package/nodes/store/index.js +34 -0
  26. package/nodes/utils/index.js +35 -0
  27. package/nodes/widgets/locales/en-US/ui_button.html +7 -0
  28. package/nodes/widgets/locales/en-US/ui_button.json +24 -0
  29. package/nodes/widgets/locales/en-US/ui_chart.html +41 -0
  30. package/nodes/widgets/locales/en-US/ui_chart.json +17 -0
  31. package/nodes/widgets/locales/en-US/ui_dropdown.html +24 -0
  32. package/nodes/widgets/locales/en-US/ui_form.html +16 -0
  33. package/nodes/widgets/locales/en-US/ui_form.json +36 -0
  34. package/nodes/widgets/locales/en-US/ui_markdown.html +10 -0
  35. package/nodes/widgets/locales/en-US/ui_slider.html +9 -0
  36. package/nodes/widgets/locales/en-US/ui_switch.html +32 -0
  37. package/nodes/widgets/locales/en-US/ui_template.html +59 -0
  38. package/nodes/widgets/locales/en-US/ui_template.json +18 -0
  39. package/nodes/widgets/locales/en-US/ui_text.html +16 -0
  40. package/nodes/widgets/locales/en-US/ui_text_input.html +19 -0
  41. package/nodes/widgets/ui_button.html +146 -0
  42. package/nodes/widgets/ui_button.js +65 -0
  43. package/nodes/widgets/ui_chart.html +314 -0
  44. package/nodes/widgets/ui_chart.js +195 -0
  45. package/nodes/widgets/ui_dropdown.html +199 -0
  46. package/nodes/widgets/ui_dropdown.js +19 -0
  47. package/nodes/widgets/ui_form.html +368 -0
  48. package/nodes/widgets/ui_form.js +18 -0
  49. package/nodes/widgets/ui_markdown.html +134 -0
  50. package/nodes/widgets/ui_markdown.js +14 -0
  51. package/nodes/widgets/ui_notification.html +139 -0
  52. package/nodes/widgets/ui_notification.js +14 -0
  53. package/nodes/widgets/ui_radio_group.html +186 -0
  54. package/nodes/widgets/ui_radio_group.js +20 -0
  55. package/nodes/widgets/ui_slider.html +162 -0
  56. package/nodes/widgets/ui_slider.js +31 -0
  57. package/nodes/widgets/ui_switch.html +194 -0
  58. package/nodes/widgets/ui_switch.js +98 -0
  59. package/nodes/widgets/ui_table.html +149 -0
  60. package/nodes/widgets/ui_table.js +16 -0
  61. package/nodes/widgets/ui_template.html +283 -0
  62. package/nodes/widgets/ui_template.js +19 -0
  63. package/nodes/widgets/ui_text.html +358 -0
  64. package/nodes/widgets/ui_text.js +98 -0
  65. package/nodes/widgets/ui_text_input.html +141 -0
  66. package/nodes/widgets/ui_text_input.js +37 -0
  67. package/package.json +114 -0
@@ -0,0 +1,807 @@
1
+ <style>
2
+ .red-ui-editor {
3
+ --nrdb-node-light: rgb(160, 230, 236);
4
+ --nrdb-node-medium: rgb(90, 210, 220);
5
+ --nrdb-node-dark: rgb(39, 183, 195);
6
+ }
7
+ .red-ui-editor .form-row-flex {
8
+ display: flex;
9
+ align-items: center;
10
+ gap: 4px;
11
+ }
12
+ .red-ui-editor .form-row-flex input,
13
+ .red-ui-editor .form-row-flex label {
14
+ margin: 0;
15
+ width: auto;
16
+ }
17
+ #ff-node-red-dashboard {
18
+ --ff-grey-50: #F9FAFB;
19
+ --ff-grey-100: #F3F4F6;
20
+ --ff-grey-200: #E5E7EB;
21
+ position: absolute;
22
+ top: 1px;
23
+ bottom: 2px;
24
+ left: 1px;
25
+ right: 1px;
26
+ overflow-y: auto;
27
+ }
28
+ #ff-node-red-dashboard .red-ui-sidebar-header {
29
+ display: flex;
30
+ justify-content: space-between;
31
+ }
32
+ #ff-node-red-dashboard .red-ui-sidebar-header label {
33
+ margin-bottom: 0;
34
+ }
35
+ #ff-node-red-dashboard .red-ui-editableList-container {
36
+ padding: 0;
37
+ }
38
+ /* don't show border for nexted editable lists */
39
+ .red-ui-editableList-border .red-ui-editableList-border {
40
+ border: 0;
41
+ }
42
+ /* Dashboard 2.0 Sidebar */
43
+ .nrdb2-layout-order-editor {
44
+ padding: 8px 10px;
45
+ }
46
+ .nrdb2-layout-helptext {
47
+ padding: 0 0 9px;
48
+ font-style: italic;
49
+ color: #a2a2a2;
50
+ font-size: 8pt;
51
+ line-height: 12pt;
52
+ }
53
+ .nrdb2-layout-order-editor--pages {
54
+ display: flex;
55
+ justify-content: space-between;
56
+ align-items: center;
57
+ margin-bottom: 9px;
58
+ }
59
+ .nrdb2-sb-pages-list li {
60
+ padding: 0;
61
+ border-bottom: 0;
62
+ }
63
+ .nrdb2-sb-list-header {
64
+ display: flex;
65
+ gap: 6px;
66
+ align-items: center;
67
+ padding: 9px 6px;
68
+ cursor: pointer;
69
+ }
70
+ .nrdb2-sb-list-header.nrdb2-sb-pages-list-header {
71
+ border-top: 1px solid var(--ff-grey-200);
72
+ border-bottom: 1px solid var(--ff-grey-200);
73
+ }
74
+ .nrdb2-sb-list-header .nrdb2-sb-title {
75
+ text-overflow: ellipsis;
76
+ overflow: hidden;
77
+ white-space: nowrap;
78
+ }
79
+ .nrdb2-sb-list-header-button-group {
80
+ position: absolute;
81
+ right: 1rem;
82
+ }
83
+ .nrdb2-sb-list-header-button-group,
84
+ .nrdb2-sb-list-handle {
85
+ opacity: 0;
86
+ transition: 0.15s opacity;
87
+ }
88
+ .nrdb2-sb-list-header:hover {
89
+ background-color: var(--ff-grey-100);
90
+ }
91
+ .nrdb2-sb-list-header:hover .nrdb2-sb-list-handle,
92
+ .nrdb2-sb-list-header:hover .nrdb2-sb-list-header-button-group {
93
+ opacity: 1;
94
+ }
95
+ /* indent the groups */
96
+ .nrdb2-sb-groups-list-header .nrdb2-sb-list-chevron {
97
+ margin-left: 1.5rem;
98
+ }
99
+ /* indent the widgets */
100
+ .nrdb2-sb-widgets-list-header .nrdb2-sb-widget-icon {
101
+ margin-left: 3.5rem;
102
+ }
103
+ </style>
104
+
105
+
106
+ <script type="text/javascript">
107
+ (function () {
108
+ const sidebarContainer = '<div style="position: relative; height: 100%;"></div>'
109
+ const sidebarContentTemplate = $('<div id="ff-node-red-dashboard"></div>').appendTo(sidebarContainer)
110
+
111
+ // convert to i18 text
112
+ function c_ (x) {
113
+ return RED._('@flowforge/node-red-dashboard/ui-base:ui-base.' + x)
114
+ }
115
+
116
+ function hasProperty (obj, prop) {
117
+ return Object.prototype.hasOwnProperty.call(obj, prop)
118
+ }
119
+ function debounce (func, wait, immediate) {
120
+ let timeout
121
+ return function () {
122
+ const context = this; const args = arguments
123
+ const later = function () {
124
+ timeout = null
125
+ if (!immediate) func.apply(context, args)
126
+ }
127
+ const callNow = immediate && !timeout
128
+ clearTimeout(timeout)
129
+ timeout = setTimeout(later, wait)
130
+ if (callNow) func.apply(context, args)
131
+ }
132
+ }
133
+ RED.nodes.registerType('ui-base', {
134
+ category: 'config',
135
+ defaults: {
136
+ name: {
137
+ value: 'UI Name',
138
+ required: true
139
+ },
140
+ path: {
141
+ value: '/dashboard',
142
+ required: true
143
+ }
144
+ },
145
+ label: function () {
146
+ return `${this.name} [${this.path}]` || 'UI Config'
147
+ }
148
+ })
149
+
150
+ /**
151
+ * Add Custom Dashboard Side Menu
152
+ * */
153
+
154
+ const sidebar = $(sidebarContentTemplate)
155
+
156
+ function uiLink (name, path) {
157
+ const base = RED.settings.httpNodeRoot || '/'
158
+ const basePart = base.endsWith('/') ? base : `${base}/`
159
+ const dashPart = path.startsWith('/') ? path.slice(1) : path
160
+ const fullPath = `${basePart}${dashPart}`
161
+ return `<div class="red-ui-sidebar-header"><label>${name}</label><a id="open-dashboard" href="${fullPath}" target="nr-dashboard" class="editor-button editor-button-small nr-db-sb-list-header-button">Open Dashboard<i style="margin-left: 3px;" class="fa fa-external-link"></i></a></div>`
162
+ }
163
+
164
+ /**
165
+ * Add an editor to control the ordering of groups & widgets
166
+ */
167
+
168
+ function updateItemOrder (items, events) {
169
+ items.each((i, el) => {
170
+ const node = el.data('data')
171
+ const nodeBefore = RED.nodes.node(node.id)
172
+ if (node.order !== i + 1) {
173
+ const originalOrder = nodeBefore.order
174
+ const wasDirty = node.dirty
175
+ const wasChanged = node.changed
176
+ // update Node-RED node properties
177
+ node.order = i + 1 // start from 1, makes backup to SAFE_INT logic easier on frontend
178
+ node.dirty = true
179
+ node.changed = true
180
+ // generate a history event
181
+ const hev = {
182
+ t: 'edit',
183
+ node,
184
+ changes: {
185
+ order: originalOrder
186
+ },
187
+ dirty: wasDirty,
188
+ changed: wasChanged
189
+ }
190
+ events.push(hev)
191
+ }
192
+ })
193
+ }
194
+
195
+ // toggle slide tab group content
196
+ const titleToggle = function (id, content, chevron) {
197
+ return function (evt) {
198
+ if (content.is(':visible')) {
199
+ content.slideUp()
200
+ chevron.css({ transform: 'rotate(-90deg)' })
201
+ content.addClass('nr-db-sb-collapsed')
202
+ } else {
203
+ content.slideDown()
204
+ chevron.css({ transform: '' })
205
+ content.removeClass('nr-db-sb-collapsed')
206
+ }
207
+ }
208
+ }
209
+
210
+ // Utility function to store events in NR history, trigger a redraw, and detect if a re-deploy is necessary
211
+ function recordEvents (events) {
212
+ if (events.length === 0) { return } // nothing to record
213
+
214
+ // note the state of the editor before pushing to history
215
+ const isDirty = RED.nodes.dirty()
216
+ if (RED._db2debug) { console.log('recordEvents', isDirty, events) }
217
+
218
+ // add our changes to NR history and trigger whether or not we need to redeploy
219
+ RED.history.push({
220
+ t: 'multi',
221
+ events,
222
+ dirty: isDirty
223
+ })
224
+ RED.nodes.dirty(true)
225
+ RED.view.redraw()
226
+ }
227
+
228
+ // watch for nodes changed, added, removed - use this to refresh the sidebar
229
+ const refreshLayoutEditorDebounced = debounce(refreshLayoutEditor, 300)
230
+ RED.events.on('nodes:change', function (event) {
231
+ if (RED._db2debug) { console.log('nodes:change', event) }
232
+ if (event.dirty && event.type && event.type.startsWith('ui-')) {
233
+ if (RED._db2debug) { console.log('nodes:change - this is a ui- node! queuing a call to refreshLayoutEditor') }
234
+ // debounce the call to refreshLayoutEditor as multiple events can be fired in quick succession
235
+ refreshLayoutEditorDebounced()
236
+ }
237
+ })
238
+ RED.events.on('nodes:add', function (event) {
239
+ if (RED._db2debug) { console.log('nodes:add', event) }
240
+ if (event.dirty && event.type && event.type.startsWith('ui-')) {
241
+ if (RED._db2debug) { console.log('nodes:add - this is a ui- node! queuing a call to refreshLayoutEditor') }
242
+ // debounce the call to refreshLayoutEditor as multiple events can be fired in quick succession
243
+ refreshLayoutEditorDebounced()
244
+ }
245
+ })
246
+ RED.events.on('nodes:remove', function (event) {
247
+ if (RED._db2debug) { console.log('nodes:remove', event) }
248
+ if (event.dirty && event.type && event.type.startsWith('ui-')) {
249
+ if (RED._db2debug) { console.log('nodes:remove - this is a ui- node! queuing a call to refreshLayoutEditor') }
250
+ // debounce the call to refreshLayoutEditor as multiple events can be fired in quick succession
251
+ refreshLayoutEditorDebounced()
252
+ }
253
+ })
254
+
255
+ /**
256
+ * Add group of actions to the right-side of a row in the sidebar editable list.
257
+ * @param {Object} row - jQuery object to add this button group as a child element to
258
+ * @param {Object} row - The page/group/widget that these actions are bound to
259
+ */
260
+ function addRowActions (row, item) {
261
+ const btnGroup = $('<div>', { class: 'nrdb2-sb-list-header-button-group', id: item.id }).appendTo(row)
262
+ 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)
263
+ editButton.on('click', function (evt) {
264
+ if (item.type === 'ui-page' || item.type === 'ui-group') {
265
+ RED.editor.editConfig('', item.type, item.id)
266
+ } else {
267
+ RED.editor.edit(item)
268
+ }
269
+ evt.stopPropagation()
270
+ evt.preventDefault()
271
+ })
272
+ }
273
+
274
+ // Adds child list of groups for a given page
275
+ function addGroupOrderingList (pageId, container, groups, widgetsByGroup) {
276
+ // ordered list of groupss to live within a container (e.g. page list item)
277
+ const groupsOL = $('<ol>', { class: 'nrdb2-sb-group-list' }).appendTo(container).editableList({
278
+ sortable: '.nrdb2-sb-groups-list-header',
279
+ addButton: false,
280
+ height: 'auto',
281
+ connectWith: '.nrdb2-sb-group-list',
282
+ addItem: function (container, i, group) {
283
+ const titleRow = $('<div>', { class: 'nrdb2-sb-list-header nrdb2-sb-groups-list-header' }).appendTo(container)
284
+ $('<i class="nrdb2-sb-list-handle nrdb2-sb-group-list-handle fa fa-bars"></i>').appendTo(titleRow)
285
+ const chevron = $('<i class="fa fa-angle-down nrdb2-sb-list-chevron">', { style: 'width:10px;' }).appendTo(titleRow)
286
+ const groupicon = 'fa-table'
287
+ $('<i>', { class: 'nrdb2-sb-icon nrdb2-sb-group-icon fa ' + groupicon }).appendTo(titleRow)
288
+ $('<span>', { class: 'nrdb2-sb-title' }).text(group.name || group.id).appendTo(titleRow)
289
+
290
+ addRowActions(titleRow, group)
291
+
292
+ // adds widgets within this group
293
+ const widgets = widgetsByGroup[group.id] || []
294
+ const widgetsList = $('<div>', { class: 'nrdb2-sb-widget-list-container' }).appendTo(container)
295
+
296
+ // add chevron/list toggle
297
+ titleRow.click(titleToggle(group.id, widgetsList, chevron))
298
+
299
+ addWidgetToList(group.id, widgetsList, widgets)
300
+ },
301
+ sortItems: function (items) {
302
+ // track any changes
303
+ const events = []
304
+
305
+ // check if we have any new widgets added to this list
306
+ items.each((i, el) => {
307
+ const widget = el.data('data')
308
+ if (widget.page !== pageId) {
309
+ const oldPageId = widget.page
310
+ widget.page = pageId
311
+ events.push({
312
+ t: 'edit',
313
+ node: widget,
314
+ changes: {
315
+ page: oldPageId
316
+ },
317
+ dirty: widget.changed,
318
+ changed: widget.dirty
319
+ })
320
+ }
321
+ })
322
+
323
+ updateItemOrder(items, events)
324
+
325
+ // add our changes to NR history and trigger whether or not we need to redeploy
326
+ recordEvents(events)
327
+ },
328
+ sort: function (a, b) {
329
+ return Number(a.order) - Number(b.order)
330
+ }
331
+ })
332
+
333
+ groups.forEach(function (group) {
334
+ if (RED._db2debug) { if (RED._db2debug) { console.log(group) } }
335
+ groupsOL.editableList('addItem', group)
336
+ })
337
+ }
338
+
339
+ // Adds list of widgets underneath a group
340
+ function addWidgetToList (groupId, container, widgets) {
341
+ // ordered list of groupss to live within a container (e.g. page list item)
342
+ const widgetsOL = $('<ol>', { class: 'nrdb2-sb-widget-list' }).appendTo(container).editableList({
343
+ sortable: '.nrdb2-sb-widgets-list-header',
344
+ addButton: false,
345
+ height: 'auto',
346
+ connectWith: '.nrdb2-sb-widget-list',
347
+ addItem: function (container, i, widget) {
348
+ const titleRow = $('<div>', { class: 'nrdb2-sb-list-header nrdb2-sb-widgets-list-header' }).appendTo(container)
349
+ $('<i class="nrdb2-sb-list-handle nrdb2-sb-widget-list-handle fa fa-bars"></i>').appendTo(titleRow)
350
+
351
+ const groupicon = 'fa-image'
352
+ $('<i>', { class: 'nrdb2-sb-icon nrdb2-sb-widget-icon fa ' + groupicon }).appendTo(titleRow)
353
+ $('<span>', { class: 'nrdb2-sb-title' }).text(widget.name || widget.label || widget.type || widget.id).appendTo(titleRow)
354
+
355
+ addRowActions(titleRow, widget)
356
+ },
357
+ sortItems: function (items) {
358
+ // track any changes
359
+ const events = []
360
+
361
+ // check if we have any new widgets added to this list
362
+ items.each((i, el) => {
363
+ const widget = el.data('data')
364
+ if (widget.group !== groupId) {
365
+ const oldGroupId = widget.group
366
+ widget.group = groupId
367
+ events.push({
368
+ t: 'edit',
369
+ node: widget,
370
+ changes: {
371
+ group: oldGroupId
372
+ },
373
+ dirty: widget.dirty,
374
+ changed: widget.changed
375
+ })
376
+ }
377
+ })
378
+
379
+ updateItemOrder(items, events)
380
+
381
+ // add our changes to NR history and trigger whether or not we need to redeploy
382
+ recordEvents(events)
383
+ },
384
+ sort: function (a, b) {
385
+ return Number(a.order) - Number(b.order)
386
+ }
387
+ })
388
+
389
+ widgets.forEach(function (w) {
390
+ widgetsOL.editableList('addItem', w)
391
+ })
392
+ }
393
+
394
+ // expand / collapse buttons
395
+ let layoutDisplayLevel = 2 // all open by default
396
+ const getGroupsInLayout = function () {
397
+ const content = $('.nrdb2-layout-order-editor > .red-ui-editableList .nrdb2-sb-widget-list-container')
398
+ return {
399
+ content,
400
+ chevrons: content.parent().find('div.nrdb2-sb-list-header > .nrdb2-sb-list-chevron')
401
+ }
402
+ }
403
+ const getPagesInLayout = function () {
404
+ const content = $('.nrdb2-layout-order-editor > .red-ui-editableList .nrdb2-sb-group-list-container')
405
+ return {
406
+ content,
407
+ chevrons: content.parent().find('div.nrdb2-sb-pages-list-header > .nrdb2-sb-list-chevron')
408
+ }
409
+ }
410
+ const collapseLayoutItems = function ({ chevrons, content }) {
411
+ chevrons.css({ transform: 'rotate(-90deg)' })
412
+ content.slideUp()
413
+ content.addClass('nr-db-sb-collapsed')
414
+ }
415
+ const expandLayoutItems = function ({ chevrons, content }) {
416
+ chevrons.css({ transform: '' })
417
+ content.slideDown()
418
+ content.removeClass('nr-db-sb-collapsed')
419
+ }
420
+ /**
421
+ * Update the visibility of the layout editor expandable lists
422
+ * @param {0|1|2} level - 0 = collapse all, 1 = expand pages (groups collapsed), 2 = expand pages and groups (to expose widgets)
423
+ */
424
+ const updateLayoutVisibility = function (level) {
425
+ if (RED._db2debug) { console.log('updateLayoutVisibility', level) }
426
+ if (level === 2) {
427
+ expandLayoutItems(getGroupsInLayout())
428
+ expandLayoutItems(getPagesInLayout())
429
+ } else if (level === 1) {
430
+ expandLayoutItems(getPagesInLayout())
431
+ collapseLayoutItems(getGroupsInLayout())
432
+ } else {
433
+ collapseLayoutItems(getGroupsInLayout())
434
+ collapseLayoutItems(getPagesInLayout())
435
+ }
436
+ }
437
+
438
+ function buildLayoutOrderEditor () {
439
+ // layout/order editor
440
+ const divTabs = $('.nrdb2-layout-order-editor').length ? $('.nrdb2-layout-order-editor') : $('<div>', { class: 'nrdb2-layout-order-editor' }).appendTo(sidebar)
441
+
442
+ // section header - Pages
443
+ const pagesHeader = $('<div>', { class: 'nrdb2-layout-order-editor--pages' }).appendTo(divTabs)
444
+ $('<b>').html(c_('layout.pages')).appendTo(pagesHeader)
445
+
446
+ // toggle "all" buttons
447
+ const buttonGroup = $('<div>', { class: 'nrdb2-sb-list-button-group' }).appendTo(pagesHeader)
448
+
449
+ const buttonCollapse = $('<a href="#" class="editor-button editor-button-small nrdb2-sb-list-header-button"><i class="fa fa-angle-double-up"></i></a>')
450
+ .click(function (evt) {
451
+ evt.preventDefault()
452
+ if (--layoutDisplayLevel < 0) { layoutDisplayLevel = 0 }
453
+ updateLayoutVisibility(layoutDisplayLevel)
454
+ })
455
+ .appendTo(buttonGroup)
456
+ RED.popover.tooltip(buttonCollapse, c_('layout.collapse'))
457
+
458
+ // expand button
459
+ const buttonExpand = $('<a href="#" class="editor-button editor-button-small nrdb2-sb-list-header-button"><i class="fa fa-angle-double-down"></i></a>')
460
+ .click(function (evt) {
461
+ if (++layoutDisplayLevel > 2) { layoutDisplayLevel = 2 }
462
+ updateLayoutVisibility(layoutDisplayLevel)
463
+ }).appendTo(buttonGroup)
464
+ .appendTo(buttonGroup)
465
+ RED.popover.tooltip(buttonExpand, c_('layout.expand'))
466
+
467
+ divTabs.append('<div class="nrdb2-layout-helptext">Here you can re-order and move your widgets, groups and pages.</div>')
468
+
469
+ const pages = {}
470
+ const groupsByPage = {}
471
+ const widgetsByGroup = {}
472
+
473
+ // get all pages & all groups
474
+ RED.nodes.eachConfig(function (n) {
475
+ if (n.type === 'ui-page' && !!n.ui) {
476
+ pages[n.id] = n
477
+ }
478
+ if (n.type === 'ui-group' && !!n.page) {
479
+ if (!groupsByPage[n.page]) {
480
+ groupsByPage[n.page] = []
481
+ }
482
+ groupsByPage[n.page].push(n)
483
+ }
484
+ })
485
+
486
+ // get all widgets
487
+ RED.nodes.eachNode(function (n) {
488
+ if (/^ui-/.test(n.type) && n.group) {
489
+ if (!widgetsByGroup[n.group]) {
490
+ widgetsByGroup[n.group] = []
491
+ }
492
+ widgetsByGroup[n.group].push(n)
493
+ }
494
+ })
495
+
496
+ const pagesOL = $('<ol>', { class: 'nrdb2-sb-pages-list' }).appendTo(divTabs).editableList({
497
+ sortable: '.nrdb2-sb-pages-list-header',
498
+ addButton: false,
499
+ addItem: function (container, i, page) {
500
+ container.addClass('nrdb2-sb-pages-list-item')
501
+
502
+ const titleRow = $('<div>', { class: 'nrdb2-sb-list-header nrdb2-sb-pages-list-header' }).appendTo(container)
503
+ $('<i class="nrdb2-sb-list-handle nrdb2-sb-page-list-handle fa fa-bars"></i>').appendTo(titleRow)
504
+ const chevron = $('<i class="fa fa-angle-down nrdb2-sb-list-chevron">', { style: 'width:10px;' }).appendTo(titleRow)
505
+ const tabicon = 'fa-object-group'
506
+ $('<i>', { class: 'nrdb2-sb-icon nrdb2-sb-tab-icon fa ' + tabicon }).appendTo(titleRow)
507
+ $('<span>', { class: 'nrdb2-sb-title' }).text(page.name || page.id).appendTo(titleRow)
508
+
509
+ // page - actions
510
+ addRowActions(titleRow, page)
511
+
512
+ // adds groups within this page
513
+ const groups = groupsByPage[page.id] || []
514
+ const groupsList = $('<div>', { class: 'nrdb2-sb-group-list-container' }).appendTo(container)
515
+
516
+ titleRow.click(titleToggle(page.id, groupsList, chevron))
517
+
518
+ addGroupOrderingList(page.id, groupsList, groups, widgetsByGroup)
519
+ },
520
+ sortItems: function (items) {
521
+ // track any changes
522
+ const events = []
523
+ updateItemOrder(items, events)
524
+
525
+ // add our changes to NR history and trigger whether or not we need to redeploy
526
+ recordEvents(events)
527
+ },
528
+ sort: function (a, b) {
529
+ return Number(a.order) - Number(b.order)
530
+ }
531
+ })
532
+
533
+ Object.values(groupsByPage).sort((a, b) => a.order - b.order).forEach(function (groups) {
534
+ if (RED._db2debug) { console.log(groups) }
535
+ const page = pages[groups[0].page]
536
+ if (page) {
537
+ pagesOL.editableList('addItem', page)
538
+ }
539
+ // groups.forEach(() => {
540
+
541
+ // })
542
+ })
543
+
544
+ // call updateLayoutVisibility to sync display level
545
+ updateLayoutVisibility(layoutDisplayLevel)
546
+ }
547
+
548
+ function refreshLayoutEditor () {
549
+ if (RED._db2debug) { console.log('refreshLayoutEditor called') }
550
+ const layoutOrderDiv = $('.nrdb2-layout-order-editor')
551
+ // empty the list if any items exist
552
+ if (layoutOrderDiv.length) {
553
+ // TODO: create a lookup of which items are expanded / collapsed
554
+ layoutOrderDiv.empty()
555
+ }
556
+
557
+ // now rebuild
558
+ buildLayoutOrderEditor()
559
+
560
+ // finally, restore previous state of expanded/collapsed items
561
+ // TODO: expand/collapse any items that were expanded before
562
+ // for now, we will just re-sync the display level
563
+ updateLayoutVisibility(layoutDisplayLevel)
564
+ }
565
+
566
+ RED.sidebar.addTab({
567
+ id: 'dashboard-2.0',
568
+ label: 'Dashboard 2.0',
569
+ name: 'Dashboard 2.0',
570
+ content: sidebar,
571
+ closeable: true,
572
+ pinned: true,
573
+ disableOnEdit: true,
574
+ iconClass: 'fa fa-bar-chart',
575
+ action: '@flowforge/node-red-dashboard:show-dashboard-2.0-tab',
576
+ onchange: function () {
577
+ sidebar.empty()
578
+ RED.nodes.eachConfig(function (n) {
579
+ if (n.type === 'ui-base') {
580
+ sidebar.append(uiLink(n.name, n.path))
581
+ }
582
+ })
583
+
584
+ // add layout editor
585
+ buildLayoutOrderEditor()
586
+ }
587
+ })
588
+
589
+ RED.actions.add('@flowforge/node-red-dashboard:show-dashboard-2.0-tab', function () {
590
+ RED.sidebar.show('flowforge-nr-tools')
591
+ })
592
+
593
+ /**
594
+ * jQuery widget to provide a selector for the sizing (width & height) of a widget & group
595
+ */
596
+ $.widget('nodereddashboard.elementSizer', {
597
+ _create: function () {
598
+ // convert to i18 text
599
+ function c_ (x) {
600
+ return RED._(`@flowforge/node-red-dashboard/ui-base:ui-base.${x}`)
601
+ }
602
+
603
+ const thisWidget = this
604
+ let gridWidth = 6
605
+ const width = parseInt($(this.options.width).val() || 0)
606
+ const height = parseInt(hasProperty(this.options, 'height') ? $(this.options.height).val() : '1') || 0
607
+ const hasAuto = (!hasProperty(this.options, 'auto') || this.options.auto)
608
+
609
+ this.element.css({
610
+ minWidth: this.element.height() + 4
611
+ })
612
+ const autoText = c_('auto')
613
+ const sizeLabel = (width === 0 && height === 0) ? autoText : width + (hasProperty(this.options, 'height') ? ' x ' + height : '')
614
+ this.element.text(sizeLabel).on('mousedown', function (evt) {
615
+ evt.stopPropagation()
616
+ evt.preventDefault()
617
+
618
+ const width = parseInt($(thisWidget.options.width).val() || 0)
619
+ const height = parseInt(hasProperty(thisWidget.options, 'height') ? $(thisWidget.options.height).val() : '1') || 0
620
+ let maxWidth = 0
621
+ let maxHeight
622
+ let fixedWidth = false
623
+ const fixedHeight = false
624
+ const group = $(thisWidget.options.group).val()
625
+ if (group) {
626
+ const groupNode = RED.nodes.node(group)
627
+ if (groupNode) {
628
+ gridWidth = Math.max(6, groupNode.width, +width)
629
+ maxWidth = groupNode.width || gridWidth
630
+ fixedWidth = true
631
+ }
632
+ maxHeight = Math.max(6, +height + 1)
633
+ } else {
634
+ gridWidth = Math.max(12, +width)
635
+ maxWidth = gridWidth
636
+ maxHeight = height + 1
637
+ // fixedHeight = false;
638
+ }
639
+
640
+ const pos = $(this).offset()
641
+ const container = $('<div>').css({
642
+ position: 'absolute',
643
+ background: 'var(--red-ui-secondary-background, white)',
644
+ padding: '5px 10px 10px 10px',
645
+ border: '1px solid var(--red-ui-primary-border-color, grey)',
646
+ zIndex: '20',
647
+ borderRadius: '4px',
648
+ display: 'none'
649
+ }).appendTo(document.body)
650
+
651
+ let closeTimer
652
+ container.on('mouseleave', function (evt) {
653
+ closeTimer = setTimeout(function () {
654
+ container.fadeOut(200, function () { $(this).remove() })
655
+ }, 100)
656
+ })
657
+ container.on('mouseenter', function () {
658
+ clearTimeout(closeTimer)
659
+ })
660
+
661
+ const label = $('<div>').css({
662
+ fontSize: '13px',
663
+ color: 'var(--red-ui-tertiary-text-color, #aaa)',
664
+ float: 'left',
665
+ paddingTop: '1px'
666
+ }).appendTo(container).text((width === 0 && height === 0) ? autoText : (width + (hasProperty(thisWidget.options, 'height') ? ' x ' + height : '')))
667
+ label.hover(function () {
668
+ $(this).css('text-decoration', 'underline')
669
+ }, function () {
670
+ $(this).css('text-decoration', 'none')
671
+ })
672
+
673
+ label.click(function (e) {
674
+ const group = $(thisWidget.options.group).val()
675
+ let groupNode = null
676
+ if (group) {
677
+ groupNode = RED.nodes.node(group)
678
+ if (groupNode === null) {
679
+ return
680
+ }
681
+ }
682
+ $(thisWidget).elementSizerByNum({
683
+ width: thisWidget.options.width,
684
+ height: thisWidget.options.height,
685
+ groupNode,
686
+ pos,
687
+ label: thisWidget.element,
688
+ has_height: hasProperty(thisWidget.options, 'height')
689
+ })
690
+ closeTimer = setTimeout(function () {
691
+ container.fadeOut(200, function () {
692
+ $(this).remove()
693
+ })
694
+ }, 100)
695
+ })
696
+
697
+ const buttonRow = $('<div>', { style: 'text-align:right; height:25px;' }).appendTo(container)
698
+
699
+ if (hasAuto) {
700
+ $('<a>', { href: '#', class: 'editor-button editor-button-small', style: 'margin-bottom:5px' })
701
+ .text(autoText)
702
+ .appendTo(buttonRow)
703
+ .on('mouseup', function (evt) {
704
+ thisWidget.element.text(autoText)
705
+ $(thisWidget.options.width).val(0).change()
706
+ $(thisWidget.options.height).val(0).change()
707
+ evt.preventDefault()
708
+ container.fadeOut(200, function () { $(this).remove() })
709
+ })
710
+ }
711
+
712
+ const cellBorder = '1px dashed var(--red-ui-secondary-border-color, lightGray)'
713
+ const cellBorderExisting = '1px solid gray'
714
+ const cellBorderHighlight = '1px dashed var(--red-ui-primary-border-color, black)'
715
+ const rows = []
716
+ const cells = []
717
+
718
+ function addRow (i) {
719
+ const row = $('<div>').css({ padding: 0, margin: 0, height: '25px', 'box-sizing': 'border-box' }).appendTo(container)
720
+ rows.push(row)
721
+ cells.push([])
722
+ for (let j = 0; j < gridWidth; j++) {
723
+ addCell(i, j)
724
+ }
725
+ }
726
+
727
+ function addCell (i, j) {
728
+ const row = rows[i]
729
+ const cell = $('<div>').css({
730
+ display: 'inline-block',
731
+ width: '25px',
732
+ height: '25px',
733
+ borderRight: (j === (width - 1) && i < height) ? cellBorderExisting : cellBorder,
734
+ borderBottom: (i === (height - 1) && j < width) ? cellBorderExisting : cellBorder,
735
+ boxSizing: 'border-box',
736
+ cursor: 'pointer',
737
+ background: (j < maxWidth) ? 'var(--red-ui-secondary-background, #fff)' : 'var(--red-ui-node-background-placeholder, #eee)'
738
+ }).appendTo(row)
739
+ cells[i].push(cell)
740
+ if (j === 0) {
741
+ cell.css({ borderLeft: ((i <= height - 1) ? cellBorderExisting : cellBorder) })
742
+ }
743
+ if (i === 0) {
744
+ cell.css({ borderTop: ((j <= width - 1) ? cellBorderExisting : cellBorder) })
745
+ }
746
+ if (j < maxWidth) {
747
+ cell.data('w', j)
748
+ cell.data('h', i)
749
+ cell.on('mouseup', function () {
750
+ thisWidget.element.text(($(this).data('w') + 1) + (hasProperty(thisWidget.options, 'height') ? ' x ' + ($(this).data('h') + 1) : ''))
751
+ $(thisWidget.options.width).val($(this).data('w') + 1).change()
752
+ $(thisWidget.options.height).val($(this).data('h') + 1).change()
753
+ container.fadeOut(200, function () { $(this).remove() })
754
+ })
755
+ cell.on('mouseover', function () {
756
+ const w = $(this).data('w')
757
+ const h = $(this).data('h')
758
+ label.text((w + 1) + (hasProperty(thisWidget.options, 'height') ? ' x ' + (h + 1) : ''))
759
+ for (let y = 0; y < maxHeight; y++) {
760
+ for (let x = 0; x < maxWidth; x++) {
761
+ cells[y][x].css({
762
+ background: (y <= h && x <= w) ? 'var(--red-ui-secondary-background-selected, #ddd)' : 'var(--red-ui-secondary-background, #fff)',
763
+ borderLeft: (x === 0 && y <= h) ? cellBorderHighlight : (x === 0) ? ((y <= height - 1) ? cellBorderExisting : cellBorder) : '',
764
+ borderTop: (y === 0 && x <= w) ? cellBorderHighlight : (y === 0) ? ((x <= width - 1) ? cellBorderExisting : cellBorder) : '',
765
+ borderRight: (x === w && y <= h) ? cellBorderHighlight : ((x === width - 1 && y <= height - 1) ? cellBorderExisting : cellBorder),
766
+ borderBottom: (y === h && x <= w) ? cellBorderHighlight : ((y === height - 1 && x <= width - 1) ? cellBorderExisting : cellBorder)
767
+ })
768
+ }
769
+ }
770
+ if (!fixedHeight && h === maxHeight - 1) {
771
+ addRow(maxHeight++)
772
+ }
773
+ if (!fixedWidth && w === maxWidth - 1) {
774
+ maxWidth++
775
+ gridWidth++
776
+ for (let r = 0; r < maxHeight; r++) {
777
+ addCell(r, maxWidth - 1)
778
+ }
779
+ }
780
+ })
781
+ }
782
+ }
783
+ for (let i = 0; i < maxHeight; i++) {
784
+ addRow(i)
785
+ }
786
+ container.css({
787
+ top: (pos.top) + 'px',
788
+ left: (pos.left) + 'px'
789
+ })
790
+ container.fadeIn(200)
791
+ })
792
+ }
793
+ })
794
+ })()
795
+ </script>
796
+
797
+ <script type="text/html" data-template-name="ui-base">
798
+ <div class="form-row">
799
+ <label for="node-config-input-name"><i class="fa fa-bookmark"></i> Name</label>
800
+ <input type="text" id="node-config-input-name">
801
+ </div>
802
+ <div class="form-row">
803
+ <label for="node-config-input-path"><i class="fa fa-bookmark"></i> Path</label>
804
+ <input type="text" id="node-config-input-path" disabled>
805
+ <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>
806
+ </div>
807
+ </script>