Skip to content

Commit 0a924cd

Browse files
authored
fix: DH-17292 Handle disconnect from GridWidgetPlugin (#2086)
- When the model is disconnected, we should just display an error. There's no option to reconnect, as the widget schema could change entirely - By unloading the IrisGrid component, it's no longer throwing an error by trying to access table methods after a table.close() - Fixes DH-17292 from Enterprise - Tested using the steps in the description
1 parent 336e1f3 commit 0a924cd

4 files changed

Lines changed: 302 additions & 85 deletions

File tree

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,34 @@
1-
import { useEffect, useState } from 'react';
21
import { type WidgetComponentProps } from '@deephaven/plugin';
32
import { type dh } from '@deephaven/jsapi-types';
4-
import { useApi } from '@deephaven/jsapi-bootstrap';
5-
import {
6-
IrisGrid,
7-
IrisGridModelFactory,
8-
type IrisGridModel,
9-
} from '@deephaven/iris-grid';
3+
import { IrisGrid } from '@deephaven/iris-grid';
104
import { useSelector } from 'react-redux';
115
import { getSettings, RootState } from '@deephaven/redux';
6+
import { LoadingOverlay } from '@deephaven/components';
7+
import { getErrorMessage } from '@deephaven/utils';
8+
import { useIrisGridModel } from './useIrisGridModel';
129

13-
export function GridWidgetPlugin(
14-
props: WidgetComponentProps<dh.Table>
15-
): JSX.Element | null {
16-
const dh = useApi();
10+
export function GridWidgetPlugin({
11+
fetch,
12+
}: WidgetComponentProps<dh.Table>): JSX.Element | null {
1713
const settings = useSelector(getSettings<RootState>);
18-
const [model, setModel] = useState<IrisGridModel>();
1914

20-
const { fetch } = props;
15+
const fetchResult = useIrisGridModel(fetch);
2116

22-
useEffect(() => {
23-
let cancelled = false;
24-
async function init() {
25-
const table = await fetch();
26-
const newModel = await IrisGridModelFactory.makeModel(dh, table);
27-
if (!cancelled) {
28-
setModel(newModel);
29-
}
30-
}
17+
if (fetchResult.status === 'loading') {
18+
return <LoadingOverlay isLoading />;
19+
}
3120

32-
init();
21+
if (fetchResult.status === 'error') {
22+
return (
23+
<LoadingOverlay
24+
errorMessage={getErrorMessage(fetchResult.error)}
25+
isLoading={false}
26+
/>
27+
);
28+
}
3329

34-
return () => {
35-
cancelled = true;
36-
};
37-
}, [dh, fetch]);
38-
39-
return model ? <IrisGrid model={model} settings={settings} /> : null;
30+
const { model } = fetchResult;
31+
return <IrisGrid model={model} settings={settings} />;
4032
}
4133

4234
export default GridWidgetPlugin;

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

Lines changed: 26 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,34 @@
1-
import { useCallback, useEffect, useState } from 'react';
21
import { WidgetComponentProps } from '@deephaven/plugin';
32
import { type dh } from '@deephaven/jsapi-types';
4-
import IrisGrid, {
5-
IrisGridModelFactory,
6-
type IrisGridModel,
7-
} from '@deephaven/iris-grid';
8-
import { useApi } from '@deephaven/jsapi-bootstrap';
3+
import IrisGrid from '@deephaven/iris-grid';
94
import { LoadingOverlay } from '@deephaven/components';
5+
import { getErrorMessage } from '@deephaven/utils';
106
import { PandasReloadButton } from './panels/PandasReloadButton';
11-
12-
export function PandasWidgetPlugin(
13-
props: WidgetComponentProps<dh.Table>
14-
): JSX.Element | null {
15-
const dh = useApi();
16-
const [model, setModel] = useState<IrisGridModel>();
17-
const [isLoading, setIsLoading] = useState(true);
18-
const [isLoaded, setIsLoaded] = useState(false);
19-
20-
const { fetch } = props;
21-
22-
const makeModel = useCallback(async () => {
23-
const table = await fetch();
24-
return IrisGridModelFactory.makeModel(dh, table);
25-
}, [dh, fetch]);
26-
27-
const handleReload = useCallback(async () => {
28-
setIsLoading(true);
29-
const newModel = await makeModel();
30-
setModel(newModel);
31-
setIsLoading(false);
32-
}, [makeModel]);
33-
34-
useEffect(() => {
35-
let cancelled = false;
36-
async function init() {
37-
const newModel = await makeModel();
38-
if (!cancelled) {
39-
setModel(newModel);
40-
setIsLoaded(true);
41-
setIsLoading(false);
42-
}
43-
}
44-
45-
init();
46-
setIsLoading(true);
47-
48-
return () => {
49-
cancelled = true;
50-
};
51-
}, [makeModel]);
52-
7+
import { useIrisGridModel } from './useIrisGridModel';
8+
9+
export function PandasWidgetPlugin({
10+
fetch,
11+
}: WidgetComponentProps<dh.Table>): JSX.Element | null {
12+
const fetchResult = useIrisGridModel(fetch);
13+
14+
if (fetchResult.status === 'loading') {
15+
return <LoadingOverlay isLoading />;
16+
}
17+
18+
if (fetchResult.status === 'error') {
19+
return (
20+
<LoadingOverlay
21+
errorMessage={getErrorMessage(fetchResult.error)}
22+
isLoading={false}
23+
/>
24+
);
25+
}
26+
27+
const { model, reload } = fetchResult;
5328
return (
54-
<>
55-
<LoadingOverlay isLoaded={isLoaded} isLoading={isLoading} />
56-
{model && (
57-
<IrisGrid model={model}>
58-
<PandasReloadButton onClick={handleReload} />
59-
</IrisGrid>
60-
)}
61-
</>
29+
<IrisGrid model={model}>
30+
<PandasReloadButton onClick={reload} />
31+
</IrisGrid>
6232
);
6333
}
6434

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { IrisGridModel } from '@deephaven/iris-grid';
2+
import { type dh } from '@deephaven/jsapi-types';
3+
import { TestUtils } from '@deephaven/utils';
4+
import { renderHook } from '@testing-library/react-hooks';
5+
import { act } from 'react-test-renderer';
6+
import {
7+
IrisGridModelFetchErrorResult,
8+
IrisGridModelFetchSuccessResult,
9+
useIrisGridModel,
10+
} from './useIrisGridModel';
11+
12+
const mockApi = TestUtils.createMockProxy<typeof dh>();
13+
// Mock out the useApi hook to just return the API
14+
jest.mock('@deephaven/jsapi-bootstrap', () => ({
15+
useApi: () => mockApi,
16+
}));
17+
18+
const mockModel = TestUtils.createMockProxy<IrisGridModel>();
19+
// Mock out the IrisGridModelFactory as well
20+
jest.mock('@deephaven/iris-grid', () => ({
21+
...jest.requireActual('@deephaven/iris-grid'),
22+
IrisGridModelFactory: {
23+
makeModel: jest.fn(() => mockModel),
24+
},
25+
}));
26+
27+
it('should return loading status while fetching', () => {
28+
const fetch = jest.fn(
29+
() =>
30+
new Promise<dh.Table>(() => {
31+
// Do nothing
32+
})
33+
);
34+
const { result } = renderHook(() => useIrisGridModel(fetch));
35+
expect(result.current.status).toBe('loading');
36+
});
37+
38+
it('should return error status on fetch error', async () => {
39+
const error = new Error('Test error');
40+
const fetch = jest.fn(() => Promise.reject(error));
41+
const { result, waitForNextUpdate } = renderHook(() =>
42+
useIrisGridModel(fetch)
43+
);
44+
await waitForNextUpdate();
45+
const fetchResult = result.current;
46+
expect(fetchResult.status).toBe('error');
47+
expect((fetchResult as IrisGridModelFetchErrorResult).error).toBe(error);
48+
});
49+
50+
it('should return success status on fetch success', async () => {
51+
const table = TestUtils.createMockProxy<dh.Table>();
52+
const fetch = jest.fn(() => Promise.resolve(table));
53+
const { result, waitForNextUpdate } = renderHook(() =>
54+
useIrisGridModel(fetch)
55+
);
56+
await waitForNextUpdate();
57+
const fetchResult = result.current;
58+
expect(fetchResult.status).toBe('success');
59+
expect((fetchResult as IrisGridModelFetchSuccessResult).model).toBeDefined();
60+
});
61+
62+
it('should reload the model on reload', async () => {
63+
const table = TestUtils.createMockProxy<dh.Table>();
64+
let fetchResolve;
65+
const fetch = jest.fn(
66+
() =>
67+
new Promise<dh.Table>(resolve => {
68+
fetchResolve = resolve;
69+
})
70+
);
71+
const { result, waitForNextUpdate } = renderHook(() =>
72+
useIrisGridModel(fetch)
73+
);
74+
expect(result.current.status).toBe('loading');
75+
fetchResolve(table);
76+
await waitForNextUpdate();
77+
expect(result.current.status).toBe('success');
78+
// Check that it will reload, transitioning to loading then to success again
79+
80+
fetch.mockClear();
81+
fetch.mockReturnValue(
82+
new Promise(resolve => {
83+
fetchResolve = resolve;
84+
})
85+
);
86+
await act(async () => {
87+
result.current.reload();
88+
});
89+
expect(fetch).toHaveBeenCalledTimes(1);
90+
expect(result.current.status).toBe('loading');
91+
fetchResolve(table);
92+
await waitForNextUpdate();
93+
expect(result.current.status).toBe('success');
94+
expect(
95+
(result.current as IrisGridModelFetchSuccessResult).model
96+
).toBeDefined();
97+
98+
// Now check that it will handle a failure on reload, transitioning from loading to failure
99+
fetch.mockClear();
100+
101+
let fetchReject;
102+
fetch.mockReturnValue(
103+
new Promise((resolve, reject) => {
104+
fetchReject = reject;
105+
})
106+
);
107+
await act(async () => {
108+
result.current.reload();
109+
});
110+
expect(fetch).toHaveBeenCalledTimes(1);
111+
expect(result.current.status).toBe('loading');
112+
const error = new Error('Test error');
113+
fetchReject(error);
114+
await waitForNextUpdate();
115+
expect(result.current.status).toBe('error');
116+
expect((result.current as IrisGridModelFetchErrorResult).error).toBe(error);
117+
118+
// Check that it will reload again after an error
119+
fetch.mockClear();
120+
fetch.mockReturnValue(
121+
new Promise(resolve => {
122+
fetchResolve = resolve;
123+
})
124+
);
125+
await act(async () => {
126+
result.current.reload();
127+
});
128+
expect(fetch).toHaveBeenCalledTimes(1);
129+
expect(result.current.status).toBe('loading');
130+
fetchResolve(table);
131+
await waitForNextUpdate();
132+
expect(result.current.status).toBe('success');
133+
expect(
134+
(result.current as IrisGridModelFetchSuccessResult).model
135+
).toBeDefined();
136+
});

0 commit comments

Comments
 (0)