@object-ui/plugin-dashboard 3.3.0 → 3.3.1
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/CHANGELOG.md +10 -0
- package/README.md +21 -1
- package/dist/index.js +869 -787
- package/dist/index.umd.cjs +4 -4
- package/dist/packages/plugin-dashboard/src/DashboardRenderer.d.ts +5 -0
- package/dist/packages/plugin-dashboard/src/DashboardRenderer.d.ts.map +1 -1
- package/dist/packages/plugin-dashboard/src/MetricWidget.d.ts +4 -1
- package/dist/packages/plugin-dashboard/src/MetricWidget.d.ts.map +1 -1
- package/dist/packages/plugin-dashboard/src/ObjectMetricWidget.d.ts +2 -0
- package/dist/packages/plugin-dashboard/src/ObjectMetricWidget.d.ts.map +1 -1
- package/dist/packages/plugin-dashboard/src/index.d.ts +1 -1
- package/package.json +40 -7
- package/.turbo/turbo-build.log +0 -41
- package/src/DashboardConfigPanel.stories.tsx +0 -164
- package/src/DashboardConfigPanel.tsx +0 -158
- package/src/DashboardGridLayout.tsx +0 -367
- package/src/DashboardRenderer.stories.tsx +0 -173
- package/src/DashboardRenderer.tsx +0 -479
- package/src/DashboardWithConfig.tsx +0 -211
- package/src/MetricCard.tsx +0 -102
- package/src/MetricWidget.tsx +0 -96
- package/src/ObjectDataTable.tsx +0 -226
- package/src/ObjectMetricWidget.tsx +0 -159
- package/src/ObjectPivotTable.tsx +0 -160
- package/src/PivotTable.tsx +0 -262
- package/src/WidgetConfigPanel.tsx +0 -540
- package/src/__tests__/DashboardConfigPanel.test.tsx +0 -206
- package/src/__tests__/DashboardGridLayout.test.tsx +0 -199
- package/src/__tests__/DashboardRenderer.autoRefresh.test.tsx +0 -124
- package/src/__tests__/DashboardRenderer.designMode.test.tsx +0 -386
- package/src/__tests__/DashboardRenderer.header.test.tsx +0 -114
- package/src/__tests__/DashboardRenderer.mobile.test.tsx +0 -214
- package/src/__tests__/DashboardRenderer.widgetData.test.tsx +0 -1411
- package/src/__tests__/DashboardWithConfig.test.tsx +0 -276
- package/src/__tests__/MetricCard.test.tsx +0 -107
- package/src/__tests__/ObjectDataTable.test.tsx +0 -211
- package/src/__tests__/ObjectMetricWidget.test.tsx +0 -196
- package/src/__tests__/ObjectPivotTable.test.tsx +0 -192
- package/src/__tests__/PivotTable.test.tsx +0 -162
- package/src/__tests__/WidgetConfigPanel.test.tsx +0 -492
- package/src/__tests__/ensureWidgetIds.test.tsx +0 -103
- package/src/index.tsx +0 -236
- package/src/utils.ts +0 -17
- package/tsconfig.json +0 -19
- package/vite.config.ts +0 -64
- package/vitest.config.ts +0 -9
- package/vitest.setup.tsx +0 -18
|
@@ -1,367 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import { ResponsiveGridLayout, useContainerWidth, type LayoutItem as RGLLayout, type Layout, type ResponsiveLayouts } from 'react-grid-layout';
|
|
3
|
-
import 'react-grid-layout/css/styles.css';
|
|
4
|
-
import { cn, Card, CardHeader, CardTitle, CardContent, Button } from '@object-ui/components';
|
|
5
|
-
import { Edit, GripVertical, Save, X, RefreshCw } from 'lucide-react';
|
|
6
|
-
import { SchemaRenderer, useHasDndProvider, useDnd } from '@object-ui/react';
|
|
7
|
-
import type { DashboardSchema, DashboardWidgetSchema } from '@object-ui/types';
|
|
8
|
-
import { isObjectProvider } from './utils';
|
|
9
|
-
|
|
10
|
-
/** Bridges editMode transitions to the ObjectUI DnD system when a DndProvider is present. */
|
|
11
|
-
function DndEditModeBridge({ editMode }: { editMode: boolean }) {
|
|
12
|
-
const dnd = useDnd();
|
|
13
|
-
|
|
14
|
-
React.useEffect(() => {
|
|
15
|
-
if (editMode) {
|
|
16
|
-
dnd.startDrag({ id: 'dashboard-layout', type: 'dashboard-widget', data: {} });
|
|
17
|
-
return () => { dnd.endDrag(); };
|
|
18
|
-
} else {
|
|
19
|
-
dnd.endDrag('dashboard');
|
|
20
|
-
}
|
|
21
|
-
}, [editMode, dnd]);
|
|
22
|
-
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const CHART_COLORS = [
|
|
27
|
-
'hsl(var(--chart-1))',
|
|
28
|
-
'hsl(var(--chart-2))',
|
|
29
|
-
'hsl(var(--chart-3))',
|
|
30
|
-
'hsl(var(--chart-4))',
|
|
31
|
-
'hsl(var(--chart-5))',
|
|
32
|
-
];
|
|
33
|
-
|
|
34
|
-
export interface DashboardGridLayoutProps {
|
|
35
|
-
schema: DashboardSchema;
|
|
36
|
-
className?: string;
|
|
37
|
-
onLayoutChange?: (layout: RGLLayout[]) => void;
|
|
38
|
-
persistLayoutKey?: string;
|
|
39
|
-
/** Callback invoked when dashboard refresh is triggered (manual or auto) */
|
|
40
|
-
onRefresh?: () => void;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
|
|
44
|
-
schema,
|
|
45
|
-
className,
|
|
46
|
-
onLayoutChange,
|
|
47
|
-
persistLayoutKey = 'dashboard-layout',
|
|
48
|
-
onRefresh,
|
|
49
|
-
}) => {
|
|
50
|
-
const { width, containerRef, mounted } = useContainerWidth();
|
|
51
|
-
const [editMode, setEditMode] = React.useState(false);
|
|
52
|
-
const [refreshing, setRefreshing] = React.useState(false);
|
|
53
|
-
const hasDndProvider = useHasDndProvider();
|
|
54
|
-
const intervalRef = React.useRef<ReturnType<typeof setInterval> | null>(null);
|
|
55
|
-
|
|
56
|
-
const handleRefresh = React.useCallback(() => {
|
|
57
|
-
if (!onRefresh) return;
|
|
58
|
-
setRefreshing(true);
|
|
59
|
-
onRefresh();
|
|
60
|
-
setTimeout(() => setRefreshing(false), 600);
|
|
61
|
-
}, [onRefresh]);
|
|
62
|
-
|
|
63
|
-
// Auto-refresh interval
|
|
64
|
-
React.useEffect(() => {
|
|
65
|
-
if (!schema.refreshInterval || schema.refreshInterval <= 0 || !onRefresh) return;
|
|
66
|
-
intervalRef.current = setInterval(handleRefresh, schema.refreshInterval * 1000);
|
|
67
|
-
return () => {
|
|
68
|
-
if (intervalRef.current) clearInterval(intervalRef.current);
|
|
69
|
-
};
|
|
70
|
-
}, [schema.refreshInterval, onRefresh, handleRefresh]);
|
|
71
|
-
const [layouts, setLayouts] = React.useState<{ lg: RGLLayout[] }>(() => {
|
|
72
|
-
// Try to load saved layout
|
|
73
|
-
if (typeof window !== 'undefined' && persistLayoutKey) {
|
|
74
|
-
const saved = localStorage.getItem(persistLayoutKey);
|
|
75
|
-
if (saved) {
|
|
76
|
-
try {
|
|
77
|
-
return JSON.parse(saved);
|
|
78
|
-
} catch (e) {
|
|
79
|
-
console.error('Failed to parse saved layout:', e);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Default layout from schema
|
|
85
|
-
return {
|
|
86
|
-
lg: schema.widgets?.map((widget: DashboardWidgetSchema, index: number) => ({
|
|
87
|
-
i: widget.id || `widget-${index}`,
|
|
88
|
-
x: widget.layout?.x || (index % 4) * 3,
|
|
89
|
-
y: widget.layout?.y || Math.floor(index / 4) * 4,
|
|
90
|
-
w: widget.layout?.w || 3,
|
|
91
|
-
h: widget.layout?.h || 4,
|
|
92
|
-
})) || [],
|
|
93
|
-
};
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const handleLayoutChange = React.useCallback(
|
|
97
|
-
(layout: Layout, allLayouts: ResponsiveLayouts) => {
|
|
98
|
-
setLayouts(allLayouts as { lg: RGLLayout[] });
|
|
99
|
-
onLayoutChange?.(layout as RGLLayout[]);
|
|
100
|
-
},
|
|
101
|
-
[onLayoutChange]
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
const handleSaveLayout = React.useCallback(() => {
|
|
105
|
-
if (typeof window !== 'undefined' && persistLayoutKey) {
|
|
106
|
-
localStorage.setItem(persistLayoutKey, JSON.stringify(layouts));
|
|
107
|
-
}
|
|
108
|
-
setEditMode(false);
|
|
109
|
-
}, [layouts, persistLayoutKey]);
|
|
110
|
-
|
|
111
|
-
const handleResetLayout = React.useCallback(() => {
|
|
112
|
-
const defaultLayouts = {
|
|
113
|
-
lg: schema.widgets?.map((widget: DashboardWidgetSchema, index: number) => ({
|
|
114
|
-
i: widget.id || `widget-${index}`,
|
|
115
|
-
x: widget.layout?.x || (index % 4) * 3,
|
|
116
|
-
y: widget.layout?.y || Math.floor(index / 4) * 4,
|
|
117
|
-
w: widget.layout?.w || 3,
|
|
118
|
-
h: widget.layout?.h || 4,
|
|
119
|
-
})) || [],
|
|
120
|
-
};
|
|
121
|
-
setLayouts(defaultLayouts);
|
|
122
|
-
if (typeof window !== 'undefined' && persistLayoutKey) {
|
|
123
|
-
localStorage.removeItem(persistLayoutKey);
|
|
124
|
-
}
|
|
125
|
-
}, [schema.widgets, persistLayoutKey]);
|
|
126
|
-
|
|
127
|
-
const getComponentSchema = React.useCallback((widget: DashboardWidgetSchema) => {
|
|
128
|
-
if (widget.component) return widget.component;
|
|
129
|
-
|
|
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 || [];
|
|
183
|
-
|
|
184
|
-
return {
|
|
185
|
-
type: 'chart',
|
|
186
|
-
chartType: widgetType,
|
|
187
|
-
data: dataItems,
|
|
188
|
-
xAxisKey: xAxisKey,
|
|
189
|
-
series: [{ dataKey: yField }],
|
|
190
|
-
colors: CHART_COLORS,
|
|
191
|
-
className: "h-full"
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
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
|
-
|
|
226
|
-
return {
|
|
227
|
-
type: 'data-table',
|
|
228
|
-
...options,
|
|
229
|
-
data: widgetData?.items || [],
|
|
230
|
-
searchable: false,
|
|
231
|
-
pagination: false,
|
|
232
|
-
className: "border-0"
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
|
|
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
|
-
};
|
|
262
|
-
}, []);
|
|
263
|
-
|
|
264
|
-
return (
|
|
265
|
-
<div ref={containerRef} className={cn("w-full", className)} data-testid="grid-layout">
|
|
266
|
-
{hasDndProvider && <DndEditModeBridge editMode={editMode} />}
|
|
267
|
-
<div className="mb-4 flex items-center justify-between">
|
|
268
|
-
<h2 className="text-2xl font-bold">{schema.title || 'Dashboard'}</h2>
|
|
269
|
-
<div className="flex gap-2">
|
|
270
|
-
{editMode ? (
|
|
271
|
-
<>
|
|
272
|
-
<Button onClick={handleSaveLayout} size="sm" variant="default">
|
|
273
|
-
<Save className="h-4 w-4 mr-2" />
|
|
274
|
-
Save Layout
|
|
275
|
-
</Button>
|
|
276
|
-
<Button onClick={handleResetLayout} size="sm" variant="outline">
|
|
277
|
-
<X className="h-4 w-4 mr-2" />
|
|
278
|
-
Reset
|
|
279
|
-
</Button>
|
|
280
|
-
<Button onClick={() => setEditMode(false)} size="sm" variant="ghost">
|
|
281
|
-
Cancel
|
|
282
|
-
</Button>
|
|
283
|
-
</>
|
|
284
|
-
) : (
|
|
285
|
-
<>
|
|
286
|
-
{onRefresh && (
|
|
287
|
-
<Button
|
|
288
|
-
onClick={handleRefresh}
|
|
289
|
-
size="sm"
|
|
290
|
-
variant="outline"
|
|
291
|
-
disabled={refreshing}
|
|
292
|
-
aria-label="Refresh dashboard"
|
|
293
|
-
>
|
|
294
|
-
<RefreshCw className={cn("h-4 w-4 mr-2", refreshing && "animate-spin")} />
|
|
295
|
-
{refreshing ? 'Refreshing…' : 'Refresh All'}
|
|
296
|
-
</Button>
|
|
297
|
-
)}
|
|
298
|
-
<Button onClick={() => setEditMode(true)} size="sm" variant="outline">
|
|
299
|
-
<Edit className="h-4 w-4 mr-2" />
|
|
300
|
-
Edit Layout
|
|
301
|
-
</Button>
|
|
302
|
-
</>
|
|
303
|
-
)}
|
|
304
|
-
</div>
|
|
305
|
-
</div>
|
|
306
|
-
|
|
307
|
-
{mounted && (
|
|
308
|
-
<ResponsiveGridLayout
|
|
309
|
-
className="layout"
|
|
310
|
-
width={width}
|
|
311
|
-
layouts={layouts}
|
|
312
|
-
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
|
313
|
-
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
|
|
314
|
-
rowHeight={60}
|
|
315
|
-
dragConfig={{ enabled: editMode, handle: ".drag-handle" }}
|
|
316
|
-
resizeConfig={{ enabled: editMode }}
|
|
317
|
-
onLayoutChange={handleLayoutChange}
|
|
318
|
-
>
|
|
319
|
-
{schema.widgets?.map((widget, index) => {
|
|
320
|
-
const widgetId = widget.id || `widget-${index}`;
|
|
321
|
-
const componentSchema = getComponentSchema(widget);
|
|
322
|
-
const isSelfContained = widget.type === 'metric';
|
|
323
|
-
|
|
324
|
-
return (
|
|
325
|
-
<div key={widgetId} className="h-full">
|
|
326
|
-
{isSelfContained ? (
|
|
327
|
-
<div className="h-full w-full relative">
|
|
328
|
-
{editMode && (
|
|
329
|
-
<div className="drag-handle absolute top-2 right-2 z-10 cursor-move p-1 bg-background/80 rounded border border-border">
|
|
330
|
-
<GripVertical className="h-4 w-4" />
|
|
331
|
-
</div>
|
|
332
|
-
)}
|
|
333
|
-
<SchemaRenderer schema={componentSchema} className="h-full w-full" />
|
|
334
|
-
</div>
|
|
335
|
-
) : (
|
|
336
|
-
<Card className={cn(
|
|
337
|
-
"h-full overflow-hidden border-border/50 shadow-sm transition-all",
|
|
338
|
-
"bg-card/50 backdrop-blur-sm",
|
|
339
|
-
editMode && "ring-2 ring-primary/20"
|
|
340
|
-
)}>
|
|
341
|
-
{widget.title && (
|
|
342
|
-
<CardHeader className="pb-2 border-b border-border/40 bg-muted/20 flex flex-row items-center justify-between">
|
|
343
|
-
<CardTitle className="text-base font-medium tracking-tight truncate" title={widget.title}>
|
|
344
|
-
{widget.title}
|
|
345
|
-
</CardTitle>
|
|
346
|
-
{editMode && (
|
|
347
|
-
<div className="drag-handle cursor-move p-1 hover:bg-muted/40 rounded">
|
|
348
|
-
<GripVertical className="h-4 w-4" />
|
|
349
|
-
</div>
|
|
350
|
-
)}
|
|
351
|
-
</CardHeader>
|
|
352
|
-
)}
|
|
353
|
-
<CardContent className="p-0 h-full">
|
|
354
|
-
<div className={cn("h-full w-full overflow-auto p-4")}>
|
|
355
|
-
<SchemaRenderer schema={componentSchema} />
|
|
356
|
-
</div>
|
|
357
|
-
</CardContent>
|
|
358
|
-
</Card>
|
|
359
|
-
)}
|
|
360
|
-
</div>
|
|
361
|
-
);
|
|
362
|
-
})}
|
|
363
|
-
</ResponsiveGridLayout>
|
|
364
|
-
)}
|
|
365
|
-
</div>
|
|
366
|
-
);
|
|
367
|
-
};
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
-
import { SchemaRenderer } from '@object-ui/react';
|
|
3
|
-
import type { BaseSchema } from '@object-ui/types';
|
|
4
|
-
|
|
5
|
-
const meta = {
|
|
6
|
-
title: 'Plugins/DashboardRenderer',
|
|
7
|
-
component: SchemaRenderer,
|
|
8
|
-
parameters: {
|
|
9
|
-
layout: 'padded',
|
|
10
|
-
},
|
|
11
|
-
tags: ['autodocs'],
|
|
12
|
-
argTypes: {
|
|
13
|
-
schema: { table: { disable: true } },
|
|
14
|
-
},
|
|
15
|
-
} satisfies Meta<any>;
|
|
16
|
-
|
|
17
|
-
export default meta;
|
|
18
|
-
type Story = StoryObj<typeof meta>;
|
|
19
|
-
|
|
20
|
-
const renderStory = (args: any) => <SchemaRenderer schema={args as unknown as BaseSchema} />;
|
|
21
|
-
|
|
22
|
-
export const Default: Story = {
|
|
23
|
-
render: renderStory,
|
|
24
|
-
args: {
|
|
25
|
-
type: 'dashboard',
|
|
26
|
-
columns: 3,
|
|
27
|
-
gap: 4,
|
|
28
|
-
widgets: [
|
|
29
|
-
{
|
|
30
|
-
id: 'metric-1',
|
|
31
|
-
component: {
|
|
32
|
-
type: 'metric',
|
|
33
|
-
label: 'Total Revenue',
|
|
34
|
-
value: '$128,430',
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
id: 'metric-2',
|
|
39
|
-
component: {
|
|
40
|
-
type: 'metric',
|
|
41
|
-
label: 'Active Users',
|
|
42
|
-
value: '3,842',
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
id: 'metric-3',
|
|
47
|
-
component: {
|
|
48
|
-
type: 'metric',
|
|
49
|
-
label: 'Conversion Rate',
|
|
50
|
-
value: '4.2%',
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
],
|
|
54
|
-
} as any,
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export const WithMetricCards: Story = {
|
|
58
|
-
render: renderStory,
|
|
59
|
-
args: {
|
|
60
|
-
type: 'dashboard',
|
|
61
|
-
columns: 3,
|
|
62
|
-
gap: 4,
|
|
63
|
-
widgets: [
|
|
64
|
-
{
|
|
65
|
-
id: 'mc-1',
|
|
66
|
-
component: {
|
|
67
|
-
type: 'metric-card',
|
|
68
|
-
title: 'Monthly Revenue',
|
|
69
|
-
value: '$52,489',
|
|
70
|
-
icon: 'DollarSign',
|
|
71
|
-
trend: 'up',
|
|
72
|
-
trendValue: '+14.2%',
|
|
73
|
-
description: 'vs last month',
|
|
74
|
-
},
|
|
75
|
-
layout: { x: 0, y: 0, w: 1, h: 1 },
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
id: 'mc-2',
|
|
79
|
-
component: {
|
|
80
|
-
type: 'metric-card',
|
|
81
|
-
title: 'New Signups',
|
|
82
|
-
value: '1,205',
|
|
83
|
-
icon: 'Users',
|
|
84
|
-
trend: 'up',
|
|
85
|
-
trendValue: '+8.1%',
|
|
86
|
-
description: 'vs last month',
|
|
87
|
-
},
|
|
88
|
-
layout: { x: 1, y: 0, w: 1, h: 1 },
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
id: 'mc-3',
|
|
92
|
-
component: {
|
|
93
|
-
type: 'metric-card',
|
|
94
|
-
title: 'Churn Rate',
|
|
95
|
-
value: '1.8%',
|
|
96
|
-
icon: 'TrendingDown',
|
|
97
|
-
trend: 'down',
|
|
98
|
-
trendValue: '-0.3%',
|
|
99
|
-
description: 'vs last month',
|
|
100
|
-
},
|
|
101
|
-
layout: { x: 2, y: 0, w: 1, h: 1 },
|
|
102
|
-
},
|
|
103
|
-
],
|
|
104
|
-
} as any,
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
export const WithChartsAndMetrics: Story = {
|
|
108
|
-
render: renderStory,
|
|
109
|
-
args: {
|
|
110
|
-
type: 'dashboard',
|
|
111
|
-
columns: 3,
|
|
112
|
-
gap: 4,
|
|
113
|
-
widgets: [
|
|
114
|
-
{
|
|
115
|
-
id: 'd-m1',
|
|
116
|
-
component: {
|
|
117
|
-
type: 'metric-card',
|
|
118
|
-
title: 'Total Orders',
|
|
119
|
-
value: '1,284',
|
|
120
|
-
icon: 'ShoppingCart',
|
|
121
|
-
trend: 'up',
|
|
122
|
-
trendValue: '+11%',
|
|
123
|
-
},
|
|
124
|
-
layout: { x: 0, y: 0, w: 1, h: 1 },
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
id: 'd-m2',
|
|
128
|
-
component: {
|
|
129
|
-
type: 'metric-card',
|
|
130
|
-
title: 'Avg Order Value',
|
|
131
|
-
value: '$86.50',
|
|
132
|
-
icon: 'DollarSign',
|
|
133
|
-
trend: 'up',
|
|
134
|
-
trendValue: '+3.2%',
|
|
135
|
-
},
|
|
136
|
-
layout: { x: 1, y: 0, w: 1, h: 1 },
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
id: 'd-m3',
|
|
140
|
-
component: {
|
|
141
|
-
type: 'metric-card',
|
|
142
|
-
title: 'Return Rate',
|
|
143
|
-
value: '2.1%',
|
|
144
|
-
icon: 'TrendingDown',
|
|
145
|
-
trend: 'down',
|
|
146
|
-
trendValue: '-0.8%',
|
|
147
|
-
},
|
|
148
|
-
layout: { x: 2, y: 0, w: 1, h: 1 },
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
id: 'd-chart',
|
|
152
|
-
title: 'Weekly Sales',
|
|
153
|
-
component: {
|
|
154
|
-
type: 'chart',
|
|
155
|
-
chartType: 'bar',
|
|
156
|
-
data: [
|
|
157
|
-
{ day: 'Mon', sales: 120 },
|
|
158
|
-
{ day: 'Tue', sales: 180 },
|
|
159
|
-
{ day: 'Wed', sales: 150 },
|
|
160
|
-
{ day: 'Thu', sales: 210 },
|
|
161
|
-
{ day: 'Fri', sales: 190 },
|
|
162
|
-
],
|
|
163
|
-
xAxisKey: 'day',
|
|
164
|
-
series: [{ dataKey: 'sales' }],
|
|
165
|
-
config: {
|
|
166
|
-
sales: { label: 'Sales', color: '#3b82f6' },
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
layout: { x: 0, y: 1, w: 3, h: 2 },
|
|
170
|
-
},
|
|
171
|
-
],
|
|
172
|
-
} as any,
|
|
173
|
-
};
|