@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.
- package/.woodpecker/buildRelease.sh +13 -0
- package/.woodpecker/buildSlackNotify.sh +46 -0
- package/.woodpecker/release.yml +57 -0
- package/README.md +59 -0
- package/custom/api/dashboardApi.ts +213 -0
- package/custom/composables/useElementSize.ts +41 -0
- package/custom/model/dashboard.types.ts +73 -0
- package/custom/package.json +9 -0
- package/custom/pnpm-lock.yaml +24 -0
- package/custom/queries/useDashboardConfig.ts +51 -0
- package/custom/queries/useWidgetData.ts +51 -0
- package/custom/runtime/DashboardGroup.vue +185 -0
- package/custom/runtime/DashboardPage.vue +122 -0
- package/custom/runtime/DashboardRuntime.vue +435 -0
- package/custom/runtime/WidgetRenderer.vue +60 -0
- package/custom/runtime/WidgetShell.vue +152 -0
- package/custom/skills/adminforth-dashboard/SKILL.md +125 -0
- package/custom/widgets/chart/ChartWidget.vue +188 -0
- package/custom/widgets/chart/bar/BarChart.vue +167 -0
- package/custom/widgets/chart/chart.types.ts +34 -0
- package/custom/widgets/chart/chart.utils.ts +54 -0
- package/custom/widgets/chart/funnel/FunnelChart.vue +197 -0
- package/custom/widgets/chart/histogram/HistogramChart.vue +21 -0
- package/custom/widgets/chart/line/LineChart.vue +175 -0
- package/custom/widgets/chart/pie/PieChart.vue +161 -0
- package/custom/widgets/chart/stacked-bar/StackedBarChart.vue +256 -0
- package/custom/widgets/gauge-card/GaugeCardWidget.vue +107 -0
- package/custom/widgets/kpi-card/KpiCardWidget.vue +73 -0
- package/custom/widgets/pivot-table/PivotTableWidget.vue +122 -0
- package/custom/widgets/registry.ts +51 -0
- package/custom/widgets/table/TableWidget.vue +110 -0
- package/dist/custom/api/dashboardApi.d.ts +32 -0
- package/dist/custom/api/dashboardApi.js +179 -0
- package/dist/custom/api/dashboardApi.ts +213 -0
- package/dist/custom/composables/useElementSize.d.ts +8 -0
- package/dist/custom/composables/useElementSize.js +30 -0
- package/dist/custom/composables/useElementSize.ts +41 -0
- package/dist/custom/model/dashboard.types.d.ts +45 -0
- package/dist/custom/model/dashboard.types.js +14 -0
- package/dist/custom/model/dashboard.types.ts +73 -0
- package/dist/custom/package.json +9 -0
- package/dist/custom/pnpm-lock.yaml +24 -0
- package/dist/custom/queries/useDashboardConfig.d.ts +112 -0
- package/dist/custom/queries/useDashboardConfig.js +57 -0
- package/dist/custom/queries/useDashboardConfig.ts +51 -0
- package/dist/custom/queries/useWidgetData.d.ts +90 -0
- package/dist/custom/queries/useWidgetData.js +57 -0
- package/dist/custom/queries/useWidgetData.ts +51 -0
- package/dist/custom/runtime/DashboardGroup.vue +185 -0
- package/dist/custom/runtime/DashboardPage.vue +122 -0
- package/dist/custom/runtime/DashboardRuntime.vue +435 -0
- package/dist/custom/runtime/WidgetRenderer.vue +60 -0
- package/dist/custom/runtime/WidgetShell.vue +152 -0
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +125 -0
- package/dist/custom/widgets/chart/ChartWidget.vue +188 -0
- package/dist/custom/widgets/chart/bar/BarChart.vue +167 -0
- package/dist/custom/widgets/chart/chart.types.d.ts +25 -0
- package/dist/custom/widgets/chart/chart.types.js +2 -0
- package/dist/custom/widgets/chart/chart.types.ts +34 -0
- package/dist/custom/widgets/chart/chart.utils.d.ts +5 -0
- package/dist/custom/widgets/chart/chart.utils.js +52 -0
- package/dist/custom/widgets/chart/chart.utils.ts +54 -0
- package/dist/custom/widgets/chart/funnel/FunnelChart.vue +197 -0
- package/dist/custom/widgets/chart/histogram/HistogramChart.vue +21 -0
- package/dist/custom/widgets/chart/line/LineChart.vue +175 -0
- package/dist/custom/widgets/chart/pie/PieChart.vue +161 -0
- package/dist/custom/widgets/chart/stacked-bar/StackedBarChart.vue +256 -0
- package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +107 -0
- package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +73 -0
- package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +122 -0
- package/dist/custom/widgets/registry.d.ts +11 -0
- package/dist/custom/widgets/registry.js +47 -0
- package/dist/custom/widgets/registry.ts +51 -0
- package/dist/custom/widgets/table/TableWidget.vue +110 -0
- package/dist/endpoint/dashboard.d.ts +7 -0
- package/dist/endpoint/dashboard.js +29 -0
- package/dist/endpoint/groups.d.ts +30 -0
- package/dist/endpoint/groups.js +131 -0
- package/dist/endpoint/widgets.d.ts +15 -0
- package/dist/endpoint/widgets.js +182 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +124 -0
- package/dist/schema/api.d.ts +1205 -0
- package/dist/schema/api.js +84 -0
- package/dist/schema/widget.d.ts +514 -0
- package/dist/schema/widget.js +133 -0
- package/dist/services/dashboardConfigService.d.ts +35 -0
- package/dist/services/dashboardConfigService.js +79 -0
- package/dist/services/widgetConfigValidator.d.ts +8 -0
- package/dist/services/widgetConfigValidator.js +65 -0
- package/dist/services/widgetDataService.d.ts +20 -0
- package/dist/services/widgetDataService.js +32 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.js +1 -0
- package/endpoint/dashboard.ts +32 -0
- package/endpoint/groups.ts +213 -0
- package/endpoint/widgets.ts +255 -0
- package/index.ts +141 -0
- package/package.json +64 -0
- package/schema/api.ts +99 -0
- package/schema/widget.ts +159 -0
- package/services/dashboardConfigService.ts +136 -0
- package/services/widgetConfigValidator.ts +93 -0
- package/services/widgetDataService.ts +57 -0
- package/shims-vue.d.ts +5 -0
- package/tsconfig.json +18 -0
- 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>
|