Skip to content

Commit f006bca

Browse files
authored
feat: Add usePersistentState implementation to GridWidgetPlugin (#2427)
Part of DH-19000. This adds the `GridWidgetPlugin` implementation of `usePersistentState`. It will effectively do nothing right now since the state will always be `undefined` to start. Once the dh.ui side of this merges, then normal tables in a `dh.ui` panel will persist their state
1 parent 728ad95 commit f006bca

4 files changed

Lines changed: 162 additions & 16 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from 'react';
2+
import { Provider } from 'react-redux';
3+
import { render, waitFor } from '@testing-library/react';
4+
import { ApiContext } from '@deephaven/jsapi-bootstrap';
5+
import { TestUtils } from '@deephaven/test-utils';
6+
import type { dh as DhType } from '@deephaven/jsapi-types';
7+
import { createMockStore } from '@deephaven/redux';
8+
import dh from '@deephaven/jsapi-shim';
9+
import GridWidgetPlugin from './GridWidgetPlugin';
10+
11+
const MockIrisGrid: React.FC & jest.Mock = jest.fn(() => (
12+
<div>MockIrisGrid</div>
13+
));
14+
15+
jest.mock('@deephaven/iris-grid', () => {
16+
const { forwardRef } = jest.requireActual('react');
17+
return {
18+
...(jest.requireActual('@deephaven/iris-grid') as Record<string, unknown>),
19+
// eslint-disable-next-line react/jsx-props-no-spreading
20+
IrisGrid: forwardRef((props, ref) => <MockIrisGrid {...props} />),
21+
};
22+
});
23+
24+
it('mounts without crashing', async () => {
25+
const table = TestUtils.createMockProxy<DhType.Table>();
26+
const fetch = jest.fn(() => Promise.resolve(table));
27+
28+
const store = createMockStore();
29+
30+
const { container, queryByText } = render(
31+
<Provider store={store}>
32+
<ApiContext.Provider value={dh}>
33+
<GridWidgetPlugin fetch={fetch} />
34+
</ApiContext.Provider>
35+
</Provider>
36+
);
37+
38+
expect(queryByText('MockIrisGrid')).not.toBeInTheDocument();
39+
40+
await waitFor(() =>
41+
expect(
42+
container.querySelector('[role=progressbar].loading-spinner-large')
43+
).toBeInTheDocument()
44+
);
45+
46+
await waitFor(() =>
47+
expect(
48+
container.querySelector('[role=progressbar].loading-spinner-large')
49+
).not.toBeInTheDocument()
50+
);
51+
52+
expect(queryByText('MockIrisGrid')).toBeInTheDocument();
53+
});

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

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,87 @@
1-
import { type WidgetComponentProps } from '@deephaven/plugin';
2-
import { type dh } from '@deephaven/jsapi-types';
3-
import { IrisGrid } from '@deephaven/iris-grid';
1+
import { useCallback, useMemo, useRef } from 'react';
2+
import {
3+
type WidgetComponentProps,
4+
usePersistentState,
5+
} from '@deephaven/plugin';
6+
import { type dh as DhType } from '@deephaven/jsapi-types';
7+
import {
8+
type DehydratedGridState,
9+
type DehydratedIrisGridState,
10+
IrisGrid,
11+
IrisGridCacheUtils,
12+
type IrisGridState,
13+
IrisGridUtils,
14+
} from '@deephaven/iris-grid';
415
import { useSelector } from 'react-redux';
516
import { getSettings, type RootState } from '@deephaven/redux';
617
import { LoadingOverlay } from '@deephaven/components';
718
import { getErrorMessage } from '@deephaven/utils';
19+
import { useApi } from '@deephaven/jsapi-bootstrap';
20+
import { type GridState } from '@deephaven/grid';
821
import { useIrisGridModel } from './useIrisGridModel';
922

1023
export function GridWidgetPlugin({
1124
fetch,
12-
}: WidgetComponentProps<dh.Table>): JSX.Element | null {
25+
}: WidgetComponentProps<DhType.Table>): JSX.Element | null {
1326
const settings = useSelector(getSettings<RootState>);
1427

1528
const fetchResult = useIrisGridModel(fetch);
1629

30+
const dh = useApi();
31+
const irisGridUtils = useMemo(() => new IrisGridUtils(dh), [dh]);
32+
33+
const [state, setState] = usePersistentState<
34+
(DehydratedIrisGridState & DehydratedGridState) | undefined
35+
>(undefined, {
36+
version: 1,
37+
type: 'GridWidgetPlugin',
38+
});
39+
const initialState = useRef(state);
40+
const hydratedState = useMemo(() => {
41+
if (
42+
fetchResult.status !== 'success' ||
43+
initialState.current === undefined
44+
) {
45+
return;
46+
}
47+
return {
48+
...irisGridUtils.hydrateIrisGridState(
49+
fetchResult.model,
50+
initialState.current
51+
),
52+
...IrisGridUtils.hydrateGridState(
53+
fetchResult.model,
54+
initialState.current
55+
),
56+
};
57+
}, [fetchResult, irisGridUtils]);
58+
59+
const dehydrateIrisGridState = useMemo(
60+
() => IrisGridCacheUtils.makeMemoizedCombinedGridStateDehydrator(),
61+
[]
62+
);
63+
64+
const handleIrisGridChange = useCallback(
65+
(irisGridState: IrisGridState, gridState: GridState) => {
66+
if (
67+
fetchResult.status !== 'success' ||
68+
irisGridState == null ||
69+
gridState == null
70+
) {
71+
return;
72+
}
73+
74+
const newState = dehydrateIrisGridState(
75+
fetchResult.model,
76+
irisGridState,
77+
gridState
78+
);
79+
80+
setState(newState);
81+
},
82+
[fetchResult, setState, dehydrateIrisGridState]
83+
);
84+
1785
if (fetchResult.status === 'loading') {
1886
return <LoadingOverlay isLoading />;
1987
}
@@ -28,7 +96,15 @@ export function GridWidgetPlugin({
2896
}
2997

3098
const { model } = fetchResult;
31-
return <IrisGrid model={model} settings={settings} />;
99+
return (
100+
<IrisGrid
101+
model={model}
102+
settings={settings}
103+
onStateChange={handleIrisGridChange}
104+
// eslint-disable-next-line react/jsx-props-no-spreading
105+
{...hydratedState}
106+
/>
107+
);
32108
}
33109

34110
export default GridWidgetPlugin;

packages/dashboard-core-plugins/src/useIrisGridModel.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,16 @@ it('should reload the model on reload', async () => {
134134
(result.current as IrisGridModelFetchSuccessResult).model
135135
).toBeDefined();
136136
});
137+
138+
it('should return a memoized object for repeated calls', async () => {
139+
const table = TestUtils.createMockProxy<dh.Table>();
140+
const fetch = jest.fn(() => Promise.resolve(table));
141+
const { rerender, result, waitForNextUpdate } = renderHook(() =>
142+
useIrisGridModel(fetch)
143+
);
144+
await waitForNextUpdate();
145+
const fetchResult = result.current;
146+
expect(fetchResult.status).toBe('success');
147+
rerender(fetch);
148+
expect(result.current).toBe(fetchResult);
149+
});

