@object-ui/plugin-dashboard 3.0.2 → 3.1.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 (55) hide show
  1. package/.turbo/turbo-build.log +40 -7
  2. package/CHANGELOG.md +9 -0
  3. package/dist/index.js +3893 -2669
  4. package/dist/index.umd.cjs +5 -5
  5. package/dist/src/DashboardConfigPanel.d.ts +28 -0
  6. package/dist/src/DashboardConfigPanel.d.ts.map +1 -0
  7. package/dist/src/DashboardConfigPanel.stories.d.ts +14 -0
  8. package/dist/src/DashboardConfigPanel.stories.d.ts.map +1 -0
  9. package/dist/src/DashboardGridLayout.d.ts.map +1 -1
  10. package/dist/src/DashboardRenderer.d.ts +14 -0
  11. package/dist/src/DashboardRenderer.d.ts.map +1 -1
  12. package/dist/src/DashboardWithConfig.d.ts +32 -0
  13. package/dist/src/DashboardWithConfig.d.ts.map +1 -0
  14. package/dist/src/MetricCard.d.ts +8 -2
  15. package/dist/src/MetricCard.d.ts.map +1 -1
  16. package/dist/src/MetricWidget.d.ts +12 -3
  17. package/dist/src/MetricWidget.d.ts.map +1 -1
  18. package/dist/src/ObjectDataTable.d.ts +39 -0
  19. package/dist/src/ObjectDataTable.d.ts.map +1 -0
  20. package/dist/src/ObjectPivotTable.d.ts +29 -0
  21. package/dist/src/ObjectPivotTable.d.ts.map +1 -0
  22. package/dist/src/PivotTable.d.ts +14 -0
  23. package/dist/src/PivotTable.d.ts.map +1 -0
  24. package/dist/src/WidgetConfigPanel.d.ts +43 -0
  25. package/dist/src/WidgetConfigPanel.d.ts.map +1 -0
  26. package/dist/src/index.d.ts +13 -1
  27. package/dist/src/index.d.ts.map +1 -1
  28. package/dist/src/utils.d.ts +14 -0
  29. package/dist/src/utils.d.ts.map +1 -0
  30. package/package.json +7 -7
  31. package/src/DashboardConfigPanel.stories.tsx +164 -0
  32. package/src/DashboardConfigPanel.tsx +158 -0
  33. package/src/DashboardGridLayout.tsx +113 -10
  34. package/src/DashboardRenderer.tsx +283 -37
  35. package/src/DashboardWithConfig.tsx +211 -0
  36. package/src/MetricCard.tsx +11 -4
  37. package/src/MetricWidget.tsx +18 -11
  38. package/src/ObjectDataTable.tsx +191 -0
  39. package/src/ObjectPivotTable.tsx +160 -0
  40. package/src/PivotTable.tsx +262 -0
  41. package/src/WidgetConfigPanel.tsx +540 -0
  42. package/src/__tests__/DashboardConfigPanel.test.tsx +206 -0
  43. package/src/__tests__/DashboardRenderer.designMode.test.tsx +386 -0
  44. package/src/__tests__/DashboardRenderer.header.test.tsx +114 -0
  45. package/src/__tests__/DashboardRenderer.mobile.test.tsx +214 -0
  46. package/src/__tests__/DashboardRenderer.widgetData.test.tsx +1283 -0
  47. package/src/__tests__/DashboardWithConfig.test.tsx +276 -0
  48. package/src/__tests__/MetricCard.test.tsx +23 -0
  49. package/src/__tests__/ObjectDataTable.test.tsx +122 -0
  50. package/src/__tests__/ObjectPivotTable.test.tsx +192 -0
  51. package/src/__tests__/PivotTable.test.tsx +162 -0
  52. package/src/__tests__/WidgetConfigPanel.test.tsx +492 -0
  53. package/src/__tests__/ensureWidgetIds.test.tsx +103 -0
  54. package/src/index.tsx +107 -1
  55. package/src/utils.ts +17 -0
