@adminforth/dashboard 1.5.0 → 1.6.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 (50) hide show
  1. package/custom/api/dashboardApi.ts +137 -1
  2. package/custom/model/dashboard.types.ts +32 -22
  3. package/custom/runtime/DashboardRuntime.vue +2 -3
  4. package/custom/skills/adminforth-dashboard/SKILL.md +66 -10
  5. package/custom/widgets/KpiCardWidget.vue +172 -9
  6. package/custom/widgets/chart/ChartWidget.vue +5 -5
  7. package/custom/widgets/registry.ts +4 -4
  8. package/dist/custom/api/dashboardApi.d.ts +46 -1
  9. package/dist/custom/api/dashboardApi.js +90 -0
  10. package/dist/custom/api/dashboardApi.ts +137 -1
  11. package/dist/custom/model/dashboard.types.d.ts +30 -14
  12. package/dist/custom/model/dashboard.types.js +2 -2
  13. package/dist/custom/model/dashboard.types.ts +32 -22
  14. package/dist/custom/queries/useDashboardConfig.d.ts +106 -104
  15. package/dist/custom/queries/useWidgetData.d.ts +106 -104
  16. package/dist/custom/runtime/DashboardRuntime.vue +2 -3
  17. package/dist/custom/skills/adminforth-dashboard/SKILL.md +66 -10
  18. package/dist/custom/widgets/KpiCardWidget.vue +172 -9
  19. package/dist/custom/widgets/chart/ChartWidget.vue +5 -5
  20. package/dist/custom/widgets/registry.js +4 -4
  21. package/dist/custom/widgets/registry.ts +4 -4
  22. package/dist/endpoint/widgets.js +99 -14
  23. package/dist/schema/api.d.ts +11426 -1634
  24. package/dist/schema/api.js +118 -21
  25. package/dist/schema/widget.d.ts +425 -1980
  26. package/dist/schema/widget.js +13 -374
  27. package/dist/schema/widgets/charts.d.ts +1689 -0
  28. package/dist/schema/widgets/charts.js +92 -0
  29. package/dist/schema/widgets/common.d.ts +275 -0
  30. package/dist/schema/widgets/common.js +171 -0
  31. package/dist/schema/widgets/gauge-card.d.ts +172 -0
  32. package/dist/schema/widgets/gauge-card.js +28 -0
  33. package/dist/schema/widgets/kpi-card.d.ts +212 -0
  34. package/dist/schema/widgets/kpi-card.js +43 -0
  35. package/dist/schema/widgets/pivot-table.d.ts +196 -0
  36. package/dist/schema/widgets/pivot-table.js +17 -0
  37. package/dist/schema/widgets/table.d.ts +130 -0
  38. package/dist/schema/widgets/table.js +12 -0
  39. package/dist/services/widgetDataService.js +96 -2
  40. package/endpoint/widgets.ts +173 -26
  41. package/package.json +1 -1
  42. package/schema/api.ts +148 -22
  43. package/schema/widget.ts +43 -425
  44. package/schema/widgets/charts.ts +113 -0
  45. package/schema/widgets/common.ts +194 -0
  46. package/schema/widgets/gauge-card.ts +34 -0
  47. package/schema/widgets/kpi-card.ts +49 -0
  48. package/schema/widgets/pivot-table.ts +24 -0
  49. package/schema/widgets/table.ts +18 -0
  50. package/services/widgetDataService.ts +129 -3
@@ -16,14 +16,68 @@
16
16
 
17
17
  <div
18
18
  v-else
19
- class="grid gap-1"
19
+ class="grid gap-3"
20
20
  >