packages/dashboard-core-plugins/src/useIrisGridModel.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type dh } from '@deephaven/jsapi-types';
22
import { useApi } from '@deephaven/jsapi-bootstrap';
33
import { IrisGridModel, IrisGridModelFactory } from '@deephaven/iris-grid';
4-
import { useCallback, useEffect, useState } from 'react';
4+
import { useCallback, useEffect, useMemo, useState } from 'react';
55

66
export type IrisGridModelFetch = () => Promise<dh.Table>;
77

@@ -106,14 +106,18 @@ export function useIrisGridModel(
106106
[model]
107107
);
108108

109-
if (isLoading) {
110-
return { reload, status: 'loading' };
111-
}
112-
if (error != null) {
113-
return { error, reload, status: 'error' };
114-
}
115-
if (model != null) {
116-
return { model, reload, status: 'success' };
117-
}
118-
throw new Error('Invalid state');
109+
const result: IrisGridModelFetchResult = useMemo(() => {
110+
if (isLoading) {
111+
return { reload, status: 'loading' };
112+
}
113+
if (error != null) {
114+
return { error, reload, status: 'error' };
115+
}
116+
if (model != null) {
117+
return { model, reload, status: 'success' };
118+
}
119+
throw new Error('Invalid state');
120+
}, [error, isLoading, model, reload]);
121+
122+
return result;
119123
}

0 commit comments

Comments
 (0)