@@ -0,0 +1,158 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ import * as React from 'react';
10
+ import {
11
+ ConfigPanelRenderer,
12
+ useConfigDraft,
13
+ } from '@object-ui/components';
14
+ import type { ConfigPanelSchema } from '@object-ui/components';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Schema — describes the full DashboardConfigPanel structure
18
+ // ---------------------------------------------------------------------------
19
+
20
+ const dashboardSchema: ConfigPanelSchema = {
21
+ breadcrumb: ['Dashboard', 'Configuration'],
22
+ sections: [
23
+ {
24
+ key: 'layout',
25
+ title: 'Layout',
26
+ fields: [
27
+ {
28
+ key: 'columns',
29
+ label: 'Columns',
30
+ type: 'slider',
31
+ defaultValue: 3,
32
+ min: 1,
33
+ max: 12,
34
+ step: 1,
35
+ },
36
+ {
37
+ key: 'gap',
38
+ label: 'Gap',
39
+ type: 'slider',
40
+ defaultValue: 4,
41
+ min: 0,
42
+ max: 16,
43
+ step: 1,
44
+ },
45
+ {
46
+ key: 'rowHeight',
47
+ label: 'Row height',
48
+ type: 'input',
49
+ defaultValue: '120',
50
+ placeholder: 'e.g. 120',
51
+ },
52
+ ],
53
+ },
54
+ {
55
+ key: 'data',
56
+ title: 'Data',
57
+ collapsible: true,
58
+ fields: [
59
+ {
60
+ key: 'refreshInterval',
61
+ label: 'Refresh interval',
62
+ type: 'select',
63
+ defaultValue: '0',
64
+ options: [
65
+ { value: '0', label: 'Manual' },
66
+ { value: '30', label: '30s' },
67
+ { value: '60', label: '1 min' },
68
+ { value: '300', label: '5 min' },
69
+ ],
70
+ },
71
+ ],
72
+ },
73
+ {
74
+ key: 'appearance',
75
+ title: 'Appearance',
76
+ collapsible: true,
77
+ defaultCollapsed: true,
78
+ fields: [
79
+ {
80
+ key: 'title',
81
+ label: 'Title',
82
+ type: 'input',
83
+ placeholder: 'Dashboard title',
84
+ },
85
+ {
86
+ key: 'showDescription',
87
+ label: 'Show description',
88
+ type: 'switch',
89
+ defaultValue: true,
90
+ },
91
+ {
92
+ key: 'theme',
93
+ label: 'Theme',
94
+ type: 'select',
95
+ defaultValue: 'auto',
96
+ options: [
97
+ { value: 'light', label: 'Light' },
98
+ { value: 'dark', label: 'Dark' },
99
+ { value: 'auto', label: 'Auto' },
100
+ ],
101
+ },
102
+ ],
103
+ },
104
+ ],
105
+ };
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Props
109
+ // ---------------------------------------------------------------------------
110
+
111
+ export interface DashboardConfigPanelProps {
112
+ /** Whether the panel is open */
113
+ open: boolean;
114
+ /** Close handler */
115
+ onClose: () => void;
116
+ /** Initial / committed dashboard configuration */
117
+ config: Record<string, any>;
118
+ /** Persist the updated config */
119
+ onSave: (config: Record<string, any>) => void;
120
+ /** Optional live-update callback */
121
+ onFieldChange?: (field: string, value: any) => void;
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // Component
126
+ // ---------------------------------------------------------------------------
127
+
128
+ /**
129
+ * DashboardConfigPanel — Schema-driven configuration panel for dashboards.
130
+ *
131
+ * Built entirely on the generic ConfigPanelRenderer + useConfigDraft,
132
+ * demonstrating that a full config panel can be expressed in ~60 lines
133
+ * of declarative schema rather than 1500+ lines of imperative code.
134
+ */
135
+ export function DashboardConfigPanel({
136
+ open,
137
+ onClose,
138
+ config,
139
+ onSave,
140
+ onFieldChange,
141
+ }: DashboardConfigPanelProps) {
142
+ const { draft, isDirty, updateField, discard } = useConfigDraft(config, {
143
+ onUpdate: onFieldChange,
144
+ });
145
+
146
+ return (
147
+ <ConfigPanelRenderer
148
+ open={open}
149
+ onClose={onClose}
150
+ schema={dashboardSchema}
151
+ draft={draft}
152
+ isDirty={isDirty}
153
+ onFieldChange={updateField}
154
+ onSave={() => onSave(draft)}
155
+ onDiscard={discard}
156
+ />
157
+ );
158
+ }
@@ -5,6 +5,7 @@ import { cn, Card, CardHeader, CardTitle, CardContent, Button } from '@object-ui
5
5
  import { Edit, GripVertical, Save, X, RefreshCw } from 'lucide-react';
6
6
  import { SchemaRenderer, useHasDndProvider, useDnd } from '@object-ui/react';
7
7
  import type { DashboardSchema, DashboardWidgetSchema } from '@object-ui/types';
8
+ import { isObjectProvider } from './utils';
8
9
 
9
10
  /** Bridges editMode transitions to the ObjectUI DnD system when a DndProvider is present. */
10
11
  function DndEditModeBridge({ editMode }: { editMode: boolean }) {
@@ -126,12 +127,59 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
126
127
  const getComponentSchema = React.useCallback((widget: DashboardWidgetSchema) => {
127
128
  if (widget.component) return widget.component;
128
129
 
129
- const widgetType = (widget as any).type;
130
- if (widgetType === 'bar' || widgetType === 'line' || widgetType === 'area' || widgetType === 'pie' || widgetType === 'donut') {
131
- const dataItems = Array.isArray((widget as any).data) ? (widget as any).data : (widget as any).data?.items || [];
132
- const options = (widget as any).options || {};
133
- const xAxisKey = options.xField || 'name';
134
- const yField = options.yField || 'value';
130
+ const widgetType = widget.type;
131
+ const options = (widget.options || {}) as Record<string, any>;
132
+ if (widgetType === 'bar' || widgetType === 'line' || widgetType === 'area' || widgetType === 'pie' || widgetType === 'donut' || widgetType === 'scatter') {
133
+ const widgetData = (widget as any).data || options.data;
134
+ // Widget-level fields (from config panel) override options-level fields
135
+ const xAxisKey = widget.categoryField || options.xField || 'name';
136
+ const yField = widget.valueField || options.yField || 'value';
137
+
138
+ // provider: 'object' — delegate to ObjectChart for async data loading
139
+ if (isObjectProvider(widgetData)) {
140
+ // Merge widget-level fields with data provider config.
141
+ // Widget-level fields take precedence so that config panel
142
+ // edits are immediately reflected in the live preview.
143
+ const providerAgg = widgetData.aggregate;
144
+ const effectiveAggregate = providerAgg ? {
145
+ field: widget.valueField || providerAgg.field,
146
+ function: widget.aggregate || providerAgg.function,
147
+ groupBy: widget.categoryField || providerAgg.groupBy,
148
+ } : undefined;
149
+ const effectiveYField = effectiveAggregate?.field || yField;
150
+ return {
151
+ type: 'object-chart',
152
+ chartType: widgetType,
153
+ objectName: widget.object || widgetData.object,
154
+ aggregate: effectiveAggregate,
155
+ xAxisKey: xAxisKey,
156
+ series: [{ dataKey: effectiveYField }],
157
+ colors: CHART_COLORS,
158
+ className: "h-full"
159
+ };
160
+ }
161
+
162
+ // No explicit data provider but widget has object binding
163
+ // (e.g. newly created widget via config panel) — build object-chart
164
+ if (!widgetData && widget.object) {
165
+ const aggregate = widget.aggregate ? {
166
+ field: widget.valueField || 'value',
167
+ function: widget.aggregate,
168
+ groupBy: widget.categoryField || 'name',
169
+ } : undefined;
170
+ return {
171
+ type: 'object-chart',
172
+ chartType: widgetType,
173
+ objectName: widget.object,
174
+ aggregate,
175
+ xAxisKey: xAxisKey,
176
+ series: [{ dataKey: widget.valueField || 'value' }],
177
+ colors: CHART_COLORS,
178
+ className: "h-full"
179
+ };
180
+ }
181
+
182
+ const dataItems = Array.isArray(widgetData) ? widgetData : widgetData?.items || [];
135
183
 
136
184
  return {
137
185
  type: 'chart',
@@ -145,17 +193,72 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
145
193
  }
146
194
 
147
195
  if (widgetType === 'table') {
196
+ const widgetData = (widget as any).data || options.data;
197
+
198
+ // provider: 'object' — pass through object config for async data loading
199
+ if (isObjectProvider(widgetData)) {
200
+ const { data: _data, ...restOptions } = options;
201
+ return {
202
+ type: 'data-table',
203
+ ...restOptions,
204
+ objectName: widget.object || widgetData.object,
205
+ dataProvider: widgetData,
206
+ data: [],
207
+ searchable: false,
208
+ pagination: false,
209
+ className: "border-0"
210
+ };
211
+ }
212
+
213
+ // No explicit data provider but widget has object binding
214
+ if (!widgetData && widget.object) {
215
+ return {
216
+ type: 'data-table',
217
+ ...options,
218
+ objectName: widget.object,
219
+ data: [],
220
+ searchable: false,
221
+ pagination: false,
222
+ className: "border-0"
223
+ };
224
+ }
225
+
148
226
  return {
149
227
  type: 'data-table',
150
- ...(widget as any).options,
151
- data: (widget as any).data?.items || [],
228
+ ...options,
229
+ data: widgetData?.items || [],
152
230
  searchable: false,
153
231
  pagination: false,
154
232
  className: "border-0"
155
233
  };
156
234
  }
157
235
 
158
- return widget;
236
+ if (widgetType === 'pivot') {
237
+ const widgetData = (widget as any).data || options.data;
238
+
239
+ // provider: 'object' — pass through object config for async data loading
240
+ if (isObjectProvider(widgetData)) {
241
+ const { data: _data, ...restOptions } = options;
242
+ return {
243
+ type: 'pivot',
244
+ ...restOptions,
245
+ objectName: widget.object || widgetData.object,
246
+ dataProvider: widgetData,
247
+ data: [],
248
+ };
249
+ }
250
+
251
+ return {
252
+ type: 'pivot',
253
+ ...options,
254
+ data: Array.isArray(widgetData) ? widgetData : widgetData?.items || [],
255
+ };
256
+ }
257
+
258
+ return {
259
+ ...widget,
260
+ ...options
261
+ };
159
262
  }, []);
160
263
 
161
264
  return (
@@ -216,7 +319,7 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
216
319
  {schema.widgets?.map((widget, index) => {
217
320
  const widgetId = widget.id || `widget-${index}`;
218
321
  const componentSchema = getComponentSchema(widget);
219
- const isSelfContained = (widget as any).type === 'metric';
322
+ const isSelfContained = widget.type === 'metric';
220
323
 
221
324
  return (
222
325
  <div key={widgetId} className="h-full">