21
- <div class="text-3xl font-bold text-lightNavbarText dark:text-darkNavbarText">
22
- {{ formattedValue }}
23
- </div>
24
- <div class="text-sm text-lightListTableText dark:text-darkListTableText">
25
- {{ label }}
21
+ <div class="grid gap-1">
22
+ <div class="text-3xl font-bold text-lightNavbarText dark:text-darkNavbarText">
23
+ {{ formattedValue }}
24
+ </div>
25
+ <div class="flex flex-wrap items-center gap-2 text-sm text-lightListTableText dark:text-darkListTableText">
26
+ <span>{{ label }}</span>
27
+ <span
28
+ v-if="comparison"
29
+ class="rounded px-1.5 py-0.5 text-xs font-medium"
30
+ :class="comparisonClass"
31
+ :title="comparison.tooltip"
32
+ >
33
+ {{ comparison.label }}
34
+ </span>
35
+ </div>
26
36
  </div>
37
+ <svg
38
+ v-if="sparklinePoints"
39
+ class="h-12 w-full overflow-visible"
40
+ viewBox="0 0 100 32"
41
+ preserveAspectRatio="none"
42
+ aria-hidden="true"
43
+ >
44
+ <defs v-if="usesSparklineGradient">
45
+ <linearGradient
46
+ :id="sparklineGradientId"
47
+ x1="0"
48
+ y1="0"
49
+ x2="0"
50
+ y2="1"
51
+ >
52
+ <stop
53
+ offset="0%"
54
+ stop-color="currentColor"
55
+ stop-opacity="0.24"
56
+ />
57
+ <stop
58
+ offset="100%"
59
+ stop-color="currentColor"
60
+ stop-opacity="0"
61
+ />
62
+ </linearGradient>
63
+ </defs>
64
+ <polygon
65
+ v-if="usesSparklineGradient"
66
+ class="text-lightPrimary dark:text-darkPrimary"
67
+ :points="sparklineFillPoints"
68
+ :fill="`url(#${sparklineGradientId})`"
69
+ />
70
+ <polyline
71
+ class="text-lightPrimary dark:text-darkPrimary"
72
+ :points="sparklinePoints"
73
+ fill="none"
74
+ stroke="currentColor"
75
+ stroke-width="2"
76
+ stroke-linecap="round"
77
+ stroke-linejoin="round"
78
+ vector-effect="non-scaling-stroke"
79
+ />
80
+ </svg>
27
81
  </div>
28
82
  </div>
29
83
  </template>
