Skip to content

Commit a92044b

Browse files
Add dashboard grid layout presets (#12)
1 parent 6ba8620 commit a92044b

3 files changed

Lines changed: 169 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ The rules for this file:
3333
- Reduce package size by moving away from mdi/font to mdi/js (PR #9)
3434
- Added support to display imdclient session info (PR #10)
3535
- Added widget inputs support (PR #11)
36+
- Added dashboard grid layout presets (PR #12)
3637

3738
### Fixed
3839

mdadash/frontend/src/__tests__/views/DashboardView.spec.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,4 +366,32 @@ describe('DashboardView.vue', () => {
366366
// check remove widget sent to server
367367
expect(socket.emit).toHaveBeenCalledWith('widgets:remove_widget', 'uuid1')
368368
})
369+
370+
it('test display presets', async () => {
371+
const wrapper = mount(KeepAliveDashboardView, {
372+
global: {
373+
provide: allProvides,
374+
},
375+
})
376+
expect(wrapper.findComponent(DashboardView).exists()).toBe(true)
377+
const dashboard = wrapper.findComponent(DashboardView)
378+
expect(dashboard.exists()).toBe(true)
379+
// create test layout
380+
dashboard.vm.layoutWidgets = widgetsLayout
381+
dashboard.vm.displayedLayoutWidgets = ref(widgetsLayout)
382+
await flushPromises()
383+
// check preset values
384+
let components = dashboard.findAllComponents({ name: 'VSelect' })
385+
const gridPresetSelect = components[0]
386+
gridPresetSelect.vm.$slots.item({ item: dashboard.vm.gridPresetIconItems })
387+
expect(gridPresetSelect).toBeDefined()
388+
await gridPresetSelect.setValue(dashboard.vm.gridPresetIcons.col1)
389+
expect(dashboard.vm.gridEditable).toBe(false)
390+
await gridPresetSelect.setValue(dashboard.vm.gridPresetIcons.col2)
391+
expect(dashboard.vm.gridEditable).toBe(false)
392+
await gridPresetSelect.setValue(dashboard.vm.gridPresetIcons.col3)
393+
expect(dashboard.vm.gridEditable).toBe(false)
394+
await gridPresetSelect.setValue(dashboard.vm.gridPresetIcons.editable)
395+
expect(dashboard.vm.gridEditable).toBe(true)
396+
})
369397
})

mdadash/frontend/src/views/DashboardView.vue

Lines changed: 140 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,39 @@
8181

8282
<!-- Filter Widgets and Add Widget -->
8383
<div class="d-flex align-center gap-4 mb-4">
84+
<div>
85+
<v-select
86+
id="grid-preset"
87+
width="56"
88+
class="custom-v-select-field-height"
89+
v-model="gridPresetIcon"
90+
:items="gridPresetIconItems"
91+
item-value="icon"
92+
variant="solo"
93+
hide-details
94+
menu-icon=""
95+
@update:model-value="handleGridPresetChange"
96+
v-tooltip="{ text: 'Arrange grid', location: 'left' }"
97+
>
98+
<template #selection>
99+
<v-icon :icon="gridPresetIcon" size="large"></v-icon>
100+
</template>
101+
<template #item="{ props, item }">
102+
<div
103+
v-bind="props"
104+
class="v-list-item v-list-item--link d-flex justify-center align-center py-2 px-3"
105+
>
106+
<v-icon :icon="item.icon"></v-icon>
107+
<!-- v8 ignore start -->
108+
<v-tooltip activator="parent">
109+
{{ item.desc }}
110+
</v-tooltip>
111+
<!-- v8 ignore stop -->
112+
</div>
113+
<v-divider v-if="item.name != 'col3'"></v-divider>
114+
</template>
115+
</v-select>
116+
</div>
84117
<!-- Filter Widgets -->
85118
<v-sheet elevation="1" width="100%" rounded>
86119
<v-autocomplete
@@ -180,8 +213,8 @@
180213
@update:layout="updateDisplayedLayoutWidgets"
181214
:key="widgetsGridKey"
182215
:row-height="30"
183-
:is-draggable="true"
184-
:is-resizable="true"
216+
:is-draggable="gridEditable"
217+
:is-resizable="gridEditable"
185218
:responsive="false"
186219
@selectstart.prevent
187220
>
@@ -253,7 +286,7 @@
253286

254287
<script setup>
255288
import { socket } from '@/socket'
256-
import { ref, inject, nextTick, onActivated, onDeactivated } from 'vue'
289+
import { computed, ref, inject, nextTick, onActivated, onDeactivated, h } from 'vue'
257290
import { GridLayout, GridItem } from 'grid-layout-plus'
258291
import { useRouter } from 'vue-router'
259292
import {
@@ -270,6 +303,53 @@ import {
270303
mdiClose,
271304
} from '@mdi/js'
272305
306+
const gridPresetIcons = {
307+
editable: () =>
308+
h('svg', { viewBox: '0 0 24 24' }, [
309+
h('path', {
310+
d: 'M4 5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5ZM14 5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1V5ZM4 16a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-3ZM14 13a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1v-6Z',
311+
fill: 'none',
312+
stroke: '#000000',
313+
'stroke-width': 2,
314+
'stroke-linecap': 'round',
315+
'stroke-linejoin': 'round',
316+
}),
317+
]),
318+
col1: () =>
319+
h('svg', { viewBox: '0 0 24 24' }, [
320+
h('path', {
321+
d: 'M6 5C6 4.44772 6.44772 4 7 4H17C17.5523 4 18 4.44772 18 5V19C18 19.5523 17.5523 20 17 20H7C6.44772 20 6 19.5523 6 19V5Z',
322+
fill: 'none',
323+
stroke: '#000000',
324+
'stroke-width': 2,
325+
'stroke-linecap': 'round',
326+
'stroke-linejoin': 'round',
327+
}),
328+
]),
329+
col2: () =>
330+
h('svg', { viewBox: '0 0 24 24' }, [
331+
h('path', {
332+
d: 'M8,2 C9.65685425,2 11,3.34314575 11,5 L11,19 C11,20.6568542 9.65685425,22 8,22 L5,22 C3.34314575,22 2,20.6568542 2,19 L2,5 C2,3.34314575 3.34314575,2 5,2 L8,2 Z M19,2 C20.6568542,2 22,3.34314575 22,5 L22,19 C22,20.6568542 20.6568542,22 19,22 L16,22 C14.3431458,22 13,20.6568542 13,19 L13,5 C13,3.34314575 14.3431458,2 16,2 L19,2 Z M8,4 L5,4 C4.44771525,4 4,4.44771525 4,5 L4,19 C4,19.5522847 4.44771525,20 5,20 L8,20 C8.55228475,20 9,19.5522847 9,19 L9,5 C9,4.44771525 8.55228475,4 8,4 Z M19,4 L16,4 C15.4477153,4 15,4.44771525 15,5 L15,19 C15,19.5522847 15.4477153,20 16,20 L19,20 C19.5522847,20 20,19.5522847 20,19 L20,5 C20,4.44771525 19.5522847,4 19,4 Z',
333+
fill: '#000000',
334+
}),
335+
]),
336+
col3: () =>
337+
h('svg', { viewBox: '0 0 24 24' }, [
338+
h('path', {
339+
d: 'M6.23694 3.0004C7.20344 3.0004 7.98694 3.7839 7.98694 4.7504V19.2504C7.98694 20.2169 7.20344 21.0004 6.23694 21.0004H3.73694C2.77044 21.0004 1.98694 20.2169 1.98694 19.2504V4.7504C1.98694 3.83223 2.69405 3.07921 3.59341 3.0062L3.73694 3.0004H6.23694ZM20.263 3.0004C21.2295 3.0004 22.013 3.7839 22.013 4.7504V19.2504C22.013 20.2169 21.2295 21.0004 20.263 21.0004H17.763C16.7965 21.0004 16.013 20.2169 16.013 19.2504V4.7504C16.013 3.7839 16.7965 3.0004 17.763 3.0004H20.263ZM13.2369 2.99957C14.2034 2.99957 14.9869 3.78307 14.9869 4.74957V19.2496C14.9869 20.2161 14.2034 20.9996 13.2369 20.9996H10.7369C9.77044 20.9996 8.98694 20.2161 8.98694 19.2496V4.74957C8.98694 3.78307 9.77044 2.99957 10.7369 2.99957H13.2369ZM6.23694 4.5004H3.73694L3.67962 4.50701C3.56917 4.53292 3.48694 4.63206 3.48694 4.7504V19.2504C3.48694 19.3885 3.59887 19.5004 3.73694 19.5004H6.23694C6.37501 19.5004 6.48694 19.3885 6.48694 19.2504V4.7504C6.48694 4.61233 6.37501 4.5004 6.23694 4.5004ZM20.263 4.5004H17.763C17.6249 4.5004 17.513 4.61233 17.513 4.7504V19.2504C17.513 19.3885 17.6249 19.5004 17.763 19.5004H20.263C20.4011 19.5004 20.513 19.3885 20.513 19.2504V4.7504C20.513 4.61233 20.4011 4.5004 20.263 4.5004ZM13.2369 4.49957H10.7369C10.5989 4.49957 10.4869 4.6115 10.4869 4.74957V19.2496C10.4869 19.3876 10.5989 19.4996 10.7369 19.4996H13.2369C13.375 19.4996 13.4869 19.3876 13.4869 19.2496V4.74957C13.4869 4.6115 13.375 4.49957 13.2369 4.49957Z',
340+
fill: '#000000',
341+
}),
342+
]),
343+
}
344+
const gridPresetIcon = ref(gridPresetIcons.editable)
345+
const gridPresetIconItems = ref([
346+
{ name: 'editable', icon: gridPresetIcons.editable, desc: 'Default' },
347+
{ name: 'col1', icon: gridPresetIcons.col1, desc: 'One column' },
348+
{ name: 'col2', icon: gridPresetIcons.col2, desc: 'Two columns' },
349+
{ name: 'col3', icon: gridPresetIcons.col3, desc: 'Three columns' },
350+
])
351+
const gridEditable = ref(true)
352+
273353
const router = useRouter()
274354
275355
const sessionInfoItems = [
@@ -314,6 +394,45 @@ const widgetsGridKey = ref(0)
314394
const widgetOutputs = ref({})
315395
var displayedLayoutWidgets = ref([...layoutWidgets.value])
316396
397+
const displayedLayoutWidgetsByPosition = computed(() => {
398+
return [...displayedLayoutWidgets.value].sort((a, b) => {
399+
if (a.y !== b.y) {
400+
return a.y - b.y
401+
}
402+
return a.x - b.x
403+
})
404+
})
405+
406+
const handleGridPresetChange = (newPreset) => {
407+
let x = () => 0
408+
let w = 12
409+
let h = 14
410+
if (newPreset === gridPresetIcons.editable) {
411+
if (selectedLayoutWidgets.value.length == 0) {
412+
gridEditable.value = true
413+
displayedLayoutWidgets = ref([...layoutWidgets.value])
414+
return
415+
}
416+
} else if (newPreset === gridPresetIcons.col2) {
417+
x = (index) => (index % 2) * 6
418+
w = 6
419+
h = 11
420+
} else if (newPreset === gridPresetIcons.col3) {
421+
x = (index) => (index % 3) * 4
422+
w = 4
423+
h = 9
424+
}
425+
gridEditable.value = false
426+
displayedLayoutWidgets.value = displayedLayoutWidgetsByPosition.value.map((item, index) => ({
427+
...item,
428+
x: x(index),
429+
y: 0,
430+
w: w,
431+
h: h,
432+
}))
433+
widgetsGridKey.value++
434+
}
435+
317436
function setAddWidgetMenuState(value) {
318437
isAddWidgetOpen.value = value
319438
}
@@ -342,6 +461,7 @@ const onWidgetsGridFilter = () => {
342461
.filter((item) => selectedLayoutWidgets.value.includes(item.i))
343462
.map((item) => ({ ...item, x: 0, y: 0, w: 12, h: 14 }))
344463
}
464+
handleGridPresetChange(gridPresetIcon.value)
345465
widgetsGridKey.value++
346466
}
347467
@@ -421,6 +541,7 @@ onActivated(() => {
421541
// v8 ignore next
422542
if (selectedLayoutWidgets.value.length == 0) {
423543
displayedLayoutWidgets = ref([...layoutWidgets.value])
544+
handleGridPresetChange(gridPresetIcon.value)
424545
widgetsGridKey.value++
425546
}
426547
}
@@ -438,4 +559,19 @@ onDeactivated(() => {
438559
})
439560
</script>
440561

441-
<style scoped></style>
562+
<style scoped>
563+
/* These are required to force v-select height to 56px */
564+
.custom-v-select-field-height :deep(.v-field) {
565+
--v-input-control-height: 56px !important;
566+
min-height: 56px !important;
567+
height: 56px !important;
568+
}
569+
570+
.custom-v-select-field-height :deep(.v-field__input) {
571+
min-height: 56px !important;
572+
height: 56px !important;
573+
padding-top: 0px !important;
574+
padding-bottom: 0px !important;
575+
align-items: center;
576+
}
577+
</style>

0 commit comments

Comments
 (0)