@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.
- package/.turbo/turbo-build.log +40 -7
- package/CHANGELOG.md +9 -0
- package/dist/index.js +3893 -2669
- package/dist/index.umd.cjs +5 -5
- package/dist/src/DashboardConfigPanel.d.ts +28 -0
- package/dist/src/DashboardConfigPanel.d.ts.map +1 -0
- package/dist/src/DashboardConfigPanel.stories.d.ts +14 -0
- package/dist/src/DashboardConfigPanel.stories.d.ts.map +1 -0
- package/dist/src/DashboardGridLayout.d.ts.map +1 -1
- package/dist/src/DashboardRenderer.d.ts +14 -0
- package/dist/src/DashboardRenderer.d.ts.map +1 -1
- package/dist/src/DashboardWithConfig.d.ts +32 -0
- package/dist/src/DashboardWithConfig.d.ts.map +1 -0
- package/dist/src/MetricCard.d.ts +8 -2
- package/dist/src/MetricCard.d.ts.map +1 -1
- package/dist/src/MetricWidget.d.ts +12 -3
- package/dist/src/MetricWidget.d.ts.map +1 -1
- package/dist/src/ObjectDataTable.d.ts +39 -0
- package/dist/src/ObjectDataTable.d.ts.map +1 -0
- package/dist/src/ObjectPivotTable.d.ts +29 -0
- package/dist/src/ObjectPivotTable.d.ts.map +1 -0
- package/dist/src/PivotTable.d.ts +14 -0
- package/dist/src/PivotTable.d.ts.map +1 -0
- package/dist/src/WidgetConfigPanel.d.ts +43 -0
- package/dist/src/WidgetConfigPanel.d.ts.map +1 -0
- package/dist/src/index.d.ts +13 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/utils.d.ts +14 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/package.json +7 -7
- package/src/DashboardConfigPanel.stories.tsx +164 -0
- package/src/DashboardConfigPanel.tsx +158 -0
- package/src/DashboardGridLayout.tsx +113 -10
- package/src/DashboardRenderer.tsx +283 -37
- package/src/DashboardWithConfig.tsx +211 -0
- package/src/MetricCard.tsx +11 -4
- package/src/MetricWidget.tsx +18 -11
- package/src/ObjectDataTable.tsx +191 -0
- package/src/ObjectPivotTable.tsx +160 -0
- package/src/PivotTable.tsx +262 -0
- package/src/WidgetConfigPanel.tsx +540 -0
- package/src/__tests__/DashboardConfigPanel.test.tsx +206 -0
- package/src/__tests__/DashboardRenderer.designMode.test.tsx +386 -0
- package/src/__tests__/DashboardRenderer.header.test.tsx +114 -0
- package/src/__tests__/DashboardRenderer.mobile.test.tsx +214 -0
- package/src/__tests__/DashboardRenderer.widgetData.test.tsx +1283 -0
- package/src/__tests__/DashboardWithConfig.test.tsx +276 -0
- package/src/__tests__/MetricCard.test.tsx +23 -0
- package/src/__tests__/ObjectDataTable.test.tsx +122 -0
- package/src/__tests__/ObjectPivotTable.test.tsx +192 -0
- package/src/__tests__/PivotTable.test.tsx +162 -0
- package/src/__tests__/WidgetConfigPanel.test.tsx +492 -0
- package/src/__tests__/ensureWidgetIds.test.tsx +103 -0
- package/src/index.tsx +107 -1
- 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 =
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
const
|
|
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
|
-
...
|
|
151
|
-
data:
|
|
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
|
-
|
|
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 =
|
|
322
|
+
const isSelfContained = widget.type === 'metric';
|
|
220
323
|
|
|
221
324
|
return (
|
|
222
325
|
<div key={widgetId} className="h-full">
|