@@ -61,11 +115,120 @@ watch(
61
115
  const kpiConfig = computed(() => props.widget.target === 'kpi_card' ? props.widget.card : undefined)
62
116
  const widgetData = computed(() => data.value?.data as DashboardWidgetTableData | null)
63
117
  const columns = computed(() => widgetData.value?.columns ?? [])
64
- const firstRow = computed(() => widgetData.value?.rows[0] ?? {})
118
+ const firstRow = computed(() => widgetData.value?.values ?? widgetData.value?.rows[0] ?? {})
65
119
  const valueField = computed(() => kpiConfig.value?.value.field || columns.value[0])
66
120
  const value = computed(() => toFiniteNumber(firstRow.value[valueField.value]))
67
121
  const label = computed(() => kpiConfig.value?.subtitle?.field
68
- ? String(firstRow.value[kpiConfig.value.subtitle.field])
122
+ ? [kpiConfig.value.subtitle.text, formatValue(firstRow.value[kpiConfig.value.subtitle.field], kpiConfig.value.value.format)]
123
+ .filter(Boolean)
124
+ .join(': ')
69
125
  : kpiConfig.value?.subtitle?.text ?? kpiConfig.value?.title ?? props.widget.label)
70
- const formattedValue = computed(() => `${kpiConfig.value?.value.prefix ?? ''}${formatChartValue(value.value)}${kpiConfig.value?.value.suffix ?? ''}`)
126
+ const formattedValue = computed(() => `${kpiConfig.value?.value.prefix ?? ''}${formatValue(value.value, kpiConfig.value?.value.format)}${kpiConfig.value?.value.suffix ?? ''}`)
127
+ const comparisonValue = computed(() => toFiniteNumber(kpiConfig.value?.comparison?.field
128
+ ? firstRow.value[kpiConfig.value.comparison.field]
129
+ : undefined))
130
+ const comparison = computed(() => {
131
+ const config = kpiConfig.value?.comparison
132
+
133
+ if (!config) {
134
+ return null
135
+ }
136
+
137
+ const template = config.compact?.template ?? '{sign}{value}'
138
+ const tooltipTemplate = config.tooltip?.template
139
+ const valueText = formatValue(Math.abs(comparisonValue.value), config.format, { signed: false, compactTemplate: true })
140
+ const sign = comparisonValue.value > 0 ? '+' : comparisonValue.value < 0 ? '-' : ''
141
+
142
+ return {
143
+ value: comparisonValue.value,
144
+ label: config.compact?.show === false ? valueText : applyTemplate(template, sign, valueText),
145
+ tooltip: tooltipTemplate
146
+ ? applyTemplate(tooltipTemplate, sign, valueText)
147
+ : config.tooltip?.label,
148
+ positiveIsGood: config.positive_is_good ?? true,
149
+ }
150
+ })
151
+ const comparisonClass = computed(() => {
152
+ if (!comparison.value || comparison.value.value === 0) {
153
+ return 'bg-lightListBorder text-lightListTableText dark:bg-darkListBorder dark:text-darkListTableText'
154
+ }
155
+
156
+ const isGood = comparison.value.positiveIsGood
157
+ ? comparison.value.value > 0
158
+ : comparison.value.value < 0
159
+
160
+ return isGood
161
+ ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
162
+ : 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300'
163
+ })
164
+ const sparklineRows = computed(() => widgetData.value?.rows ?? [])
165
+ const sparklineConfig = computed(() => kpiConfig.value?.sparkline)
166
+ const sparklineGradientId = computed(() => `kpi-sparkline-${props.widget.id}`)
167
+ const usesSparklineGradient = computed(() => sparklineConfig.value?.fill?.type === 'gradient')
168
+ const sparklineCoordinates = computed(() => {
169
+ const field = sparklineConfig.value?.field
170
+
171
+ if (!field || sparklineRows.value.length < 2) {
172
+ return []
173
+ }
174
+
175
+ const values = sparklineRows.value.map((row) => toFiniteNumber(row[field]))
176
+ const min = Math.min(...values)
177
+ const max = Math.max(...values)
178
+ const range = max - min || 1
179
+
180
+ return values.map((item, index) => ({
181
+ x: (index / Math.max(values.length - 1, 1)) * 100,
182
+ y: 30 - ((item - min) / range) * 28,
183
+ }))
184
+ })
185
+ const sparklinePoints = computed(() => sparklineCoordinates.value.length
186
+ ? sparklineCoordinates.value.map((point) => `${point.x},${point.y}`).join(' ')
187
+ : '')
188
+ const sparklineFillPoints = computed(() => sparklineCoordinates.value.length
189
+ ? `0,32 ${sparklinePoints.value} 100,32`
190
+ : '')
191
+
192
+ function applyTemplate(template: string, sign: string, value: string) {
193
+ return template
194
+ .replaceAll('{sign}', sign)
195
+ .replaceAll('{value}', value)
196
+ }
197
+
198
+ function formatValue(
199
+ rawValue: unknown,
200
+ format = 'number',
201
+ options: { signed?: boolean, compactTemplate?: boolean } = {},
202
+ ) {
203
+ const numericValue = toFiniteNumber(rawValue)
204
+ const sign = options.signed && numericValue > 0 ? '+' : ''
205
+ const absoluteValue = options.signed ? Math.abs(numericValue) : numericValue
206
+
207
+ if (format === 'integer') {
208
+ return `${sign}${formatChartValue(absoluteValue, { maximumFractionDigits: 0 })}`
209
+ }
210
+
211
+ if (format === 'compact_number') {
212
+ return `${sign}${formatChartValue(absoluteValue, { notation: 'compact', maximumFractionDigits: 1 })}`
213
+ }
214
+
215
+ if (format === 'currency' || format === 'currency_delta') {
216
+ return `${sign}${formatChartValue(absoluteValue, {
217
+ style: 'currency',
218
+ currency: 'USD',
219
+ maximumFractionDigits: 2,
220
+ })}`
221
+ }
222
+
223
+ if (format === 'percent' || format === 'percent_delta') {
224
+ const value = formatChartValue(absoluteValue, { maximumFractionDigits: 1 })
225
+ return options.compactTemplate ? value : `${sign}${value}%`
226
+ }
227
+
228
+ if (format === 'number_delta') {
229
+ return `${sign}${formatChartValue(absoluteValue, { maximumFractionDigits: 2 })}`
230
+ }
231
+
232
+ return `${sign}${formatChartValue(absoluteValue, { maximumFractionDigits: 2 })}`
233
+ }
71
234
  </script>
@@ -83,11 +83,11 @@
83
83
  import { computed, watch } from 'vue'
84
84
  import { useWidgetData } from '../../queries/useWidgetData.js'
85
85
  import type { ChartDashboardWidgetConfig, DashboardWidgetTableData } from '../../model/dashboard.types.js'
86
- import BarChart from './bar/BarChart.vue'
87
- import FunnelChart from './funnel/FunnelChart.vue'
88
- import LineChart from './line/LineChart.vue'
89
- import PieChart from './pie/PieChart.vue'
90
- import StackedBarChart from './stacked-bar/StackedBarChart.vue'
86
+ import BarChart from './BarChart.vue'
87
+ import FunnelChart from './FunnelChart.vue'
88
+ import LineChart from './LineChart.vue'
89
+ import PieChart from './PieChart.vue'
90
+ import StackedBarChart from './StackedBarChart.vue'
91
91
  import { toFiniteNumber } from './chart.utils.js'
92
92
 
93
93
  const DEFAULT_WIDGET_HEIGHT = 500
@@ -1,8 +1,8 @@
1
1
  import ChartWidget from './chart/ChartWidget.vue';
2
- import GaugeCardWidget from './gauge-card/GaugeCardWidget.vue';
3
- import KpiCardWidget from './kpi-card/KpiCardWidget.vue';
4
- import PivotTableWidget from './pivot-table/PivotTableWidget.vue';
5
- import TableWidget from './table/TableWidget.vue';
2
+ import GaugeCardWidget from './GaugeCardWidget.vue';
3
+ import KpiCardWidget from './KpiCardWidget.vue';
4
+ import PivotTableWidget from './PivotTableWidget.vue';
5
+ import TableWidget from './TableWidget.vue';
6
6
  export const widgetRegistry = [
7
7
  {
8
8
  type: 'table',
@@ -1,10 +1,10 @@
1
1
  import type { Component } from 'vue'
2
2
  import type { DashboardWidgetTarget } from '../model/dashboard.types.js'
3
3
  import ChartWidget from './chart/ChartWidget.vue'
4
- import GaugeCardWidget from './gauge-card/GaugeCardWidget.vue'
5
- import KpiCardWidget from './kpi-card/KpiCardWidget.vue'
6
- import PivotTableWidget from './pivot-table/PivotTableWidget.vue'
7
- import TableWidget from './table/TableWidget.vue'
4
+ import GaugeCardWidget from './GaugeCardWidget.vue'
5
+ import KpiCardWidget from './KpiCardWidget.vue'
6
+ import PivotTableWidget from './PivotTableWidget.vue'
7
+ import TableWidget from './TableWidget.vue'
8
8
 
9
9
  export type DashboardWidgetType = DashboardWidgetTarget
10
10
 
@@ -8,7 +8,53 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { randomUUID } from 'crypto';
11
- import { DashboardApiResponseSchema, DashboardWidgetDataResponseSchema, GroupIdRequestSchema, MoveWidgetRequestSchema, SetWidgetConfigRequestSchema, WidgetDataRequestSchema, WidgetIdRequestSchema, } from '../schema/api.js';
11
+ import { ConfigureBarChartWidgetRequestSchema, ConfigureFunnelChartWidgetRequestSchema, ConfigureGaugeCardWidgetRequestSchema, ConfigureHistogramChartWidgetRequestSchema, ConfigureKpiCardWidgetRequestSchema, ConfigureLineChartWidgetRequestSchema, ConfigurePieChartWidgetRequestSchema, ConfigurePivotTableWidgetRequestSchema, ConfigureStackedBarChartWidgetRequestSchema, ConfigureTableWidgetRequestSchema, DashboardApiResponseSchema, DashboardWidgetDataResponseSchema, GroupIdRequestSchema, MoveWidgetRequestSchema, SetWidgetConfigRequestSchema, WidgetDataRequestSchema, WidgetIdRequestSchema, } from '../schema/api.js';
12
+ function replaceWidgetConfig(ctx, slug, widgetId, widgetConfig) {
13
+ return __awaiter(this, void 0, void 0, function* () {
14
+ let mutationError = null;
15
+ const updatedDashboard = yield ctx.updateDashboardConfig(slug, (config) => {
16
+ const widget = config.widgets.find((item) => item.id === widgetId);
17
+ if (!widget) {
18
+ mutationError = 'Dashboard widget not found';
19
+ return null;
20
+ }
21
+ const nextWidget = Object.assign(Object.assign({}, widgetConfig), { id: widget.id, group_id: widget.group_id, order: widget.order });
22
+ return Object.assign(Object.assign({}, config), { widgets: config.widgets.map((item) => item.id === widgetId
23
+ ? nextWidget
24
+ : item) });
25
+ });
26
+ return {
27
+ updatedDashboard,
28
+ mutationError,
29
+ };
30
+ });
31
+ }
32
+ function registerConfigureWidgetEndpoint(server, ctx, options) {
33
+ server.endpoint({
34
+ method: 'POST',
35
+ path: options.path,
36
+ description: options.description,
37
+ request_schema: options.requestSchema,
38
+ response_schema: DashboardApiResponseSchema,
39
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
40
+ if (!ctx.canEditDashboard(adminUser)) {
41
+ response.setStatus(403);
42
+ return { error: 'Dashboard edit is not allowed' };
43
+ }
44
+ const request = body;
45
+ const { updatedDashboard, mutationError } = yield replaceWidgetConfig(ctx, request.slug, request.widgetId, request.config);
46
+ if (!updatedDashboard) {
47
+ response.setStatus(404);
48
+ return { error: 'Dashboard not found' };
49
+ }
50
+ if (mutationError) {
51
+ response.setStatus(404);
52
+ return { error: mutationError };
53
+ }
54
+ return updatedDashboard;
55
+ }),
56
+ });
57
+ }
12
58
  export function registerWidgetEndpoints(server, ctx) {
13
59
  server.endpoint({
14
60
  method: 'POST',
@@ -138,19 +184,8 @@ export function registerWidgetEndpoints(server, ctx) {
138
184
  response.setStatus(403);
139
185
  return { error: 'Dashboard edit is not allowed' };
140
186
  }
141
- let mutationError = null;
142
- const updatedDashboard = yield ctx.updateDashboardConfig(body.slug, (config) => {
143
- const widget = config.widgets.find((item) => item.id === body.widgetId);
144
- if (!widget) {
145
- mutationError = 'Dashboard widget not found';
146
- return null;
147
- }
148
- const typedWidgetConfig = body.config;
149
- const nextWidget = Object.assign(Object.assign({}, typedWidgetConfig), { id: widget.id, group_id: widget.group_id, order: widget.order });
150
- return Object.assign(Object.assign({}, config), { widgets: config.widgets.map((item) => item.id === body.widgetId
151
- ? nextWidget
152
- : item) });
153
- });
187
+ const request = body;
188
+ const { updatedDashboard, mutationError } = yield replaceWidgetConfig(ctx, request.slug, request.widgetId, request.config);
154
189
  if (!updatedDashboard) {
155
190
  response.setStatus(404);
156
191
  return { error: 'Dashboard not found' };
@@ -162,6 +197,56 @@ export function registerWidgetEndpoints(server, ctx) {
162
197
  return updatedDashboard;
163
198
  }),
164
199
  });
200
+ registerConfigureWidgetEndpoint(server, ctx, {
201
+ path: '/dashboard/configure_table_widget',
202
+ description: 'Configures an existing dashboard widget as a table. Superadmin only.',
203
+ requestSchema: ConfigureTableWidgetRequestSchema,
204
+ });
205
+ registerConfigureWidgetEndpoint(server, ctx, {
206
+ path: '/dashboard/configure_kpi_card_widget',
207
+ description: 'Configures an existing dashboard widget as a KPI card. Superadmin only.',
208
+ requestSchema: ConfigureKpiCardWidgetRequestSchema,
209
+ });
210
+ registerConfigureWidgetEndpoint(server, ctx, {
211
+ path: '/dashboard/configure_gauge_card_widget',
212
+ description: 'Configures an existing dashboard widget as a gauge card. Superadmin only.',
213
+ requestSchema: ConfigureGaugeCardWidgetRequestSchema,
214
+ });
215
+ registerConfigureWidgetEndpoint(server, ctx, {
216
+ path: '/dashboard/configure_pivot_table_widget',
217
+ description: 'Configures an existing dashboard widget as a pivot table. Superadmin only.',
218
+ requestSchema: ConfigurePivotTableWidgetRequestSchema,
219
+ });
220
+ registerConfigureWidgetEndpoint(server, ctx, {
221
+ path: '/dashboard/configure_line_chart_widget',
222
+ description: 'Configures an existing dashboard widget as a line chart. Superadmin only.',
223
+ requestSchema: ConfigureLineChartWidgetRequestSchema,
224
+ });
225
+ registerConfigureWidgetEndpoint(server, ctx, {
226
+ path: '/dashboard/configure_bar_chart_widget',
227
+ description: 'Configures an existing dashboard widget as a bar chart. Superadmin only.',
228
+ requestSchema: ConfigureBarChartWidgetRequestSchema,
229
+ });
230
+ registerConfigureWidgetEndpoint(server, ctx, {
231
+ path: '/dashboard/configure_stacked_bar_chart_widget',
232
+ description: 'Configures an existing dashboard widget as a stacked bar chart. Superadmin only.',
233
+ requestSchema: ConfigureStackedBarChartWidgetRequestSchema,
234
+ });
235
+ registerConfigureWidgetEndpoint(server, ctx, {
236
+ path: '/dashboard/configure_pie_chart_widget',
237
+ description: 'Configures an existing dashboard widget as a pie chart. Superadmin only.',
238
+ requestSchema: ConfigurePieChartWidgetRequestSchema,
239
+ });
240
+ registerConfigureWidgetEndpoint(server, ctx, {
241
+ path: '/dashboard/configure_histogram_chart_widget',
242
+ description: 'Configures an existing dashboard widget as a histogram chart. Superadmin only.',
243
+ requestSchema: ConfigureHistogramChartWidgetRequestSchema,
244
+ });
245
+ registerConfigureWidgetEndpoint(server, ctx, {
246
+ path: '/dashboard/configure_funnel_chart_widget',
247
+ description: 'Configures an existing dashboard widget as a funnel chart. Superadmin only.',
248
+ requestSchema: ConfigureFunnelChartWidgetRequestSchema,
249
+ });
165
250
  server.endpoint({
166
251
  method: 'POST',
167
252
  path: '/dashboard/get_dashboard_widget_data',