Skip to content

Commit 99b8d59

Browse files
authored
fix: ChartBuilderPlugin fixes for charts built from PPQs in Enterprise (#2167)
- Don't sanitize the descriptor in `AppDashboards` - the descriptor gets sanitized within the objectFetcher itself - By sanitizing too early, we lose metadata needed to load an object - ChartPanelPlugin uses the `panelFetch` to get the underlying object - In cases of a `ParameterizedQuery` on Enterprise, we only have the result of the `ParameterizedQuery` run in the `ParameterizedQueryPanel` - Aside, if we're looking at improving `ParameterizedQuery` support, we should complete DH-15760 (which would be breaking an internal API) - Use correct API when fetching a Chart object
1 parent d78ad6d commit 99b8d59

7 files changed

Lines changed: 80 additions & 121 deletions

File tree

packages/app-utils/src/components/AppDashboards.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ import {
55
DehydratedDashboardPanelProps,
66
LazyDashboard,
77
} from '@deephaven/dashboard';
8-
import {
9-
sanitizeVariableDescriptor,
10-
useObjectFetcher,
11-
} from '@deephaven/jsapi-bootstrap';
8+
import { useObjectFetcher } from '@deephaven/jsapi-bootstrap';
129
import LayoutManager, {
1310
ItemConfig,
1411
Settings as LayoutSettings,
@@ -44,9 +41,8 @@ export function AppDashboards({
4441
const { metadata } = hydrateProps;
4542
try {
4643
if (metadata != null) {
47-
const widget = sanitizeVariableDescriptor(metadata);
4844
return {
49-
fetch: async () => fetchObject(widget),
45+
fetch: async () => fetchObject(metadata),
5046
...hydrateProps,
5147
localDashboardId: id,
5248
};

packages/dashboard-core-plugins/src/ChartBuilderPlugin.tsx

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
import { useCallback } from 'react';
2-
import {
3-
ChartModel,
4-
ChartModelFactory,
5-
ChartModelSettings,
6-
ChartUtils,
7-
} from '@deephaven/chart';
2+
import { ChartModelSettings, ChartUtils } from '@deephaven/chart';
83
import {
94
assertIsDashboardPluginProps,
105
DashboardPluginComponentProps,
116
LayoutUtils,
127
useListener,
138
} from '@deephaven/dashboard';
14-
import { useApi } from '@deephaven/jsapi-bootstrap';
159
import type { dh } from '@deephaven/jsapi-types';
1610
import { nanoid } from 'nanoid';
1711
import { IrisGridEvent } from './events';
@@ -28,7 +22,6 @@ export function ChartBuilderPlugin(
2822
): JSX.Element | null {
2923
assertIsDashboardPluginProps(props);
3024
const { id, layout } = props;
31-
const dh = useApi();
3225

3326
const handleCreateChart = useCallback(
3427
({
@@ -45,8 +38,7 @@ export function ChartBuilderPlugin(
4538
table: dh.Table;
4639
}) => {
4740
const { settings } = metadata;
48-
const makeModel = (): Promise<ChartModel> =>
49-
ChartModelFactory.makeModelFromSettings(dh, settings, table);
41+
const fetchTable = async () => table;
5042
const title = ChartUtils.titleFromSettings(settings);
5143

5244
const config = {
@@ -56,7 +48,7 @@ export function ChartBuilderPlugin(
5648
localDashboardId: id,
5749
id: panelId,
5850
metadata,
59-
makeModel,
51+
fetch: fetchTable,
6052
},
6153
title,
6254
id: panelId,
@@ -65,7 +57,7 @@ export function ChartBuilderPlugin(
6557
const { root } = layout;
6658
LayoutUtils.openComponent({ root, config });
6759
},
68-
[dh, id, layout]
60+
[id, layout]
6961
);
7062

7163
useListener(layout.eventHub, IrisGridEvent.CREATE_CHART, handleCreateChart);

packages/dashboard-core-plugins/src/ChartPanelPlugin.tsx

Lines changed: 46 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
1-
import { forwardRef, useMemo } from 'react';
2-
import {
3-
ObjectFetcher,
4-
useApi,
5-
useObjectFetcher,
6-
} from '@deephaven/jsapi-bootstrap';
1+
import { forwardRef, useCallback } from 'react';
2+
import { useDeferredApi } from '@deephaven/jsapi-bootstrap';
73
import { ChartModel, ChartModelFactory } from '@deephaven/chart';
84
import type { dh as DhType } from '@deephaven/jsapi-types';
95
import { IrisGridUtils } from '@deephaven/iris-grid';
106
import { getTimeZone, store } from '@deephaven/redux';
117
import { WidgetPanelProps } from '@deephaven/plugin';
8+
import { assertNotNull } from '@deephaven/utils';
129
import {
1310
ChartPanelMetadata,
1411
GLChartPanelState,
1512
isChartPanelDehydratedProps,
16-
isChartPanelFigureMetadata,
1713
isChartPanelTableMetadata,
1814
} from './panels';
1915
import ConnectedChartPanel, {
@@ -23,28 +19,18 @@ import ConnectedChartPanel, {
2319

2420
async function createChartModel(
2521
dh: typeof DhType,
26-
fetchObject: ObjectFetcher,
2722
metadata: ChartPanelMetadata,
28-
fetchFigure: () => Promise<DhType.plot.Figure>,
23+
panelFetch: () => Promise<DhType.plot.Figure | DhType.Table>,
2924
panelState?: GLChartPanelState
3025
): Promise<ChartModel> {
31-
let settings;
32-
let tableName;
33-
let figureName;
34-
let tableSettings;
26+
let settings = {};
27+
let tableName = '';
28+
let tableSettings = {};
3529

3630
if (isChartPanelTableMetadata(metadata)) {
3731
settings = metadata.settings;
3832
tableName = metadata.table;
39-
figureName = undefined;
4033
tableSettings = metadata.tableSettings;
41-
} else {
42-
settings = {};
43-
tableName = '';
44-
figureName = isChartPanelFigureMetadata(metadata)
45-
? metadata.figure
46-
: metadata.name;
47-
tableSettings = {};
4834
}
4935
if (panelState != null) {
5036
if (panelState.tableSettings != null) {
@@ -53,9 +39,6 @@ async function createChartModel(
5339
if (panelState.table != null) {
5440
tableName = panelState.table;
5541
}
56-
if (panelState.figure != null) {
57-
figureName = panelState.figure;
58-
}
5942
if (panelState.settings != null) {
6043
settings = {
6144
...settings,
@@ -64,83 +47,59 @@ async function createChartModel(
6447
}
6548
}
6649

67-
if (figureName == null && tableName == null) {
68-
const figure = await fetchFigure();
69-
70-
return ChartModelFactory.makeModel(dh, settings, figure);
71-
}
72-
73-
if (figureName != null) {
74-
let figure: DhType.plot.Figure;
75-
76-
if (metadata.type === dh.VariableType.FIGURE) {
77-
const descriptor = {
78-
...metadata,
79-
name: figureName,
80-
type: dh.VariableType.FIGURE,
81-
};
82-
figure = await fetchObject<DhType.plot.Figure>(descriptor);
83-
} else {
84-
figure = await fetchFigure();
85-
}
50+
if (tableName != null && tableName !== '') {
51+
const table = (await panelFetch()) as DhType.Table;
52+
new IrisGridUtils(dh).applyTableSettings(
53+
table,
54+
tableSettings,
55+
getTimeZone(store.getState())
56+
);
8657

87-
return ChartModelFactory.makeModel(dh, settings, figure);
58+
return ChartModelFactory.makeModelFromSettings(dh, settings, table);
8859
}
8960

90-
const descriptor = {
91-
...metadata,
92-
name: tableName,
93-
type: dh.VariableType.TABLE,
94-
};
95-
const table = await fetchObject<DhType.Table>(descriptor);
96-
new IrisGridUtils(dh).applyTableSettings(
97-
table,
98-
tableSettings,
99-
getTimeZone(store.getState())
100-
);
61+
const figure = (await panelFetch()) as DhType.plot.Figure;
10162

102-
return ChartModelFactory.makeModelFromSettings(
103-
dh,
104-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
105-
settings as any,
106-
table
107-
);
63+
return ChartModelFactory.makeModel(dh, settings, figure);
10864
}
10965

11066
export const ChartPanelPlugin = forwardRef(
11167
(props: WidgetPanelProps<DhType.plot.Figure>, ref: React.Ref<ChartPanel>) => {
112-
const dh = useApi();
113-
const fetchObject = useObjectFetcher();
114-
11568
const panelState = isChartPanelDehydratedProps(props)
11669
? (props as unknown as ChartPanelProps).panelState
11770
: undefined;
11871

11972
const { fetch: panelFetch, metadata, localDashboardId } = props;
120-
121-
const hydratedProps = useMemo(
122-
() => ({
123-
metadata: metadata as ChartPanelMetadata,
124-
localDashboardId,
125-
makeModel: () => {
126-
if (metadata == null) {
127-
throw new Error('Metadata is required for chart panel');
128-
}
129-
130-
return createChartModel(
131-
dh,
132-
fetchObject,
133-
metadata as ChartPanelMetadata,
134-
panelFetch,
135-
panelState
136-
);
137-
},
138-
}),
139-
[metadata, localDashboardId, dh, fetchObject, panelFetch, panelState]
73+
assertNotNull(metadata);
74+
const [dh, error] = useDeferredApi(metadata);
75+
76+
const makeModel = useCallback(async () => {
77+
if (error != null) {
78+
throw error;
79+
}
80+
if (dh == null) {
81+
return new Promise<ChartModel>(() => {
82+
// We don't have the API yet, just return an unresolved promise so it shows as loading
83+
});
84+
}
85+
return createChartModel(
86+
dh,
87+
metadata as ChartPanelMetadata,
88+
panelFetch,
89+
panelState
90+
);
91+
}, [dh, error, metadata, panelFetch, panelState]);
92+
93+
return (
94+
<ConnectedChartPanel
95+
ref={ref}
96+
// eslint-disable-next-line react/jsx-props-no-spreading
97+
{...props}
98+
metadata={metadata}
99+
localDashboardId={localDashboardId}
100+
makeModel={makeModel}
101+
/>
140102
);
141-
142-
// eslint-disable-next-line react/jsx-props-no-spreading
143-
return <ConnectedChartPanel ref={ref} {...props} {...hydratedProps} />;
144103
}
145104
);
146105

packages/dashboard-core-plugins/src/panels/ChartPanel.tsx

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import debounce from 'lodash.debounce';
66
import {
77
Chart,
88
ChartModel,
9+
ChartModelFactory,
910
ChartModelSettings,
10-
ChartUtils,
1111
FilterMap,
1212
isFigureChartModel,
1313
} from '@deephaven/chart';
@@ -286,14 +286,14 @@ export class ChartPanel extends Component<ChartPanelProps, ChartPanelState> {
286286
const { columnMap, model, filterMap, filterValueMap, isLinked, settings } =
287287
this.state;
288288

289-
if (!model) {
290-
return;
291-
}
292-
293289
if (makeModel !== prevProps.makeModel) {
294290
this.initModel();
295291
}
296292

293+
if (model == null) {
294+
return;
295+
}
296+
297297
if (columnMap !== prevState.columnMap) {
298298
this.pruneFilterMaps();
299299
}
@@ -352,6 +352,8 @@ export class ChartPanel extends Component<ChartPanelProps, ChartPanelState> {
352352

353353
const { makeModel } = this.props;
354354

355+
this.pending.cancel();
356+
355357
this.pending
356358
.add(makeModel(), resolved => {
357359
resolved.close();
@@ -651,12 +653,8 @@ export class ChartPanel extends Component<ChartPanelProps, ChartPanelState> {
651653
const { settings } = metadata;
652654
this.pending
653655
.add(
654-
dh.plot.Figure.create(
655-
new ChartUtils(dh).makeFigureSettings(
656-
settings,
657-
source
658-
) as unknown as dh.plot.FigureDescriptor
659-
)
656+
ChartModelFactory.makeFigureFromSettings(dh, settings, source),
657+
resolved => resolved.close()
660658
)
661659
.then(figure => {
662660
if (isFigureChartModel(model)) {

packages/dashboard-core-plugins/src/panels/WidgetPanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import React, { PureComponent, ReactElement, ReactNode } from 'react';
22
import classNames from 'classnames';
33
import memoize from 'memoize-one';
4+
import { ContextActions, createXComponent } from '@deephaven/components';
45
import { PanelComponent } from '@deephaven/dashboard';
56
import type { Container, EventEmitter } from '@deephaven/golden-layout';
6-
import { ContextActions, createXComponent } from '@deephaven/components';
77
import { copyToClipboard } from '@deephaven/utils';
88
import Panel from './Panel';
99
import WidgetPanelTooltip from './WidgetPanelTooltip';

packages/jsapi-bootstrap/src/useDeferredApi.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ describe('useDeferredApi', () => {
2929
expect(result.current).toEqual([dh1, null]);
3030

3131
const { result: result2 } = renderHook(() =>
32-
useDeferredApi({ type: 'foo', foo: 'bar' })
32+
useDeferredApi({
33+
type: 'foo',
34+
foo: 'bar',
35+
} as DhType.ide.VariableDescriptor)
3336
);
3437
expect(result2.current).toEqual([dh1, null]);
3538
});
@@ -68,4 +71,10 @@ describe('useDeferredApi', () => {
6871
const { result } = renderHook(() => useDeferredApi(objectMetadata));
6972
expect(result.current).toEqual([null, expect.any(Error)]);
7073
});
74+
75+
it('returns an error if the metadata is null', async () => {
76+
asMock(useContext).mockReturnValue(dh1);
77+
const { result } = renderHook(() => useDeferredApi(null));
78+
expect(result.current).toEqual([null, expect.any(Error)]);
79+
});
7180
});

packages/jsapi-bootstrap/src/useDeferredApi.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ export const DeferredApiContext = createContext<
2424
* @returns A tuple with the API instance, and an error if one occurred.
2525
*/
2626
export function useDeferredApi(
27-
widget: DhType.ide.VariableDescriptor
28-
): [typeof DhType | null, unknown | null] {
27+
widget: DhType.ide.VariableDescriptor | null
28+
): [dh: typeof DhType | null, error: unknown | null] {
2929
const [api, setApi] = useState<typeof DhType | null>(null);
3030
const [error, setError] = useState<unknown | null>(null);
3131
const deferredApi = useContext(DeferredApiContext);
@@ -49,7 +49,12 @@ export function useDeferredApi(
4949
let isCancelled = false;
5050

5151
async function loadApi() {
52-
if (typeof deferredApi === 'function') {
52+
if (widget == null) {
53+
if (!isCancelled) {
54+
setApi(null);
55+
setError(new Error('No widget provided to useDeferredApi'));
56+
}
57+
} else if (typeof deferredApi === 'function') {
5358
try {
5459
const newApi = await deferredApi(widget);
5560
if (!isCancelled) {

0 commit comments

Comments
 (0)