Skip to content

Commit 3652b46

Browse files
Copilothotlong
andcommitted
fix: prevent data-table/pivot crash and chart blank rendering in provider:object dashboard widgets
- Fix data-table crash: add Array.isArray() guard for non-array data (e.g. provider config object) - Fix DashboardRenderer: destructure data out of options before spreading for provider:object table/pivot widgets - Fix ObjectChart: show visible loading/warning messages instead of silent blank rendering - Fix DashboardGridLayout: add provider:object support for charts, tables, and pivot widgets - Fix PivotTable: add Array.isArray() guard for non-array data - Add regression tests for data-table and pivot crashes Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 57bfaa1 commit 3652b46

File tree

6 files changed

+151
-8
lines changed

6 files changed

+151
-8
lines changed

packages/components/src/renderers/complex/data-table.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
9191
const {
9292
caption,
9393
columns: rawColumns = [],
94-
data = [],
94+
data: rawData = [],
9595
pagination = true,
9696
pageSize: initialPageSize = 10,
9797
searchable = true,
@@ -109,6 +109,10 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
109109
showAddRow = false,
110110
} = schema;
111111

112+
// Ensure data is always an array – provider config objects or null/undefined
113+
// must not reach array operations like .filter() / .some()
114+
const data = Array.isArray(rawData) ? rawData : [];
115+
112116
// Normalize columns to support legacy keys (label/name) from existing JSONs
113117
const initialColumns = useMemo(() => {
114118
return rawColumns.map((col: any) => ({

packages/plugin-charts/src/ObjectChart.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,11 @@ export const ObjectChart = (props: any) => {
104104
};
105105

106106
if (loading && finalData.length === 0) {
107-
// Return skeleton or loading state?
108-
// ChartRenderer has suspense/skeleton handling but needs to be triggered.
109-
// We pass empty data but it might render empty chart.
107+
return <div className={"flex items-center justify-center text-muted-foreground text-sm p-4 " + (schema.className || '')}>Loading chart data…</div>;
108+
}
109+
110+
if (!dataSource && schema.objectName && finalData.length === 0) {
111+
return <div className={"flex items-center justify-center text-muted-foreground text-sm p-4 " + (schema.className || '')}>No data source available for "{schema.objectName}"</div>;
110112
}
111113

112114
return <ChartRenderer {...props} schema={finalSchema} />;

packages/plugin-dashboard/src/DashboardGridLayout.tsx

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ const CHART_COLORS = [
3030
'hsl(var(--chart-5))',
3131
];
3232

33+
/** Returns true when the widget data config uses provider: 'object' (async data source). */
34+
function isObjectProvider(widgetData: unknown): widgetData is { provider: 'object'; object?: string; aggregate?: any } {
35+
return (
36+
widgetData != null &&
37+
typeof widgetData === 'object' &&
38+
!Array.isArray(widgetData) &&
39+
(widgetData as any).provider === 'object'
40+
);
41+
}
42+
3343
export interface DashboardGridLayoutProps {
3444
schema: DashboardSchema;
3545
className?: string;
@@ -130,9 +140,25 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
130140
const options = (widget.options || {}) as Record<string, any>;
131141
if (widgetType === 'bar' || widgetType === 'line' || widgetType === 'area' || widgetType === 'pie' || widgetType === 'donut') {
132142
const widgetData = (widget as any).data || options.data;
133-
const dataItems = Array.isArray(widgetData) ? widgetData : widgetData?.items || [];
134143
const xAxisKey = options.xField || 'name';
135144
const yField = options.yField || 'value';
145+
146+
// provider: 'object' — delegate to ObjectChart for async data loading
147+
if (isObjectProvider(widgetData)) {
148+
const effectiveYField = widgetData.aggregate?.field || yField;
149+
return {
150+
type: 'object-chart',
151+
chartType: widgetType,
152+
objectName: widgetData.object || widget.object,
153+
aggregate: widgetData.aggregate,
154+
xAxisKey: xAxisKey,
155+
series: [{ dataKey: effectiveYField }],
156+
colors: CHART_COLORS,
157+
className: "h-full"
158+
};
159+
}
160+
161+
const dataItems = Array.isArray(widgetData) ? widgetData : widgetData?.items || [];
136162

137163
return {
138164
type: 'chart',
@@ -147,6 +173,22 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
147173

148174
if (widgetType === 'table') {
149175
const widgetData = (widget as any).data || options.data;
176+
177+
// provider: 'object' — pass through object config for async data loading
178+
if (isObjectProvider(widgetData)) {
179+
const { data: _dropped, ...restOptions } = options;
180+
return {
181+
type: 'data-table',
182+
...restOptions,
183+
objectName: widgetData.object || widget.object,
184+
dataProvider: widgetData,
185+
data: [],
186+
searchable: false,
187+
pagination: false,
188+
className: "border-0"
189+
};
190+
}
191+
150192
return {
151193
type: 'data-table',
152194
...options,
@@ -157,6 +199,28 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
157199
};
158200
}
159201

202+
if (widgetType === 'pivot') {
203+
const widgetData = (widget as any).data || options.data;
204+
205+
// provider: 'object' — pass through object config for async data loading
206+
if (isObjectProvider(widgetData)) {
207+
const { data: _dropped, ...restOptions } = options;
208+
return {
209+
type: 'pivot',
210+
...restOptions,
211+
objectName: widgetData.object || widget.object,
212+
dataProvider: widgetData,
213+
data: [],
214+
};
215+
}
216+
217+
return {
218+
type: 'pivot',
219+
...options,
220+
data: Array.isArray(widgetData) ? widgetData : widgetData?.items || [],
221+
};
222+
}
223+
160224
return {
161225
...widget,
162226
...options

packages/plugin-dashboard/src/DashboardRenderer.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,13 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
164164

165165
// provider: 'object' — pass through object config for async data loading
166166
if (isObjectProvider(widgetData)) {
167+
const { data: _dropped, ...restOptions } = options;
167168
return {
168169
type: 'data-table',
169-
...options,
170+
...restOptions,
170171
objectName: widgetData.object || widget.object,
171172
dataProvider: widgetData,
173+
data: [],
172174
searchable: false,
173175
pagination: false,
174176
className: "border-0"
@@ -190,11 +192,13 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
190192

191193
// provider: 'object' — pass through object config for async data loading
192194
if (isObjectProvider(widgetData)) {
195+
const { data: _dropped, ...restOptions } = options;
193196
return {
194197
type: 'pivot',
195-
...options,
198+
...restOptions,
196199
objectName: widgetData.object || widget.object,
197200
dataProvider: widgetData,
201+
data: [],
198202
};
199203
}
200204

packages/plugin-dashboard/src/PivotTable.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,16 @@ export const PivotTable: React.FC<PivotTableProps> = ({ schema, className }) =>
9595
columnField,
9696
valueField,
9797
aggregation = 'sum',
98-
data = [],
98+
data: rawData = [],
9999
showRowTotals = false,
100100
showColumnTotals = false,
101101
format,
102102
columnColors,
103103
} = schema;
104104

105+
// Ensure data is always an array – provider config objects must not reach iteration
106+
const data = Array.isArray(rawData) ? rawData : [];
107+
105108
const { rowKeys, colKeys, matrix, rowTotals, colTotals, grandTotal } = useMemo(() => {
106109
// Collect unique row/column values preserving insertion order
107110
const rowSet = new Map<string, true>();

packages/plugin-dashboard/src/__tests__/DashboardRenderer.widgetData.test.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,4 +659,70 @@ describe('DashboardRenderer widget data extraction', () => {
659659
expect(chartSchema).toBeDefined();
660660
expect(chartSchema.data).toEqual([]);
661661
});
662+
663+
it('should not crash data-table when provider:object leaks data config via options spread', () => {
664+
const schema = {
665+
type: 'dashboard' as const,
666+
name: 'test',
667+
title: 'Test',
668+
widgets: [
669+
{
670+
type: 'table',
671+
title: 'Provider Object Table',
672+
object: 'opportunity',
673+
layout: { x: 0, y: 0, w: 4, h: 2 },
674+
options: {
675+
columns: [
676+
{ header: 'Name', accessorKey: 'name' },
677+
{ header: 'Amount', accessorKey: 'amount' },
678+
],
679+
data: {
680+
provider: 'object',
681+
object: 'opportunity',
682+
},
683+
},
684+
},
685+
],
686+
} as any;
687+
688+
// Must render without throwing. Previously this crashed with
689+
// "paginatedData.some is not a function" because the provider
690+
// config object leaked through as data.
691+
const { container } = render(<DashboardRenderer schema={schema} />);
692+
expect(container).toBeDefined();
693+
// The component should not show a crash error
694+
expect(container.textContent).not.toContain('is not a function');
695+
});
696+
697+
it('should not crash pivot table when provider:object leaks data config via options spread', () => {
698+
const schema = {
699+
type: 'dashboard' as const,
700+
name: 'test',
701+
title: 'Test',
702+
widgets: [
703+
{
704+
type: 'pivot',
705+
title: 'Provider Object Pivot',
706+
object: 'sales',
707+
layout: { x: 0, y: 0, w: 4, h: 2 },
708+
options: {
709+
rowField: 'region',
710+
columnField: 'quarter',
711+
valueField: 'revenue',
712+
data: {
713+
provider: 'object',
714+
object: 'sales',
715+
},
716+
},
717+
},
718+
],
719+
} as any;
720+
721+
// Must render without throwing. Previously this crashed with
722+
// "data is not iterable" because the provider config object
723+
// leaked through as data.
724+
const { container } = render(<DashboardRenderer schema={schema} />);
725+
expect(container).toBeDefined();
726+
expect(container.textContent).not.toContain('is not iterable');
727+
});
662728
});

0 commit comments

Comments
 (0)