@object-ui/plugin-dashboard 3.0.2 → 3.0.3
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 +5 -5
- package/CHANGELOG.md +9 -0
- package/dist/index.js +1279 -1268
- package/dist/index.umd.cjs +5 -5
- package/dist/src/DashboardGridLayout.d.ts.map +1 -1
- package/dist/src/DashboardRenderer.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/DashboardGridLayout.tsx +12 -7
- package/src/DashboardRenderer.tsx +17 -12
- package/src/__tests__/DashboardRenderer.widgetData.test.tsx +261 -0
|
@@ -61,15 +61,17 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
|
|
|
61
61
|
};
|
|
62
62
|
}, [schema.refreshInterval, onRefresh, handleRefresh]);
|
|
63
63
|
|
|
64
|
-
const renderWidget = (widget: DashboardWidgetSchema) => {
|
|
64
|
+
const renderWidget = (widget: DashboardWidgetSchema, index: number) => {
|
|
65
65
|
const getComponentSchema = () => {
|
|
66
66
|
if (widget.component) return widget.component;
|
|
67
67
|
|
|
68
68
|
// Handle Shorthand Registry Mappings
|
|
69
|
-
const widgetType =
|
|
69
|
+
const widgetType = widget.type;
|
|
70
|
+
const options = (widget.options || {}) as Record<string, any>;
|
|
70
71
|
if (widgetType === 'bar' || widgetType === 'line' || widgetType === 'area' || widgetType === 'pie' || widgetType === 'donut') {
|
|
71
|
-
|
|
72
|
-
const
|
|
72
|
+
// Support data at widget level or nested inside options
|
|
73
|
+
const widgetData = (widget as any).data || options.data;
|
|
74
|
+
const dataItems = Array.isArray(widgetData) ? widgetData : widgetData?.items || [];
|
|
73
75
|
const xAxisKey = options.xField || 'name';
|
|
74
76
|
const yField = options.yField || 'value';
|
|
75
77
|
|
|
@@ -85,10 +87,12 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
|
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
if (widgetType === 'table') {
|
|
90
|
+
// Support data at widget level or nested inside options
|
|
91
|
+
const widgetData = (widget as any).data || options.data;
|
|
88
92
|
return {
|
|
89
93
|
type: 'data-table',
|
|
90
|
-
...
|
|
91
|
-
data:
|
|
94
|
+
...options,
|
|
95
|
+
data: widgetData?.items || [],
|
|
92
96
|
searchable: false,
|
|
93
97
|
pagination: false,
|
|
94
98
|
className: "border-0"
|
|
@@ -97,17 +101,18 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
|
|
|
97
101
|
|
|
98
102
|
return {
|
|
99
103
|
...widget,
|
|
100
|
-
...
|
|
104
|
+
...options
|
|
101
105
|
};
|
|
102
106
|
};
|
|
103
107
|
|
|
104
108
|
const componentSchema = getComponentSchema();
|
|
105
|
-
const isSelfContained =
|
|
109
|
+
const isSelfContained = widget.type === 'metric';
|
|
110
|
+
const widgetKey = widget.id || widget.title || `widget-${index}`;
|
|
106
111
|
|
|
107
112
|
if (isSelfContained) {
|
|
108
113
|
return (
|
|
109
114
|
<div
|
|
110
|
-
key={
|
|
115
|
+
key={widgetKey}
|
|
111
116
|
className={cn("h-full w-full", isMobile && "w-[85vw] shrink-0 snap-center")}
|
|
112
117
|
style={!isMobile && widget.layout ? {
|
|
113
118
|
gridColumn: `span ${widget.layout.w}`,
|
|
@@ -121,7 +126,7 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
|
|
|
121
126
|
|
|
122
127
|
return (
|
|
123
128
|
<Card
|
|
124
|
-
key={
|
|
129
|
+
key={widgetKey}
|
|
125
130
|
className={cn(
|
|
126
131
|
"overflow-hidden border-border/50 shadow-sm transition-all hover:shadow-md",
|
|
127
132
|
"bg-card/50 backdrop-blur-sm",
|
|
@@ -171,7 +176,7 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
|
|
|
171
176
|
className="flex overflow-x-auto snap-x snap-mandatory gap-3 pb-4 [-webkit-overflow-scrolling:touch]"
|
|
172
177
|
style={{ scrollPaddingLeft: '0.75rem' }}
|
|
173
178
|
>
|
|
174
|
-
{schema.widgets?.map((widget: DashboardWidgetSchema) => renderWidget(widget))}
|
|
179
|
+
{schema.widgets?.map((widget: DashboardWidgetSchema, index: number) => renderWidget(widget, index))}
|
|
175
180
|
</div>
|
|
176
181
|
</div>
|
|
177
182
|
);
|
|
@@ -192,7 +197,7 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
|
|
|
192
197
|
{...props}
|
|
193
198
|
>
|
|
194
199
|
{refreshButton}
|
|
195
|
-
{schema.widgets?.map((widget: DashboardWidgetSchema) => renderWidget(widget))}
|
|
200
|
+
{schema.widgets?.map((widget: DashboardWidgetSchema, index: number) => renderWidget(widget, index))}
|
|
196
201
|
</div>
|
|
197
202
|
);
|
|
198
203
|
}
|
|
@@ -0,0 +1,261 @@
|
|
|
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 { describe, it, expect } from 'vitest';
|
|
10
|
+
import { render } from '@testing-library/react';
|
|
11
|
+
import { DashboardRenderer } from '../DashboardRenderer';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract component schemas rendered by SchemaRenderer from the DOM.
|
|
15
|
+
* When a component type is not registered, SchemaRenderer renders
|
|
16
|
+
* an error block containing a JSON <pre> element with the schema.
|
|
17
|
+
* We parse those to verify the schema shape produced by DashboardRenderer.
|
|
18
|
+
*/
|
|
19
|
+
function getRenderedSchemas(container: HTMLElement): any[] {
|
|
20
|
+
const pres = container.querySelectorAll('pre');
|
|
21
|
+
return Array.from(pres).map(el => JSON.parse(el.textContent!));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('DashboardRenderer widget data extraction', () => {
|
|
25
|
+
it('should extract chart data from options.data.items', () => {
|
|
26
|
+
const schema = {
|
|
27
|
+
type: 'dashboard' as const,
|
|
28
|
+
name: 'test',
|
|
29
|
+
title: 'Test',
|
|
30
|
+
widgets: [
|
|
31
|
+
{
|
|
32
|
+
type: 'bar',
|
|
33
|
+
title: 'Test Bar',
|
|
34
|
+
layout: { x: 0, y: 0, w: 2, h: 2 },
|
|
35
|
+
options: {
|
|
36
|
+
xField: 'name',
|
|
37
|
+
yField: 'value',
|
|
38
|
+
data: {
|
|
39
|
+
provider: 'value',
|
|
40
|
+
items: [
|
|
41
|
+
{ name: 'A', value: 100 },
|
|
42
|
+
{ name: 'B', value: 200 },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
} as any;
|
|
49
|
+
|
|
50
|
+
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
51
|
+
const schemas = getRenderedSchemas(container);
|
|
52
|
+
const chartSchema = schemas.find(s => s.type === 'chart');
|
|
53
|
+
|
|
54
|
+
expect(chartSchema).toBeDefined();
|
|
55
|
+
expect(chartSchema.chartType).toBe('bar');
|
|
56
|
+
expect(chartSchema.data).toHaveLength(2);
|
|
57
|
+
expect(chartSchema.data[0]).toEqual({ name: 'A', value: 100 });
|
|
58
|
+
expect(chartSchema.xAxisKey).toBe('name');
|
|
59
|
+
expect(chartSchema.series).toEqual([{ dataKey: 'value' }]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should extract chart data from widget.data.items (backward compat)', () => {
|
|
63
|
+
const schema = {
|
|
64
|
+
type: 'dashboard' as const,
|
|
65
|
+
name: 'test',
|
|
66
|
+
title: 'Test',
|
|
67
|
+
widgets: [
|
|
68
|
+
{
|
|
69
|
+
type: 'area',
|
|
70
|
+
title: 'Test Area',
|
|
71
|
+
layout: { x: 0, y: 0, w: 3, h: 2 },
|
|
72
|
+
options: { xField: 'month', yField: 'revenue' },
|
|
73
|
+
data: {
|
|
74
|
+
provider: 'value',
|
|
75
|
+
items: [
|
|
76
|
+
{ month: 'Jan', revenue: 155000 },
|
|
77
|
+
{ month: 'Feb', revenue: 87000 },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
} as any;
|
|
83
|
+
|
|
84
|
+
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
85
|
+
const schemas = getRenderedSchemas(container);
|
|
86
|
+
const chartSchema = schemas.find(s => s.type === 'chart');
|
|
87
|
+
|
|
88
|
+
expect(chartSchema).toBeDefined();
|
|
89
|
+
expect(chartSchema.chartType).toBe('area');
|
|
90
|
+
expect(chartSchema.data).toHaveLength(2);
|
|
91
|
+
expect(chartSchema.data[0].month).toBe('Jan');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should extract table data from options.data.items', () => {
|
|
95
|
+
const schema = {
|
|
96
|
+
type: 'dashboard' as const,
|
|
97
|
+
name: 'test',
|
|
98
|
+
title: 'Test',
|
|
99
|
+
widgets: [
|
|
100
|
+
{
|
|
101
|
+
type: 'table',
|
|
102
|
+
title: 'Test Table',
|
|
103
|
+
layout: { x: 0, y: 0, w: 4, h: 2 },
|
|
104
|
+
options: {
|
|
105
|
+
columns: [
|
|
106
|
+
{ header: 'Name', accessorKey: 'name' },
|
|
107
|
+
{ header: 'Amount', accessorKey: 'amount' },
|
|
108
|
+
],
|
|
109
|
+
data: {
|
|
110
|
+
provider: 'value',
|
|
111
|
+
items: [
|
|
112
|
+
{ name: 'Item A', amount: '$100' },
|
|
113
|
+
{ name: 'Item B', amount: '$200' },
|
|
114
|
+
{ name: 'Item C', amount: '$300' },
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
} as any;
|
|
121
|
+
|
|
122
|
+
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
123
|
+
// data-table is a registered component that renders a real table,
|
|
124
|
+
// so we verify the data reaches it by checking for rendered cell content
|
|
125
|
+
expect(container.textContent).toContain('Item A');
|
|
126
|
+
expect(container.textContent).toContain('$200');
|
|
127
|
+
expect(container.textContent).toContain('Item C');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should handle donut chart data from options', () => {
|
|
131
|
+
const schema = {
|
|
132
|
+
type: 'dashboard' as const,
|
|
133
|
+
name: 'test',
|
|
134
|
+
title: 'Test',
|
|
135
|
+
widgets: [
|
|
136
|
+
{
|
|
137
|
+
type: 'donut',
|
|
138
|
+
title: 'Test Donut',
|
|
139
|
+
layout: { x: 0, y: 0, w: 1, h: 2 },
|
|
140
|
+
options: {
|
|
141
|
+
xField: 'source',
|
|
142
|
+
yField: 'value',
|
|
143
|
+
data: {
|
|
144
|
+
provider: 'value',
|
|
145
|
+
items: [
|
|
146
|
+
{ source: 'Web', value: 2 },
|
|
147
|
+
{ source: 'Referral', value: 1 },
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
} as any;
|
|
154
|
+
|
|
155
|
+
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
156
|
+
const schemas = getRenderedSchemas(container);
|
|
157
|
+
const chartSchema = schemas.find(s => s.type === 'chart');
|
|
158
|
+
|
|
159
|
+
expect(chartSchema).toBeDefined();
|
|
160
|
+
expect(chartSchema.chartType).toBe('donut');
|
|
161
|
+
expect(chartSchema.data).toHaveLength(2);
|
|
162
|
+
expect(chartSchema.xAxisKey).toBe('source');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should default to empty array when no data is provided', () => {
|
|
166
|
+
const schema = {
|
|
167
|
+
type: 'dashboard' as const,
|
|
168
|
+
name: 'test',
|
|
169
|
+
title: 'Test',
|
|
170
|
+
widgets: [
|
|
171
|
+
{
|
|
172
|
+
type: 'bar',
|
|
173
|
+
title: 'No Data Bar',
|
|
174
|
+
layout: { x: 0, y: 0, w: 2, h: 2 },
|
|
175
|
+
options: { xField: 'x', yField: 'y' },
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
} as any;
|
|
179
|
+
|
|
180
|
+
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
181
|
+
const schemas = getRenderedSchemas(container);
|
|
182
|
+
const chartSchema = schemas.find(s => s.type === 'chart');
|
|
183
|
+
|
|
184
|
+
expect(chartSchema).toBeDefined();
|
|
185
|
+
expect(chartSchema.data).toEqual([]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should render metric widgets using spec shorthand format', () => {
|
|
189
|
+
const schema = {
|
|
190
|
+
type: 'dashboard' as const,
|
|
191
|
+
name: 'test',
|
|
192
|
+
title: 'Test',
|
|
193
|
+
widgets: [
|
|
194
|
+
{
|
|
195
|
+
type: 'metric',
|
|
196
|
+
layout: { x: 0, y: 0, w: 1, h: 1 },
|
|
197
|
+
options: {
|
|
198
|
+
label: 'Total Revenue',
|
|
199
|
+
value: '$652,000',
|
|
200
|
+
trend: { value: 12.5, direction: 'up', label: 'vs last month' },
|
|
201
|
+
icon: 'DollarSign',
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
type: 'metric',
|
|
206
|
+
layout: { x: 1, y: 0, w: 1, h: 1 },
|
|
207
|
+
options: {
|
|
208
|
+
label: 'Active Deals',
|
|
209
|
+
value: '5',
|
|
210
|
+
trend: { value: 2.1, direction: 'down', label: 'vs last month' },
|
|
211
|
+
icon: 'Briefcase',
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
} as any;
|
|
216
|
+
|
|
217
|
+
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
218
|
+
|
|
219
|
+
// MetricWidget is registered in the ComponentRegistry, so it should render
|
|
220
|
+
// the label and value from the merged options
|
|
221
|
+
expect(container.textContent).toContain('Total Revenue');
|
|
222
|
+
expect(container.textContent).toContain('$652,000');
|
|
223
|
+
expect(container.textContent).toContain('Active Deals');
|
|
224
|
+
expect(container.textContent).toContain('5');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should assign unique keys to widgets without id or title', () => {
|
|
228
|
+
const schema = {
|
|
229
|
+
type: 'dashboard' as const,
|
|
230
|
+
name: 'test',
|
|
231
|
+
title: 'Test',
|
|
232
|
+
widgets: [
|
|
233
|
+
{
|
|
234
|
+
type: 'metric',
|
|
235
|
+
layout: { x: 0, y: 0, w: 1, h: 1 },
|
|
236
|
+
options: { label: 'Metric A', value: '100' },
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
type: 'metric',
|
|
240
|
+
layout: { x: 1, y: 0, w: 1, h: 1 },
|
|
241
|
+
options: { label: 'Metric B', value: '200' },
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
type: 'metric',
|
|
245
|
+
layout: { x: 2, y: 0, w: 1, h: 1 },
|
|
246
|
+
options: { label: 'Metric C', value: '300' },
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
} as any;
|
|
250
|
+
|
|
251
|
+
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
252
|
+
|
|
253
|
+
// All three metrics should render without React key warnings
|
|
254
|
+
expect(container.textContent).toContain('Metric A');
|
|
255
|
+
expect(container.textContent).toContain('Metric B');
|
|
256
|
+
expect(container.textContent).toContain('Metric C');
|
|
257
|
+
expect(container.textContent).toContain('100');
|
|
258
|
+
expect(container.textContent).toContain('200');
|
|
259
|
+
expect(container.textContent).toContain('300');
|
|
260
|
+
});
|
|
261
|
+
});
|