Skip to content
Merged
24 changes: 21 additions & 3 deletions packages/app-utils/src/components/ConnectionBootstrap.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { LoadingOverlay } from '@deephaven/components';
import { useApi, useClient } from '@deephaven/jsapi-bootstrap';
import {
getVariableDefinition,
ObjectFetcherContext,
ObjectMetadata,
useApi,
useClient,
} from '@deephaven/jsapi-bootstrap';
import type { IdeConnection } from '@deephaven/jsapi-types';
import { ConnectionContext } from '@deephaven/jsapi-components';
import Log from '@deephaven/log';
import { assertNotNull } from '@deephaven/utils';

const log = Log.module('@deephaven/app-utils.ConnectionBootstrap');

Expand Down Expand Up @@ -69,6 +76,15 @@ export function ConnectionBootstrap({
[api, connection]
);

const objectFetcher = useCallback(
async (metadata: ObjectMetadata) => {
assertNotNull(connection, 'connection');
Comment thread
mofojed marked this conversation as resolved.
Outdated
const widget = getVariableDefinition(metadata);
return connection.getObject(widget);
},
[connection]
);

if (connection == null || error != null) {
return (
<LoadingOverlay
Expand All @@ -81,7 +97,9 @@ export function ConnectionBootstrap({

return (
<ConnectionContext.Provider value={connection}>
{children}
<ObjectFetcherContext.Provider value={objectFetcher}>
{children}
</ObjectFetcherContext.Provider>
</ConnectionContext.Provider>
);
}
Expand Down
2 changes: 2 additions & 0 deletions packages/code-studio/src/main/AppMainContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import {
dhSquareFilled,
vsHome,
} from '@deephaven/icons';
import { getObjectMetadata } from '@deephaven/jsapi-bootstrap';
import dh from '@deephaven/jsapi-shim';
import type {
IdeConnection,
Expand Down Expand Up @@ -723,6 +724,7 @@ export class AppMainContainer extends Component<
this.emitLayoutEvent(PanelEvent.OPEN, {
dragEvent,
fetch: async () => connection?.getObject(widget),
metadata: getObjectMetadata(widget),
widget,
});
}
Expand Down
23 changes: 13 additions & 10 deletions packages/dashboard-core-plugins/src/panels/ConsolePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import {
LayoutUtils,
PanelEvent,
} from '@deephaven/dashboard';
import type { IdeSession, VariableDefinition } from '@deephaven/jsapi-types';
import { getObjectMetadata } from '@deephaven/jsapi-bootstrap';
import type { VariableDefinition } from '@deephaven/jsapi-types';
import { SessionWrapper } from '@deephaven/jsapi-utils';
import Log from '@deephaven/log';
import {
Expand Down Expand Up @@ -248,11 +249,6 @@ export class ConsolePanel extends PureComponent<
}

handleOpenObject(object: VariableDefinition, forceOpen = true): void {
const { sessionWrapper } = this.props;
if (sessionWrapper == null) {
return;
}
const { session } = sessionWrapper;
const { root } = this.context;
const oldPanelId =
object.title != null ? this.getItemId(object.title, false) : null;
Expand All @@ -267,7 +263,7 @@ export class ConsolePanel extends PureComponent<
false
) != null)
) {
this.openWidget(object, session);
this.openWidget(object);
}
}

Expand All @@ -293,15 +289,22 @@ export class ConsolePanel extends PureComponent<

/**
* @param widget The widget to open
* @param session The session object
*/
openWidget(widget: VariableDefinition, session: IdeSession): void {
const { glEventHub } = this.props;
openWidget(widget: VariableDefinition): void {
const { glEventHub, sessionWrapper } = this.props;
assertNotNull(sessionWrapper);

const { config, session } = sessionWrapper;
const { title } = widget;
assertNotNull(title);
const panelId = this.getItemId(title);
const metadata = {
...getObjectMetadata(widget),
sessionId: config.id,
};
const openOptions = {
fetch: () => session.getObject(widget),
metadata,
panelId,
widget,
};
Expand Down
10 changes: 5 additions & 5 deletions packages/jsapi-bootstrap/src/DeferredApiBootstrap.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import { DeferredApiContext } from './useDeferredApi';

it('should call the error callback if no API provider wrapped', () => {
const onError = jest.fn();
render(<DeferredApiBootstrap onError={onError} />);
render(<DeferredApiBootstrap onError={onError} metadata={{}} />);
expect(onError).toHaveBeenCalled();
});

it('renders children if the API is loaded', () => {
const api = TestUtils.createMockProxy<DhType>();
const { queryByText } = render(
<DeferredApiContext.Provider value={api}>
<DeferredApiBootstrap>
<DeferredApiBootstrap metadata={{}}>
<div>Child</div>
</DeferredApiBootstrap>
</DeferredApiContext.Provider>
Expand All @@ -29,17 +29,17 @@ it('waits to render children until the API is loaded', async () => {
resolveApi = resolve;
});
const deferredApi = jest.fn(() => apiPromise);
const options = { foo: 'bar' };
const metadata = { foo: 'bar' };
const { queryByText } = render(
<DeferredApiContext.Provider value={deferredApi}>
<DeferredApiBootstrap options={options}>
<DeferredApiBootstrap metadata={metadata}>
<div>Child</div>
</DeferredApiBootstrap>
</DeferredApiContext.Provider>
);
expect(queryByText('Child')).toBeNull();
expect(deferredApi).toHaveBeenCalledTimes(1);
expect(deferredApi).toHaveBeenCalledWith(options);
expect(deferredApi).toHaveBeenCalledWith(metadata);

const api = TestUtils.createMockProxy<DhType>();
await act(async () => {
Expand Down
7 changes: 4 additions & 3 deletions packages/jsapi-bootstrap/src/DeferredApiBootstrap.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import React from 'react';
import useDeferredApi from './useDeferredApi';
import { ApiContext } from './ApiBootstrap';
import { ObjectMetadata } from './useObjectFetcher';

type DeferredApiBootstrapProps = React.PropsWithChildren<{
onError?: (error: unknown) => void;
/**
* Options to use when fetching the deferred API.
*/
options?: Record<string, unknown>;
metadata: ObjectMetadata;
}>;

/**
Expand All @@ -17,9 +18,9 @@ export const DeferredApiBootstrap = React.memo(
({
children,
onError,
options,
metadata,
}: DeferredApiBootstrapProps): JSX.Element | null => {
const [api, apiError] = useDeferredApi(options);
const [api, apiError] = useDeferredApi(metadata);
if (apiError != null) {
onError?.(apiError);
return null;
Expand Down
1 change: 1 addition & 0 deletions packages/jsapi-bootstrap/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './DeferredApiBootstrap';
export * from './useApi';
export * from './useClient';
export * from './useDeferredApi';
export * from './useObjectFetcher';
24 changes: 13 additions & 11 deletions packages/jsapi-bootstrap/src/useDeferredApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { act, renderHook } from '@testing-library/react-hooks';
import type { dh as DhType } from '@deephaven/jsapi-types';
import { useContext } from 'react';
import { TestUtils } from '@deephaven/utils';
import { DeferredApiOptions, useDeferredApi } from './useDeferredApi';
import { useDeferredApi } from './useDeferredApi';
import { ObjectMetadata } from './useObjectFetcher';

const { asMock, createMockProxy, flushPromises } = TestUtils;

const dh1 = createMockProxy<DhType>();
const dh2 = createMockProxy<DhType>();
const objectMetadata = { type: 'TEST_TYPE' };

jest.mock('react', () => ({
...jest.requireActual('react'),
Expand All @@ -19,22 +21,22 @@ beforeEach(() => {
asMock(useContext).mockName('useContext');
});

describe('useApi', () => {
describe('useDeferredApi', () => {
it('should return API directly if a value is provided from useContext, whatever the options are', () => {
asMock(useContext).mockReturnValue(dh1);

const { result } = renderHook(() => useDeferredApi());
const { result } = renderHook(() => useDeferredApi(objectMetadata));
expect(result.current).toEqual([dh1, null]);

const { result: result2 } = renderHook(() =>
useDeferredApi({ foo: 'bar' })
useDeferredApi({ type: 'foo', foo: 'bar' })
);
expect(result2.current).toEqual([dh1, null]);
});

it('should resolve to the API value when it is provided from the function', async () => {
asMock(useContext).mockReturnValue(async (options?: DeferredApiOptions) => {
switch (options?.id) {
asMock(useContext).mockReturnValue(async (metadata: ObjectMetadata) => {
switch (metadata.type) {
case '1':
return dh1;
case '2':
Expand All @@ -45,25 +47,25 @@ describe('useApi', () => {
});

const { rerender, result } = renderHook(
(options?: DeferredApiOptions) => useDeferredApi(options),
{ initialProps: { id: '1' } }
(metadata: ObjectMetadata) => useDeferredApi(metadata),
{ initialProps: { type: '1' } }
);
await act(flushPromises);
expect(result.current).toEqual([dh1, null]);

rerender({ id: '2' });
rerender({ type: '2' });
await act(flushPromises);
expect(result.current).toEqual([dh2, null]);

rerender({ id: '3' });
rerender({ type: '3' });
await act(flushPromises);
expect(result.current).toEqual([null, expect.any(Error)]);
});

it('returns an error if the context is null', async () => {
asMock(useContext).mockReturnValue(null);

const { result } = renderHook(() => useDeferredApi());
const { result } = renderHook(() => useDeferredApi(objectMetadata));
expect(result.current).toEqual([null, expect.any(Error)]);
});
});
17 changes: 7 additions & 10 deletions packages/jsapi-bootstrap/src/useDeferredApi.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
import { createContext, useContext, useEffect, useState } from 'react';
import type { dh as DhType } from '@deephaven/jsapi-types';
import { ApiContext } from './ApiBootstrap';
import { ObjectMetadata } from './useObjectFetcher';

/** Options for retrieving the deferred */
export type DeferredApiOptions = Record<string, unknown>;

export type DeferredApiFetcher = (
options?: DeferredApiOptions
) => Promise<DhType>;
export type DeferredApiFetcher = (metadata?: ObjectMetadata) => Promise<DhType>;

export const DeferredApiContext = createContext<
DhType | DeferredApiFetcher | null
>(null);

/**
* Retrieve the API for the current context, given the metadata provided.
* Retrieve the API for the current context, given the object metadata provided.
* The API may need to be loaded, and will return `null` until it is ready.
* @param metadata The object metadata to use to fetch the API
* @returns A tuple with the API instance, and an error if one occurred.
*/
export function useDeferredApi(
options?: Record<string, unknown>
metadata: ObjectMetadata
): [DhType | null, unknown | null] {
const [api, setApi] = useState<DhType | null>(null);
const [error, setError] = useState<unknown | null>(null);
Expand All @@ -46,7 +43,7 @@ export function useDeferredApi(
async function loadApi() {
if (typeof deferredApi === 'function') {
try {
const newApi = await deferredApi(options);
const newApi = await deferredApi(metadata);
if (!isCancelled) {
setApi(newApi);
setError(null);
Expand All @@ -67,7 +64,7 @@ export function useDeferredApi(
return () => {
isCancelled = true;
};
}, [contextApi, deferredApi, options]);
}, [contextApi, deferredApi, metadata]);

return [api, error];
}
Expand Down
34 changes: 34 additions & 0 deletions packages/jsapi-bootstrap/src/useObjectFetcher.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { act, renderHook } from '@testing-library/react-hooks';
import { useContext } from 'react';
import { TestUtils } from '@deephaven/utils';
import { useObjectFetcher } from './useObjectFetcher';

const { asMock, flushPromises } = TestUtils;

jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn(),
}));

beforeEach(() => {
jest.clearAllMocks();
asMock(useContext).mockName('useContext');
});

it('should resolve the fetcher when set in the context', async () => {
const fetcher = jest.fn(async () => undefined);
asMock(useContext).mockReturnValue(fetcher);

const { result } = renderHook(() => useObjectFetcher());
await act(flushPromises);
expect(result.current).toEqual(fetcher);
expect(result.error).toBeUndefined();
expect(fetcher).not.toHaveBeenCalled();
});

it('throws an error if the context is null', async () => {
Comment thread
mofojed marked this conversation as resolved.
asMock(useContext).mockReturnValue(null);

const { result } = renderHook(() => useObjectFetcher());
expect(result.error).not.toBeNull();
});
Loading