Skip to content

Commit 545d2bc

Browse files
authored
feat: Add URI support and api associated with widget to useWidget (#2509)
Part of DH-19001. Tested with my VM `mattrunyon-gplus` which has the updated dh.ui and DHE web UI as well. Used the notebook w/ the ticket number as several examples, and the `Demo` and `DemoDashboard` queries w/ different users with different permissions to view those queries. Tested using tables/figures/pickers and restarting the queries to ensure they restarted and could continue being used. This is mostly type changes to augment the object fetcher stuff to accept a URI. Also adds the associated `api` to the `useWidget` hook needed for PQ URIs in DHE. Renamed `useTableClose` to `useWidgetClose` as a more generic util and deprecated `useTableClose`. They are the same, just more permissive types since this can apply to non-tables
1 parent 8407e4c commit 545d2bc

File tree

13 files changed

+182
-103
lines changed

13 files changed

+182
-103
lines changed

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
type ObjectFetchManager,
1212
ObjectFetchManagerContext,
1313
sanitizeVariableDescriptor,
14+
type UriVariableDescriptor,
1415
useApi,
1516
useClient,
1617
} from '@deephaven/jsapi-bootstrap';
@@ -168,8 +169,11 @@ export function ConnectionBootstrap({
168169
);
169170

170171
const objectFetcher = useCallback(
171-
async (descriptor: dh.ide.VariableDescriptor) => {
172+
async (descriptor: dh.ide.VariableDescriptor | UriVariableDescriptor) => {
172173
assertNotNull(connection, 'No connection available to fetch object with');
174+
if (typeof descriptor === 'string') {
175+
throw new Error('No URI resolvers available in Core');
176+
}
173177
return connection.getObject(sanitizeVariableDescriptor(descriptor));
174178
},
175179
[connection]

packages/jsapi-bootstrap/src/useDeferredApi.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import { createContext, useContext, useEffect, useState } from 'react';
22
import type { dh as DhType } from '@deephaven/jsapi-types';
33
import { ApiContext } from './ApiBootstrap';
4+
import { type UriVariableDescriptor } from './useObjectFetcher';
45

56
/**
67
* Function to fetch an API based on a provided descriptor object.
78
* Depending on the context there may be more properties on the descriptor,
89
* providing more information about the object, such as a session ID.
9-
* @param descriptor Descriptor object to fetch the API from.
10+
* @param descriptor Descriptor object or URI to fetch the API from.
1011
* @returns A promise that resolves to the API instance for the provided variable descriptor.
1112
*/
1213
export type DeferredApiFetcher = (
13-
descriptor: DhType.ide.VariableDescriptor
14+
descriptor: DhType.ide.VariableDescriptor | UriVariableDescriptor
1415
) => Promise<typeof DhType>;
1516

1617
export const DeferredApiContext = createContext<
@@ -20,14 +21,14 @@ export const DeferredApiContext = createContext<
2021
/**
2122
* Retrieve the API for the current context, given the widget provided.
2223
* The API may need to be loaded, and will return `null` until it is ready.
23-
* @param widget The widget descriptor to use to fetch the API
24+
* @param widget The widget descriptor or URI to use to fetch the API
2425
* @returns A tuple with the API instance, and an error if one occurred.
2526
*/
2627
export function useDeferredApi(
27-
widget: DhType.ide.VariableDescriptor | null
28-
): [dh: typeof DhType | null, error: unknown | null] {
28+
widget: DhType.ide.VariableDescriptor | UriVariableDescriptor | null
29+
): [dh: typeof DhType | null, error: NonNullable<unknown> | null] {
2930
const [api, setApi] = useState<typeof DhType | null>(null);
30-
const [error, setError] = useState<unknown | null>(null);
31+
const [error, setError] = useState<NonNullable<unknown> | null>(null);
3132
const deferredApi = useContext(DeferredApiContext);
3233
const contextApi = useContext(ApiContext);
3334

@@ -64,7 +65,7 @@ export function useDeferredApi(
6465
} catch (e) {
6566
if (!isCancelled) {
6667
setApi(null);
67-
setError(e);
68+
setError(e ?? new Error('Null error'));
6869
}
6970
}
7071
} else {

packages/jsapi-bootstrap/src/useObjectFetch.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createContext, useContext, useEffect, useState } from 'react';
22
import type { dh } from '@deephaven/jsapi-types';
3+
import { type UriVariableDescriptor } from './useObjectFetcher';
34

45
/** Function for unsubscribing from a given subscription */
56
export type UnsubscribeFunction = () => void;
@@ -42,12 +43,12 @@ export type ObjectFetchManager = {
4243
* Subscribe to the fetch function for an object using a variable descriptor.
4344
* It's possible that the fetch function changes over time, due to disconnection/reconnection, starting/stopping of applications that the object may be associated with, etc.
4445
*
45-
* @param descriptor Descriptor object of the object to fetch. Can be extended by a specific implementation to include more details necessary for the ObjectManager.
46+
* @param descriptor Descriptor object or URI of the object to fetch. Can be extended by a specific implementation to include more details necessary for the ObjectManager.
4647
* @param onUpdate Callback function to be called when the object is updated.
4748
* @returns An unsubscribe function to stop listening for fetch updates and clean up the object.
4849
*/
4950
subscribe: <T = unknown>(
50-
descriptor: dh.ide.VariableDescriptor,
51+
descriptor: dh.ide.VariableDescriptor | UriVariableDescriptor,
5152
onUpdate: ObjectFetchUpdateCallback<T>
5253
) => UnsubscribeFunction;
5354
};
@@ -59,12 +60,12 @@ export const ObjectFetchManagerContext =
5960
/**
6061
* Retrieve a `fetch` function for the given variable descriptor.
6162
*
62-
* @param descriptor Descriptor to get the `fetch` function for
63+
* @param descriptor Descriptor or URI to get the `fetch` function for
6364
* @returns An object with the current `fetch` function, OR an error status set if there was an issue fetching the object.
6465
* Retrying is left up to the ObjectManager implementation used from this context.
6566
*/
6667
export function useObjectFetch<T = unknown>(
67-
descriptor: dh.ide.VariableDescriptor
68+
descriptor: dh.ide.VariableDescriptor | UriVariableDescriptor
6869
): ObjectFetchUpdate<T> {
6970
const [currentUpdate, setCurrentUpdate] = useState<ObjectFetchUpdate<T>>({
7071
status: 'loading',

packages/jsapi-bootstrap/src/useObjectFetcher.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export type IdVariableDescriptor = {
2222
id: string;
2323
};
2424

25+
export type UriVariableDescriptor = string;
26+
2527
export function isNameVariableDescriptor(
2628
value: unknown
2729
): value is NameVariableDescriptor {
@@ -52,11 +54,11 @@ export function isVariableDescriptor(
5254

5355
/**
5456
* Function to fetch an object based on a provided descriptor object.
55-
* @param descriptor Descriptor object to fetch the object from. Can be extended by a specific implementation to
56-
* include additional fields (such as a session ID) to uniquely identify an object.
57+
* @param descriptor Descriptor object or URI to fetch the object from. Can be extended by a specific implementation to
58+
* include additional fields (such as a session ID) to uniquely identify an object.
5759
*/
5860
export type ObjectFetcher = <T = unknown>(
59-
descriptor: dh.ide.VariableDescriptor
61+
descriptor: dh.ide.VariableDescriptor | UriVariableDescriptor
6062
) => Promise<T>;
6163

6264
export const ObjectFetcherContext = createContext<ObjectFetcher | null>(null);

packages/jsapi-bootstrap/src/useWidget.test.tsx

Lines changed: 80 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,37 @@ import { act, renderHook } from '@testing-library/react-hooks';
33
import { type dh } from '@deephaven/jsapi-types';
44
import { TestUtils } from '@deephaven/test-utils';
55
import { useWidget } from './useWidget';
6-
import { ObjectFetchManagerContext } from './useObjectFetch';
6+
import {
7+
type ObjectFetchManager,
8+
ObjectFetchManagerContext,
9+
} from './useObjectFetch';
10+
import { ApiContext } from './ApiBootstrap';
11+
import { DeferredApiContext } from './useDeferredApi';
712

813
const WIDGET_TYPE = 'OtherWidget';
914

15+
function makeWrapper(
16+
objectManager: ObjectFetchManager,
17+
api = jest.fn(),
18+
deferredApi?: jest.Mock
19+
) {
20+
return function Wrapper({ children }: React.PropsWithChildren<never>) {
21+
return (
22+
<ObjectFetchManagerContext.Provider value={objectManager}>
23+
<ApiContext.Provider value={api as unknown as typeof dh}>
24+
{deferredApi ? (
25+
<DeferredApiContext.Provider value={deferredApi}>
26+
{children}
27+
</DeferredApiContext.Provider>
28+
) : (
29+
children
30+
)}
31+
</ApiContext.Provider>
32+
</ObjectFetchManagerContext.Provider>
33+
);
34+
};
35+
}
36+
1037
describe('useWidget', () => {
1138
it('should return a widget when available', async () => {
1239
const descriptor: dh.ide.VariableDescriptor = {
@@ -22,14 +49,45 @@ describe('useWidget', () => {
2249
return jest.fn();
2350
});
2451
const objectManager = { subscribe };
25-
const wrapper = ({ children }) => (
26-
<ObjectFetchManagerContext.Provider value={objectManager}>
27-
{children}
28-
</ObjectFetchManagerContext.Provider>
52+
const api = jest.fn();
53+
const wrapper = makeWrapper(objectManager, api);
54+
const { result } = renderHook(() => useWidget(descriptor), { wrapper });
55+
await act(TestUtils.flushPromises);
56+
expect(result.current).toEqual({
57+
widget,
58+
error: null,
59+
api,
60+
});
61+
expect(fetch).toHaveBeenCalledTimes(1);
62+
});
63+
64+
it('should use the deferred API if available', async () => {
65+
const descriptor: dh.ide.VariableDescriptor = {
66+
type: 'OtherWidget',
67+
name: 'name',
68+
};
69+
const widget = { close: jest.fn() };
70+
const fetch = jest.fn(async () => widget);
71+
const objectFetch = { fetch, error: null };
72+
const subscribe = jest.fn((subscribeDescriptor, onUpdate) => {
73+
expect(subscribeDescriptor).toEqual(descriptor);
74+
onUpdate(objectFetch);
75+
return jest.fn();
76+
});
77+
const objectManager = { subscribe };
78+
const deferredApi = {};
79+
const wrapper = makeWrapper(
80+
objectManager,
81+
jest.fn(),
82+
jest.fn(() => deferredApi)
2983
);
3084
const { result } = renderHook(() => useWidget(descriptor), { wrapper });
3185
await act(TestUtils.flushPromises);
32-
expect(result.current).toEqual({ widget, error: null });
86+
expect(result.current).toEqual({
87+
widget,
88+
error: null,
89+
api: deferredApi,
90+
});
3391
expect(fetch).toHaveBeenCalledTimes(1);
3492
});
3593

@@ -46,15 +104,11 @@ describe('useWidget', () => {
46104
return jest.fn();
47105
});
48106
const objectManager = { subscribe };
49-
const wrapper = ({ children }) => (
50-
<ObjectFetchManagerContext.Provider value={objectManager}>
51-
{children}
52-
</ObjectFetchManagerContext.Provider>
53-
);
107+
const wrapper = makeWrapper(objectManager);
54108

55109
const { result } = renderHook(() => useWidget(descriptor), { wrapper });
56110

57-
expect(result.current).toEqual({ widget: null, error });
111+
expect(result.current).toEqual({ widget: null, error, api: null });
58112
});
59113

60114
it('should return null when still loading', () => {
@@ -65,14 +119,10 @@ describe('useWidget', () => {
65119
return jest.fn();
66120
});
67121
const objectManager = { subscribe };
68-
const wrapper = ({ children }) => (
69-
<ObjectFetchManagerContext.Provider value={objectManager}>
70-
{children}
71-
</ObjectFetchManagerContext.Provider>
72-
);
122+
const wrapper = makeWrapper(objectManager);
73123
const { result } = renderHook(() => useWidget(descriptor), { wrapper });
74124

75-
expect(result.current).toEqual({ widget: null, error: null });
125+
expect(result.current).toEqual({ widget: null, error: null, api: null });
76126
});
77127

78128
it('should close the widget and exported objects when cancelled', async () => {
@@ -95,19 +145,15 @@ describe('useWidget', () => {
95145
return jest.fn();
96146
});
97147
const objectManager = { subscribe };
98-
const wrapper = ({ children }) => (
99-
<ObjectFetchManagerContext.Provider value={objectManager}>
100-
{children}
101-
</ObjectFetchManagerContext.Provider>
102-
);
148+
const wrapper = makeWrapper(objectManager);
103149
const { result, unmount } = renderHook(() => useWidget(descriptor), {
104150
wrapper,
105151
});
106152
expect(widget.close).not.toHaveBeenCalled();
107153
expect(widget.exportedObjects[0].close).not.toHaveBeenCalled();
108154
expect(widget.exportedObjects[1].close).not.toHaveBeenCalled();
109155

110-
expect(result.current).toEqual({ widget: null, error: null });
156+
expect(result.current).toEqual({ widget: null, error: null, api: null });
111157

112158
// Unmount before flushing the promise
113159
unmount();
@@ -137,16 +183,12 @@ describe('useWidget', () => {
137183
return jest.fn();
138184
});
139185
const objectManager = { subscribe };
140-
const wrapper = ({ children }) => (
141-
<ObjectFetchManagerContext.Provider value={objectManager}>
142-
{children}
143-
</ObjectFetchManagerContext.Provider>
144-
);
186+
const wrapper = makeWrapper(objectManager);
145187
const { result, unmount } = renderHook(() => useWidget(descriptor), {
146188
wrapper,
147189
});
148190

149-
expect(result.current).toEqual({ widget: null, error: null });
191+
expect(result.current).toEqual({ widget: null, error: null, api: null });
150192
await act(TestUtils.flushPromises);
151193
unmount();
152194
expect(widget.close).not.toHaveBeenCalled();
@@ -168,16 +210,17 @@ describe('useWidget', () => {
168210
return jest.fn();
169211
});
170212
const objectManager = { subscribe };
171-
const wrapper = ({ children }) => (
172-
<ObjectFetchManagerContext.Provider value={objectManager}>
173-
{children}
174-
</ObjectFetchManagerContext.Provider>
175-
);
213+
const api = jest.fn();
214+
const wrapper = makeWrapper(objectManager, api);
176215
const { result, unmount } = renderHook(() => useWidget(descriptor), {
177216
wrapper,
178217
});
179218
await act(TestUtils.flushPromises);
180-
expect(result.current).toEqual({ widget: table, error: null });
219+
expect(result.current).toEqual({
220+
widget: table,
221+
error: null,
222+
api,
223+
});
181224
expect(fetch).toHaveBeenCalledTimes(1);
182225
unmount();
183226

@@ -199,11 +242,7 @@ describe('useWidget', () => {
199242
return jest.fn();
200243
});
201244
const objectManager = { subscribe };
202-
const wrapper = ({ children }) => (
203-
<ObjectFetchManagerContext.Provider value={objectManager}>
204-
{children}
205-
</ObjectFetchManagerContext.Provider>
206-
);
245+
const wrapper = makeWrapper(objectManager);
207246
const { unmount } = renderHook(() => useWidget(descriptor), { wrapper });
208247

209248
unmount();

0 commit comments

Comments
 (0)