@adminforth/dashboard 1.0.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 (107) hide show
  1. package/.woodpecker/buildRelease.sh +13 -0
  2. package/.woodpecker/buildSlackNotify.sh +46 -0
  3. package/.woodpecker/release.yml +57 -0
  4. package/README.md +59 -0
  5. package/custom/api/dashboardApi.ts +213 -0
  6. package/custom/composables/useElementSize.ts +41 -0
  7. package/custom/model/dashboard.types.ts +73 -0
  8. package/custom/package.json +9 -0
  9. package/custom/pnpm-lock.yaml +24 -0
  10. package/custom/queries/useDashboardConfig.ts +51 -0
  11. package/custom/queries/useWidgetData.ts +51 -0
  12. package/custom/runtime/DashboardGroup.vue +185 -0
  13. package/custom/runtime/DashboardPage.vue +122 -0
  14. package/custom/runtime/DashboardRuntime.vue +435 -0
  15. package/custom/runtime/WidgetRenderer.vue +60 -0
  16. package/custom/runtime/WidgetShell.vue +152 -0
  17. package/custom/skills/adminforth-dashboard/SKILL.md +125 -0
  18. package/custom/widgets/chart/ChartWidget.vue +188 -0
  19. package/custom/widgets/chart/bar/BarChart.vue +167 -0
  20. package/custom/widgets/chart/chart.types.ts +34 -0
  21. package/custom/widgets/chart/chart.utils.ts +54 -0
  22. package/custom/widgets/chart/funnel/FunnelChart.vue +197 -0
  23. package/custom/widgets/chart/histogram/HistogramChart.vue +21 -0
  24. package/custom/widgets/chart/line/LineChart.vue +175 -0
  25. package/custom/widgets/chart/pie/PieChart.vue +161 -0
  26. package/custom/widgets/chart/stacked-bar/StackedBarChart.vue +256 -0
  27. package/custom/widgets/gauge-card/GaugeCardWidget.vue +107 -0
  28. package/custom/widgets/kpi-card/KpiCardWidget.vue +73 -0
  29. package/custom/widgets/pivot-table/PivotTableWidget.vue +122 -0
  30. package/custom/widgets/registry.ts +51 -0
  31. package/custom/widgets/table/TableWidget.vue +110 -0
  32. package/dist/custom/api/dashboardApi.d.ts +32 -0
  33. package/dist/custom/api/dashboardApi.js +179 -0
  34. package/dist/custom/api/dashboardApi.ts +213 -0
  35. package/dist/custom/composables/useElementSize.d.ts +8 -0
  36. package/dist/custom/composables/useElementSize.js +30 -0
  37. package/dist/custom/composables/useElementSize.ts +41 -0
  38. package/dist/custom/model/dashboard.types.d.ts +45 -0
  39. package/dist/custom/model/dashboard.types.js +14 -0
  40. package/dist/custom/model/dashboard.types.ts +73 -0
  41. package/dist/custom/package.json +9 -0
  42. package/dist/custom/pnpm-lock.yaml +24 -0
  43. package/dist/custom/queries/useDashboardConfig.d.ts +112 -0
  44. package/dist/custom/queries/useDashboardConfig.js +57 -0
  45. package/dist/custom/queries/useDashboardConfig.ts +51 -0
  46. package/dist/custom/queries/useWidgetData.d.ts +90 -0
  47. package/dist/custom/queries/useWidgetData.js +57 -0
  48. package/dist/custom/queries/useWidgetData.ts +51 -0
  49. package/dist/custom/runtime/DashboardGroup.vue +185 -0
  50. package/dist/custom/runtime/DashboardPage.vue +122 -0
  51. package/dist/custom/runtime/DashboardRuntime.vue +435 -0
  52. package/dist/custom/runtime/WidgetRenderer.vue +60 -0
  53. package/dist/custom/runtime/WidgetShell.vue +152 -0
  54. package/dist/custom/skills/adminforth-dashboard/SKILL.md +125 -0
  55. package/dist/custom/widgets/chart/ChartWidget.vue +188 -0
  56. package/dist/custom/widgets/chart/bar/BarChart.vue +167 -0
  57. package/dist/custom/widgets/chart/chart.types.d.ts +25 -0
  58. package/dist/custom/widgets/chart/chart.types.js +2 -0
  59. package/dist/custom/widgets/chart/chart.types.ts +34 -0
  60. package/dist/custom/widgets/chart/chart.utils.d.ts +5 -0
  61. package/dist/custom/widgets/chart/chart.utils.js +52 -0
  62. package/dist/custom/widgets/chart/chart.utils.ts +54 -0
  63. package/dist/custom/widgets/chart/funnel/FunnelChart.vue +197 -0
  64. package/dist/custom/widgets/chart/histogram/HistogramChart.vue +21 -0
  65. package/dist/custom/widgets/chart/line/LineChart.vue +175 -0
  66. package/dist/custom/widgets/chart/pie/PieChart.vue +161 -0
  67. package/dist/custom/widgets/chart/stacked-bar/StackedBarChart.vue +256 -0
  68. package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +107 -0
  69. package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +73 -0
  70. package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +122 -0
  71. package/dist/custom/widgets/registry.d.ts +11 -0
  72. package/dist/custom/widgets/registry.js +47 -0
  73. package/dist/custom/widgets/registry.ts +51 -0
  74. package/dist/custom/widgets/table/TableWidget.vue +110 -0
  75. package/dist/endpoint/dashboard.d.ts +7 -0
  76. package/dist/endpoint/dashboard.js +29 -0
  77. package/dist/endpoint/groups.d.ts +30 -0
  78. package/dist/endpoint/groups.js +131 -0
  79. package/dist/endpoint/widgets.d.ts +15 -0
  80. package/dist/endpoint/widgets.js +182 -0
  81. package/dist/index.d.ts +13 -0
  82. package/dist/index.js +124 -0
  83. package/dist/schema/api.d.ts +1205 -0
  84. package/dist/schema/api.js +84 -0
  85. package/dist/schema/widget.d.ts +514 -0
  86. package/dist/schema/widget.js +133 -0
  87. package/dist/services/dashboardConfigService.d.ts +35 -0
  88. package/dist/services/dashboardConfigService.js +79 -0
  89. package/dist/services/widgetConfigValidator.d.ts +8 -0
  90. package/dist/services/widgetConfigValidator.js +65 -0
  91. package/dist/services/widgetDataService.d.ts +20 -0
  92. package/dist/services/widgetDataService.js +32 -0
  93. package/dist/types.d.ts +8 -0
  94. package/dist/types.js +1 -0
  95. package/endpoint/dashboard.ts +32 -0
  96. package/endpoint/groups.ts +213 -0
  97. package/endpoint/widgets.ts +255 -0
  98. package/index.ts +141 -0
  99. package/package.json +64 -0
  100. package/schema/api.ts +99 -0
  101. package/schema/widget.ts +159 -0
  102. package/services/dashboardConfigService.ts +136 -0
  103. package/services/widgetConfigValidator.ts +93 -0
  104. package/services/widgetDataService.ts +57 -0
  105. package/shims-vue.d.ts +5 -0
  106. package/tsconfig.json +18 -0
  107. package/types.ts +8 -0
