From a7d8268193ee4384ee8b37db49063ea2404fcdac Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Thu, 14 Nov 2024 14:36:38 +0000 Subject: [PATCH 01/11] update edit tracking for widgets resizing/reordering --- ui/src/EditTracking.js | 85 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 6 deletions(-) diff --git a/ui/src/EditTracking.js b/ui/src/EditTracking.js index d3114b82..95750fed 100644 --- a/ui/src/EditTracking.js +++ b/ui/src/EditTracking.js @@ -7,7 +7,8 @@ const state = reactive({ editMode: false, editorPath: '', // the custom httpAdminRoot path for the NR editor isTrackingEdits: false, - originalGroups: [] + originalGroups: [], + originalWidgets: [] }) // Methods @@ -27,9 +28,9 @@ function initialise (editKey, editPage, editorPath) { /** * Start tracking edits */ -function startEditTracking (groups) { +function startEditTracking (groups, widgets) { state.isTrackingEdits = true - updateEditTracking(groups) + updateEditTracking(groups, widgets) } /** @@ -42,13 +43,34 @@ function exitEditMode () { state.isTrackingEdits = false state.initialised = false state.originalGroups = [] + state.originalWidgets = [] } /** * Update the original groups with the current groups */ -function updateEditTracking (groups) { - state.originalGroups = JSON.parse(JSON.stringify(groups)) +function updateEditTracking (groups, widgets) { + if (typeof groups !== 'undefined') { + state.originalGroups = JSON.parse(JSON.stringify(groups)) + } + if (typeof widgets !== 'undefined') { + // only store the id, props and layout of each widget (that's all we need for comparison) + const groupIds = Object.keys(widgets) + const partials = {} + for (let i = 0; i < groupIds.length; i++) { + const groupId = groupIds[i] + const groupWidgets = widgets[groupId] + const partialWidgets = groupWidgets.map((w) => { + return { + id: w.id, + props: w.props, + layout: w.layout + } + }) + partials[groupId] = partialWidgets + } + state.originalWidgets = JSON.parse(JSON.stringify(partials)) + } } // RO computed props @@ -57,6 +79,57 @@ const editPage = computed(() => state.editPage) const editMode = computed(() => !!state.editKey && !!state.editPage) const editorPath = computed(() => state.editorPath) const originalGroups = computed(() => state.originalGroups) +const originalWidgets = computed(() => state.originalWidgets) const isTrackingEdits = computed(() => state.isTrackingEdits) -export { editKey, editMode, editPage, editorPath, originalGroups, isTrackingEdits, initialise, exitEditMode, startEditTracking, updateEditTracking } +const groupPropertiesToCheck = [ + (original, current) => +original.width === +current.width, + (original, current) => +original.height === +current.height, + (original, current) => +original.order === +current.order +] + +const widgetPropertiesToCheck = [ + (original, current) => +original.layout?.width === +current.layout?.width, + (original, current) => +original.layout?.height === +current.layout?.height, + (original, current) => +original.layout?.order === +current.layout?.order, + (original, current) => +original.props?.width === +current.props?.width, + (original, current) => +original.props?.height === +current.props?.height, + (original, current) => +original.props?.order === +current.props?.order +] + +function isDirty (groups, widgets) { + console.log('isDirty', groups, widgets) + const originalGroups = state.originalGroups || [] + // scan through each group and revert changes + + for (let i = 0; i < originalGroups.length; i++) { + const originalGroup = originalGroups[i] + const currentGroup = groups?.find(group => group.id === originalGroup.id) + if (!currentGroup) { + console.warn('Group not found in pageGroups - as we do not currently support adding/removing groups, this should not happen!') + return true + } + // test group properties + if (groupPropertiesToCheck.some(check => !check(originalGroup, currentGroup))) { + return true + } + + // test widgets belonging to this group + const originalWidgetValues = state.originalWidgets?.[originalGroup.id] || [] + const currentWidgets = widgets?.[originalGroup.id] || [] + for (let j = 0; j < originalWidgetValues.length; j++) { + const originalWidget = originalWidgetValues[j] + const currentWidget = currentWidgets.find(widget => widget.id === originalWidget.id) + if (!currentWidget) { + console.warn('Widget not found in pageWidgets - as we do not currently support adding/removing widgets, this should not happen!') + return true + } + if (widgetPropertiesToCheck.some(check => !check(originalWidget, currentWidget))) { + return true + } + } + } + return false +} + +export { editKey, editMode, editPage, editorPath, originalGroups, originalWidgets, isDirty, isTrackingEdits, initialise, exitEditMode, startEditTracking, updateEditTracking } From 2bd22974782ecf2e9d72bc73bc1a79516f704c0e Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Thu, 14 Nov 2024 14:39:58 +0000 Subject: [PATCH 02/11] add widgets resizing/reordering support --- ui/src/layouts/Flex.vue | 49 +++- ui/src/layouts/Grid.vue | 49 +++- ui/src/layouts/Group.vue | 126 ++++++++-- .../{draggable.js => draggableGroup.js} | 57 ++--- ui/src/layouts/wysiwyg/draggableWidget.js | 113 +++++++++ ui/src/layouts/wysiwyg/index.js | 71 ++++-- ui/src/layouts/wysiwyg/resizable.js | 220 ++++++++++++++---- 7 files changed, 572 insertions(+), 113 deletions(-) rename ui/src/layouts/wysiwyg/{draggable.js => draggableGroup.js} (63%) create mode 100644 ui/src/layouts/wysiwyg/draggableWidget.js diff --git a/ui/src/layouts/Flex.vue b/ui/src/layouts/Flex.vue index 2d4b4e95..f8583953 100644 --- a/ui/src/layouts/Flex.vue +++ b/ui/src/layouts/Flex.vue @@ -10,10 +10,10 @@ :class="getGroupClass(g)" :style="{'width': ((rowHeight * 2 * g.width) + 'px')}" :draggable="editMode" - @dragstart="onDragStart($event, $index)" - @dragover="onDragOver($event, $index, g)" - @dragend="onDragEnd($event, $index, g)" - @dragleave="onDragLeave($event, $index, g)" + @dragstart="onGroupDragStart($event, $index, g)" + @dragover="onGroupDragOver($event, $index, g)" + @dragend="onGroupDragEnd($event, $index, g)" + @dragleave="onGroupDragLeave($event, $index, g)" @drop.prevent @dragenter.prevent > @@ -22,7 +22,7 @@ {{ g.name }} @@ -98,6 +98,12 @@ export default { pageWidgets: function () { return this.widgetsByPage(this.$route.meta.id) }, + groupWidgets () { + if (this.editMode) { // mixin property + return (groupId) => this.pageGroupWidgets[groupId] + } + return (groupId) => this.widgetsByGroup(groupId) + }, page: function () { return this.pages[this.$route.meta.id] }, @@ -109,8 +115,14 @@ export default { } }, mounted () { + console.log('flex layout mounted') if (this.editMode) { // mixin property this.pageGroups = this.getPageGroups() + const pageGroupWidgets = {} + for (const group of this.pageGroups) { + pageGroupWidgets[group.id] = this.getGroupWidgets(group.id) + } + this.pageGroupWidgets = pageGroupWidgets this.initializeEditTracking() // Mixin method } }, @@ -130,6 +142,21 @@ export default { }) return groups }, + getGroupWidgets (groupId) { + // get widgets for this group (sorted by layout.order) + const widgets = this.widgetsByGroup(groupId) + // only show the widgets that haven't had their "visible" property set to false + .filter((g) => { + if ('visible' in g) { + return g.visible && g.groupType !== 'dialog' + } + return true + }) + .sort((a, b) => { + return a?.layout?.order - b?.layout?.order + }) + return widgets + }, getWidgetClass (widget) { const classes = [] // ensure each widget has a class for its type @@ -154,7 +181,7 @@ export default { classes.push(properties.class) } // dragging interaction classes - const dragDropClass = this.getDragDropClass(group) // Mixin method + const dragDropClass = this.getGroupDragDropClass(group) // Mixin method if (dragDropClass) { classes.push(dragDropClass) } @@ -179,7 +206,8 @@ export default { this.deployChanges({ dashboard: this.page.ui, page: this.page.id, - groups: this.pageGroups + groups: this.pageGroups, + widgets: this.pageGroupWidgets }).then(() => { this.acceptChanges() // Mixin method }).catch((error) => { @@ -218,6 +246,13 @@ export default { this.discardEdits() } this.exitEditMode() // Mixin method + }, + onGroupResize (opts) { + // ensure opts.width is a number and is greater than 0 + if (typeof opts.width !== 'number' || opts.width < 1) { + return + } + this.pageGroups[opts.index].width = opts.width } } } diff --git a/ui/src/layouts/Grid.vue b/ui/src/layouts/Grid.vue index e6afc56a..d42563a8 100644 --- a/ui/src/layouts/Grid.vue +++ b/ui/src/layouts/Grid.vue @@ -10,10 +10,10 @@ :class="getGroupClass(g)" :style="`grid-column-end: span min(${ g.width }, var(--layout-columns)`" :draggable="editMode" - @dragstart="onDragStart($event, $index)" - @dragover="onDragOver($event, $index, g)" - @dragend="onDragEnd($event, $index, g)" - @dragleave="onDragLeave($event, $index, g)" + @dragstart="onGroupDragStart($event, $index, g)" + @dragover="onGroupDragOver($event, $index, g)" + @dragend="onGroupDragEnd($event, $index, g)" + @dragleave="onGroupDragLeave($event, $index, g)" @drop.prevent @dragenter.prevent > @@ -22,7 +22,7 @@ {{ g.name }} @@ -106,11 +106,23 @@ export default { return this.pageGroups } return this.getPageGroups() + }, + groupWidgets () { + if (this.editMode) { // mixin property + return (groupId) => this.pageGroupWidgets[groupId] + } + return (groupId) => this.widgetsByGroup(groupId) } }, mounted () { + console.log('grid layout mounted') if (this.editMode) { // mixin property this.pageGroups = this.getPageGroups() + const pageGroupWidgets = {} + for (const group of this.pageGroups) { + pageGroupWidgets[group.id] = this.getGroupWidgets(group.id) + } + this.pageGroupWidgets = pageGroupWidgets this.initializeEditTracking() // Mixin method } }, @@ -142,6 +154,21 @@ export default { } return classes.join(' ') }, + getGroupWidgets (groupId) { + // get widgets for this group (sorted by layout.order) + const widgets = this.widgetsByGroup(groupId) + // only show the widgets that haven't had their "visible" property set to false + .filter((g) => { + if ('visible' in g) { + return g.visible && g.groupType !== 'dialog' + } + return true + }) + .sort((a, b) => { + return a?.layout?.order - b?.layout?.order + }) + return widgets + }, getGroupClass (group) { const classes = [] // add any class set in the group's properties @@ -154,7 +181,7 @@ export default { classes.push(properties.class) } // dragging interaction classes - const dragDropClass = this.getDragDropClass(group) // Mixin method + const dragDropClass = this.getGroupDragDropClass(group) // Mixin method if (dragDropClass) { classes.push(dragDropClass) } @@ -179,7 +206,8 @@ export default { this.deployChanges({ dashboard: this.page.ui, page: this.page.id, - groups: this.pageGroups + groups: this.pageGroups, + widgets: this.pageGroupWidgets }).then(() => { this.acceptChanges() // Mixin method }).catch((error) => { @@ -218,6 +246,13 @@ export default { this.discardEdits() } this.exitEditMode() // Mixin method + }, + onGroupResize (opts) { + // ensure opts.width is a number and is greater than 0 + if (typeof opts.width !== 'number' || opts.width < 1) { + return + } + this.pageGroups[opts.index].width = opts.width } } } diff --git a/ui/src/layouts/Group.vue b/ui/src/layouts/Group.vue index a901ac30..9e59851d 100644 --- a/ui/src/layouts/Group.vue +++ b/ui/src/layouts/Group.vue @@ -1,35 +1,75 @@