@@ -0,0 +1,435 @@
1
+ <template>
2
+ <section class="w-full p-6">
3
+ <header class="mb-6 flex items-start justify-between">
4
+ <div>
5
+ <h1 class="m-0 text-2xl font-bold text-lightNavbarText dark:text-darkNavbarText">
6
+ {{ label }}
7
+ </h1>
8
+
9
+ <div
10
+ v-if="isAdmin"
11
+ class="mt-1.5 flex gap-3 text-xs text-lightListTableText dark:text-darkListTableText"
12
+ >
13
+ <span>Slug: {{ dashboardSlug }}</span>
14
+ <span>Revision: {{ currentRevision }}</span>
15
+ <span v-if="isRefreshing">Refreshing...</span>
16
+ </div>
17
+ </div>
18
+ </header>
19
+
20
+ <div class="flex flex-col gap-5">
21
+ <DashboardGroup
22
+ v-for="(group, index) in visibleGroups"
23
+ :key="group.id"
24
+ :group="group"
25
+ :widgets="widgetsByGroupId.get(group.id) ?? []"
26
+ :dashboard-slug="dashboardSlug"
27
+ :is-admin="isAdmin"
28
+ :can-move-up="index > 0"
29
+ :can-move-down="index < visibleGroups.length - 1"
30
+ @add-widget="addWidget(group.id)"
31
+ @move-up="moveGroup(group.id, 'up')"
32
+ @move-down="moveGroup(group.id, 'down')"
33
+ @remove-group="removeGroup(group.id)"
34
+ @edit-group="editGroup"
35
+ @edit-widget="editWidget"
36
+ @move-widget-up="moveWidget($event, 'up')"
37
+ @move-widget-down="moveWidget($event, 'down')"
38
+ @remove-widget="removeWidget"
39
+ />
40
+
41
+ <Button
42
+ v-if="isAdmin"
43
+ type="button"
44
+ mode="secondary"
45
+ class="h-10 w-28 border text-xs font-semibold"
46
+ @click="addGroup"
47
+ >
48
+ Add group
49
+ </Button>
50
+ </div>
51
+
52
+ <div
53
+ v-if="editingGroupId"
54
+ class="fixed inset-0 z-50 flex items-center justify-center bg-black/35 p-4"
55
+ @click.self="closeGroupConfigEditor"
56
+ >
57
+ <section class="w-full max-w-2xl rounded-lg border border-lightListBorder bg-lightDropdownOptionsBackground p-4 shadow-xl dark:border-darkListBorder dark:bg-darkDropdownOptionsBackground">
58
+ <header class="mb-3 flex items-center justify-between gap-3">
59
+ <h2 class="m-0 text-base font-bold text-lightNavbarText dark:text-darkNavbarText">
60
+ Group JSON
61
+ </h2>
62
+
63
+ <button
64
+ type="button"
65
+ class="flex h-9 w-9 items-center justify-center rounded-lg text-lightListTableText hover:bg-lightListViewButtonBackgroundHover dark:text-darkListTableText dark:hover:bg-darkListViewButtonBackgroundHover"
66
+ @click="closeGroupConfigEditor"
67
+ >
68
+ <svg
69
+ class="h-4 w-4"
70
+ viewBox="0 0 24 24"
71
+ fill="none"
72
+ stroke="currentColor"
73
+ stroke-width="2"
74
+ stroke-linecap="round"
75
+ stroke-linejoin="round"
76
+ aria-hidden="true"
77
+ >
78
+ <path d="M18 6 6 18" />
79
+ <path d="m6 6 12 12" />
80
+ </svg>
81
+ </button>
82
+ </header>
83
+
84
+ <textarea
85
+ v-model="groupConfigCode"
86
+ class="min-h-[500px] w-full resize-y rounded-lg border border-lightListBorder bg-lightListTable p-3 font-mono text-sm text-lightNavbarText outline-none focus:border-lightPrimaryButtonBackground dark:border-darkListBorder dark:bg-darkListTable dark:text-darkNavbarText dark:focus:border-darkPrimaryButtonBackground"
87
+ spellcheck="false"
88
+ @keydown.ctrl.enter.prevent="saveGroupConfig"
89
+ @keydown.meta.enter.prevent="saveGroupConfig"
90
+ />
91
+
92
+ <div
93
+ v-if="groupConfigError"
94
+ class="mt-2 text-sm text-lightInputErrorColor"
95
+ >
96
+ {{ groupConfigError }}
97
+ </div>
98
+
99
+ <div class="mt-4 flex justify-end gap-2">
100
+ <Button
101
+ type="button"
102
+ mode="secondary"
103
+ @click="closeGroupConfigEditor"
104
+ >
105
+ Cancel
106
+ </Button>
107
+
108
+ <Button
109
+ type="button"
110
+ @click="saveGroupConfig"
111
+ >
112
+ Save
113
+ </Button>
114
+ </div>
115
+ </section>
116
+ </div>
117
+
118
+ <div
119
+ v-if="editingWidgetId"
120
+ class="fixed inset-0 z-50 flex items-center justify-center bg-black/35 p-4"
121
+ @click.self="closeWidgetConfigEditor"
122
+ >
123
+ <section class="w-full max-w-2xl rounded-lg border border-lightListBorder bg-lightDropdownOptionsBackground p-4 shadow-xl dark:border-darkListBorder dark:bg-darkDropdownOptionsBackground">
124
+ <header class="mb-3 flex items-center justify-between gap-3">
125
+ <h2 class="m-0 text-base font-bold text-lightNavbarText dark:text-darkNavbarText">
126
+ Widget JSON
127
+ </h2>
128
+
129
+ <button
130
+ type="button"
131
+ class="flex h-9 w-9 items-center justify-center rounded-lg text-lightListTableText hover:bg-lightListViewButtonBackgroundHover dark:text-darkListTableText dark:hover:bg-darkListViewButtonBackgroundHover"
132
+ @click="closeWidgetConfigEditor"
133
+ >
134
+ <svg
135
+ class="h-4 w-4"
136
+ viewBox="0 0 24 24"
137
+ fill="none"
138
+ stroke="currentColor"
139
+ stroke-width="2"
140
+ stroke-linecap="round"
141
+ stroke-linejoin="round"
142
+ aria-hidden="true"
143
+ >
144
+ <path d="M18 6 6 18" />
145
+ <path d="m6 6 12 12" />
146
+ </svg>
147
+ </button>
148
+ </header>
149
+
150
+ <textarea
151
+ v-model="widgetConfigCode"
152
+ class="min-h-[500px] w-full resize-y rounded-lg border border-lightListBorder bg-lightListTable p-3 font-mono text-sm text-lightNavbarText outline-none focus:border-lightPrimaryButtonBackground dark:border-darkListBorder dark:bg-darkListTable dark:text-darkNavbarText dark:focus:border-darkPrimaryButtonBackground"
153
+ spellcheck="false"
154
+ @keydown.ctrl.enter.prevent="saveWidgetConfig"
155
+ @keydown.meta.enter.prevent="saveWidgetConfig"
156
+ />
157
+
158
+ <div
159
+ v-if="widgetConfigError"
160
+ class="mt-2 text-sm text-lightInputErrorColor"
161
+ >
162
+ {{ widgetConfigError }}
163
+ </div>
164
+
165
+ <ul
166
+ v-if="widgetConfigFieldErrors.length"
167
+ class="mt-2 grid gap-1 text-sm text-lightInputErrorColor"
168
+ >
169
+ <li
170
+ v-for="validationError in widgetConfigFieldErrors"
171
+ :key="`${validationError.field}-${validationError.message}`"
172
+ >
173
+ <span class="font-semibold">{{ validationError.field }}:</span>
174
+ {{ validationError.message }}
175
+ </li>
176
+ </ul>
177
+
178
+ <div class="mt-4 flex justify-end gap-2">
179
+ <Button
180
+ type="button"
181
+ mode="secondary"
182
+ @click="closeWidgetConfigEditor"
183
+ >
184
+ Cancel
185
+ </Button>
186
+
187
+ <Button
188
+ type="button"
189
+ @click="saveWidgetConfig"
190
+ >
191
+ Save
192
+ </Button>
193
+ </div>
194
+ </section>
195
+ </div>
196
+ </section>
197
+ </template>
198
+
199
+
200
+
201
+ <script setup lang="ts">
202
+ import { computed, ref, watch } from 'vue'
203
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'
204
+ import { Button } from '@/afcl'
205
+ import DashboardGroup from './DashboardGroup.vue'
206
+ import { DashboardApiError, dashboardApi, type DashboardResponse } from '../api/dashboardApi.js'
207
+ import type {
208
+ DashboardConfig,
209
+ DashboardGroupConfig,
210
+ DashboardGroupMoveDirection,
211
+ DashboardWidgetConfig,
212
+ DashboardWidgetMoveDirection,
213
+ } from '../model/dashboard.types.js'
214
+
215
+ const props = defineProps<{
216
+ dashboardSlug: string
217
+ dashboardId: string
218
+ label: string
219
+ config: DashboardConfig
220
+ revision: number
221
+ isAdmin: boolean
222
+ isRefreshing: boolean
223
+ }>()
224
+
225
+ const draftConfig = ref<DashboardConfig>(cloneConfig(props.config))
226
+ const currentRevision = ref(props.revision)
227
+ const editingGroupId = ref<string | null>(null)
228
+ const groupConfigCode = ref('')
229
+ const groupConfigError = ref('')
230
+ const editingWidgetId = ref<string | null>(null)
231
+ const widgetConfigCode = ref('')
232
+ const widgetConfigError = ref('')
233
+ const widgetConfigFieldErrors = ref<{ field: string, message: string }[]>([])
234
+
235
+ watch(
236
+ () => props.config,
237
+ (config: DashboardConfig) => {
238
+ draftConfig.value = cloneConfig(config)
239
+ },
240
+ { deep: true },
241
+ )
242
+
243
+ watch(
244
+ () => props.revision,
245
+ (revision: number) => {
246
+ currentRevision.value = revision
247
+ },
248
+ )
249
+
250
+ const sortedGroups = computed(() => {
251
+ return [...draftConfig.value.groups].sort((a, b) => a.order - b.order)
252
+ })
253
+
254
+ const widgetsByGroupId = computed<Map<string, DashboardWidgetConfig[]>>(() => {
255
+ const result = new Map<string, DashboardWidgetConfig[]>()
256
+
257
+ for (const widget of draftConfig.value.widgets) {
258
+ const widgets = result.get(widget.group_id) ?? []
259
+ widgets.push(widget)
260
+ result.set(widget.group_id, widgets)
261
+ }
262
+
263
+ for (const [groupId, widgets] of result.entries()) {
264
+ result.set(groupId, [...widgets].sort((a, b) => a.order - b.order))
265
+ }
266
+
267
+ return result
268
+ })
269
+
270
+ const visibleGroups = computed(() => {
271
+ if (props.isAdmin) {
272
+ return sortedGroups.value
273
+ }
274
+
275
+ return sortedGroups.value.filter((group) => {
276
+ return (widgetsByGroupId.value.get(group.id) ?? []).length > 0
277
+ })
278
+ })
279
+
280
+ async function addGroup() {
281
+ if (!props.isAdmin) {
282
+ return
283
+ }
284
+
285
+ try {
286
+ applyDashboardResponse(await dashboardApi.addDashboardGroup(props.dashboardSlug))
287
+ } catch (error) {
288
+ console.error('Failed to add dashboard group', error)
289
+ }
290
+ }
291
+
292
+ async function addWidget(groupId: string) {
293
+ if (!props.isAdmin) {
294
+ return
295
+ }
296
+
297
+ try {
298
+ applyDashboardResponse(await dashboardApi.addDashboardWidget(props.dashboardSlug, groupId))
299
+ } catch (error) {
300
+ console.error('Failed to add dashboard widget', error)
301
+ }
302
+ }
303
+
304
+ async function moveGroup(groupId: string, direction: DashboardGroupMoveDirection) {
305
+ if (!props.isAdmin) {
306
+ return
307
+ }
308
+
309
+ try {
310
+ applyDashboardResponse(
311
+ await dashboardApi.moveDashboardGroup(props.dashboardSlug, groupId, direction),
312
+ )
313
+ } catch (error) {
314
+ console.error('Failed to move dashboard group', error)
315
+ }
316
+ }
317
+
318
+ async function removeGroup(groupId: string) {
319
+ if (!props.isAdmin) {
320
+ return
321
+ }
322
+
323
+ try {
324
+ applyDashboardResponse(await dashboardApi.removeDashboardGroup(props.dashboardSlug, groupId))
325
+ } catch (error) {
326
+ console.error('Failed to remove dashboard group', error)
327
+ }
328
+ }
329
+
330
+ function editGroup(group: DashboardGroupConfig) {
331
+ editingGroupId.value = group.id
332
+ groupConfigCode.value = stringifyYaml(group)
333
+ groupConfigError.value = ''
334
+ }
335
+
336
+ async function saveGroupConfig() {
337
+ if (!editingGroupId.value) {
338
+ return
339
+ }
340
+
341
+ try {
342
+ const groupConfig = parseYaml(groupConfigCode.value) as DashboardGroupConfig
343
+
344
+ applyDashboardResponse(
345
+ await dashboardApi.setDashboardGroupConfig(
346
+ props.dashboardSlug,
347
+ editingGroupId.value,
348
+ groupConfig,
349
+ ),
350
+ )
351
+ closeGroupConfigEditor()
352
+ } catch (error) {
353
+ groupConfigError.value = error instanceof Error ? error.message : 'Invalid group config'
354
+ }
355
+ }
356
+
357
+ function closeGroupConfigEditor() {
358
+ editingGroupId.value = null
359
+ groupConfigCode.value = ''
360
+ groupConfigError.value = ''
361
+ }
362
+
363
+ async function moveWidget(widgetId: string, direction: DashboardWidgetMoveDirection) {
364
+ if (!props.isAdmin) {
365
+ return
366
+ }
367
+
368
+ try {
369
+ applyDashboardResponse(
370
+ await dashboardApi.moveDashboardWidget(props.dashboardSlug, widgetId, direction),
371
+ )
372
+ } catch (error) {
373
+ console.error('Failed to move dashboard widget', error)
374
+ }
375
+ }
376
+
377
+ async function removeWidget(widgetId: string) {
378
+ if (!props.isAdmin) {
379
+ return
380
+ }
381
+
382
+ try {
383
+ applyDashboardResponse(await dashboardApi.removeDashboardWidget(props.dashboardSlug, widgetId))
384
+ } catch (error) {
385
+ console.error('Failed to remove dashboard widget', error)
386
+ }
387
+ }
388
+
389
+ function editWidget(widget: DashboardWidgetConfig) {
390
+ editingWidgetId.value = widget.id
391
+ widgetConfigCode.value = stringifyYaml(widget)
392
+ widgetConfigError.value = ''
393
+ widgetConfigFieldErrors.value = []
394
+ }
395
+
396
+ async function saveWidgetConfig() {
397
+ if (!editingWidgetId.value) {
398
+ return
399
+ }
400
+
401
+ try {
402
+ widgetConfigError.value = ''
403
+ widgetConfigFieldErrors.value = []
404
+ const widgetConfig = parseYaml(widgetConfigCode.value) as DashboardWidgetConfig
405
+
406
+ applyDashboardResponse(
407
+ await dashboardApi.setWidgetConfig(
408
+ props.dashboardSlug,
409
+ editingWidgetId.value,
410
+ widgetConfig,
411
+ ),
412
+ )
413
+ closeWidgetConfigEditor()
414
+ } catch (error) {
415
+ widgetConfigError.value = error instanceof Error ? error.message : 'Invalid widget config'
416
+ widgetConfigFieldErrors.value = error instanceof DashboardApiError ? error.validationErrors : []
417
+ }
418
+ }
419
+
420
+ function closeWidgetConfigEditor() {
421
+ editingWidgetId.value = null
422
+ widgetConfigCode.value = ''
423
+ widgetConfigError.value = ''
424
+ widgetConfigFieldErrors.value = []
425
+ }
426
+
427
+ function applyDashboardResponse(response: DashboardResponse) {
428
+ draftConfig.value = cloneConfig(response.config)
429
+ currentRevision.value = response.revision
430
+ }
431
+
432
+ function cloneConfig(config: DashboardConfig): DashboardConfig {
433
+ return JSON.parse(JSON.stringify(config))
434
+ }
435
+ </script>
@@ -0,0 +1,60 @@
1
+ <template>
2
+ <div class="flex h-full min-h-0 flex-col gap-1">
3
+ <div
4
+ v-if="isAdmin"
5
+ class="text-xs font-bold uppercase tracking-normal text-lightListTableText dark:text-darkListTableText"
6
+ >
7
+ {{ widget.target }}
8
+ </div>
9
+
10
+ <div class="text-base font-semibold text-lightNavbarText dark:text-darkNavbarText">
11
+ {{ widgetTitle }}
12
+ </div>
13
+
14
+ <component
15
+ :is="widgetComponent"
16
+ v-if="widgetComponent"
17
+ class="mt-3 min-h-0 flex-1 overflow-hidden"
18
+ :widget="widget"
19
+ :dashboard-slug="dashboardSlug"
20
+ />
21
+ </div>
22
+ </template>
23
+
24
+
25
+
26
+ <script setup lang="ts">
27
+ import { computed } from 'vue'
28
+ import type { DashboardWidgetConfig } from '../model/dashboard.types.js'
29
+ import { getWidgetLabel, getWidgetRegistration } from '../widgets/registry.js'
30
+
31
+ const props = defineProps<{
32
+ widget: DashboardWidgetConfig
33
+ dashboardSlug: string
34
+ isAdmin: boolean
35
+ }>()
36
+
37
+ const widgetRegistration = computed(() => {
38
+ return getWidgetRegistration(props.widget.target)
39
+ })
40
+
41
+ const widgetComponent = computed(() => {
42
+ return widgetRegistration.value?.component
43
+ })
44
+
45
+ const widgetTitle = computed(() => {
46
+ if (props.widget.label) {
47
+ return props.widget.label
48
+ }
49
+
50
+ if (props.widget.target === 'empty') {
51
+ return 'Empty widget'
52
+ }
53
+
54
+ if (props.widget.target === 'chart') {
55
+ return props.widget.chart?.title || 'Untitled chart'
56
+ }
57
+
58
+ return getWidgetLabel(props.widget.target)
59
+ })
60
+ </script>
@@ -0,0 +1,152 @@
1
+ <template>
2
+ <div
3
+ class="group relative flex min-h-24 grow shrink basis-[var(--widget-basis)] flex-col overflow-hidden rounded-lg bg-lightListTable p-3 min-w-[var(--widget-min-width)] max-w-[var(--widget-max-width)] dark:bg-darkListTable"
4
+ :class="isAdmin ? 'border border-dashed border-lightListBorder dark:border-darkListBorder' : ''"
5
+ :style="widgetLayoutVars"
6
+ >
7
+ <slot />
8
+
9
+ <div
10
+ v-if="isAdmin"
11
+ class="absolute right-2 top-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100"
12
+ >
13
+ <button
14
+ type="button"
15
+ class="flex h-8 w-8 items-center justify-center rounded-lg border border-lightListViewButtonBorder bg-lightListViewButtonBackground text-lightListViewButtonText shadow-sm hover:bg-lightListViewButtonBackgroundHover hover:text-lightListViewButtonTextHover dark:border-darkListViewButtonBorder dark:bg-darkListViewButtonBackground dark:text-darkListViewButtonText dark:hover:bg-darkListViewButtonBackgroundHover dark:hover:text-darkListViewButtonTextHover"
16
+ title="Edit JSON"
17
+ @click="emit('edit')"
18
+ >
19
+ <svg
20
+ class="h-4 w-4"
21
+ viewBox="0 0 24 24"
22
+ fill="none"
23
+ stroke="currentColor"
24
+ stroke-width="1.8"
25
+ stroke-linecap="round"
26
+ stroke-linejoin="round"
27
+ aria-hidden="true"
28
+ >
29
+ <path d="M15.5 7.5a3 3 0 1 1 1 2.2l-6.8 6.8H7.5v2.2H5.3v2.2H2.8v-2.5l7.5-7.5a5.5 5.5 0 1 1 5.2 1.6" />
30
+ </svg>
31
+ </button>
32
+
33
+ <button
34
+ type="button"
35
+ class="flex h-8 w-8 items-center justify-center rounded-lg border border-lightListViewButtonBorder bg-lightListViewButtonBackground text-lightListViewButtonText shadow-sm hover:bg-lightListViewButtonBackgroundHover hover:text-lightListViewButtonTextHover disabled:opacity-45 dark:border-darkListViewButtonBorder dark:bg-darkListViewButtonBackground dark:text-darkListViewButtonText dark:hover:bg-darkListViewButtonBackgroundHover dark:hover:text-darkListViewButtonTextHover"
36
+ title="Move up"
37
+ :disabled="!canMoveUp"
38
+ @click="emit('move-up')"
39
+ >
40
+ <svg
41
+ class="h-4 w-4"
42
+ viewBox="0 0 24 24"
43
+ fill="none"
44
+ stroke="currentColor"
45
+ stroke-width="2"
46
+ stroke-linecap="round"
47
+ stroke-linejoin="round"
48
+ aria-hidden="true"
49
+ >
50
+ <path d="m18 15-6-6-6 6" />
51
+ </svg>
52
+ </button>
53
+
54
+ <button
55
+ type="button"
56
+ class="flex h-8 w-8 items-center justify-center rounded-lg border border-lightListViewButtonBorder bg-lightListViewButtonBackground text-lightListViewButtonText shadow-sm hover:bg-lightListViewButtonBackgroundHover hover:text-lightListViewButtonTextHover disabled:opacity-45 dark:border-darkListViewButtonBorder dark:bg-darkListViewButtonBackground dark:text-darkListViewButtonText dark:hover:bg-darkListViewButtonBackgroundHover dark:hover:text-darkListViewButtonTextHover"
57
+ title="Move down"
58
+ :disabled="!canMoveDown"
59
+ @click="emit('move-down')"
60
+ >
61
+ <svg
62
+ class="h-4 w-4"
63
+ viewBox="0 0 24 24"
64
+ fill="none"
65
+ stroke="currentColor"
66
+ stroke-width="2"
67
+ stroke-linecap="round"
68
+ stroke-linejoin="round"
69
+ aria-hidden="true"
70
+ >
71
+ <path d="m6 9 6 6 6-6" />
72
+ </svg>
73
+ </button>
74
+
75
+ <button
76
+ type="button"
77
+ class="flex h-8 w-8 items-center justify-center rounded-lg border border-lightInputErrorColor/30 bg-lightSecondary text-lightInputErrorColor shadow-sm hover:bg-lightListViewButtonBackgroundHover dark:bg-darkSecondary dark:hover:bg-darkListViewButtonBackgroundHover"
78
+ title="Remove"
79
+ @click="emit('remove')"
80
+ >
81
+ <svg
82
+ class="h-4 w-4"
83
+ viewBox="0 0 24 24"
84
+ fill="none"
85
+ stroke="currentColor"
86
+ stroke-width="2"
87
+ stroke-linecap="round"
88
+ stroke-linejoin="round"
89
+ aria-hidden="true"
90
+ >
91
+ <path d="M3 6h18" />
92
+ <path d="M8 6V4h8v2" />
93
+ <path d="M19 6l-1 14H6L5 6" />
94
+ <path d="M10 11v5" />
95
+ <path d="M14 11v5" />
96
+ </svg>
97
+ </button>
98
+ </div>
99
+ </div>
100
+ </template>
101
+
102
+
103
+
104
+ <script setup lang="ts">
105
+ import { computed } from 'vue'
106
+ import type { CSSProperties } from 'vue'
107
+ import type { WidgetLayout } from '../model/dashboard.types.js'
108
+
109
+ const DEFAULT_WIDGET_HEIGHT = 500
110
+
111
+ const sizeToFlexBasis: Record<NonNullable<WidgetLayout['size']>, string> = {
112
+ small: '260px',
113
+ medium: '360px',
114
+ large: '480px',
115
+ wide: '720px',
116
+ full: '100%',
117
+ }
118
+
119
+ const props = defineProps<{
120
+ isAdmin: boolean
121
+ canMoveUp: boolean
122
+ canMoveDown: boolean
123
+ layout?: WidgetLayout
124
+ }>()
125
+
126
+ const emit = defineEmits<{
127
+ (e: 'edit'): void
128
+ (e: 'move-up'): void
129
+ (e: 'move-down'): void
130
+ (e: 'remove'): void
131
+ }>()
132
+
133
+ const widgetLayoutVars = computed<CSSProperties>(() => {
134
+ const basis = sizeToFlexBasis[props.layout?.size ?? 'medium']
135
+ const fixedWidth = formatWidth(props.layout?.width)
136
+
137
+ return {
138
+ '--widget-basis': fixedWidth ?? basis,
139
+ '--widget-min-width': fixedWidth ?? formatWidth(props.layout?.minWidth) ?? basis,
140
+ '--widget-max-width': props.layout?.maxWidth === null
141
+ ? 'none'
142
+ : fixedWidth ?? formatWidth(props.layout?.maxWidth) ?? 'none',
143
+ height: formatWidth(props.layout?.height ?? DEFAULT_WIDGET_HEIGHT),
144
+ }
145
+ })
146
+
147
+ function formatWidth(value: number | undefined) {
148
+ if (typeof value === 'number') {
149
+ return `${value}px`
150
+ }
151
+ }
152
+ </script>