Skip to content

Commit d8db9ca

Browse files
authored
feat: usePanelRegistration hook (#1208)
resolves #1207
1 parent 59d3df4 commit d8db9ca

4 files changed

Lines changed: 176 additions & 2 deletions

File tree

packages/dashboard/src/DashboardPlugin.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { Component, ComponentType } from 'react';
1+
import {
2+
Component,
3+
ComponentType,
4+
ForwardRefExoticComponent,
5+
PropsWithoutRef,
6+
RefAttributes,
7+
} from 'react';
28
import { ConnectedComponent } from 'react-redux';
39
import GoldenLayout from '@deephaven/golden-layout';
410
import type {
@@ -8,6 +14,34 @@ import type {
814
} from '@deephaven/golden-layout';
915
import PanelManager from './PanelManager';
1016

17+
/**
18+
* Alias for the return type of React.forwardRef()
19+
*/
20+
export type ForwardRefComponentType<P, R> = ForwardRefExoticComponent<
21+
PropsWithoutRef<P> & RefAttributes<R>
22+
>;
23+
24+
/**
25+
* Panel components can provide static props that provide meta data about the
26+
* panel.
27+
*/
28+
export interface PanelStaticMetaData {
29+
/**
30+
* Should be set to the same name as the component type.
31+
* @deprecated Use `displayName` instead.
32+
*/
33+
COMPONENT?: string;
34+
35+
/** Title of the panel. */
36+
TITLE?: string;
37+
}
38+
39+
/**
40+
* Panels defined as functional components have to use React.forwardRef.
41+
*/
42+
export type PanelFunctionComponentType<P, R> = ForwardRefComponentType<P, R> &
43+
PanelStaticMetaData;
44+
1145
export type WrappedComponentType<
1246
P extends PanelProps,
1347
C extends ComponentType<P>
@@ -16,7 +50,12 @@ export type WrappedComponentType<
1650
export type PanelComponentType<
1751
P extends PanelProps = PanelProps,
1852
C extends ComponentType<P> = ComponentType<P>
19-
> = ComponentType<P> | WrappedComponentType<P, C>;
53+
> = (
54+
| ComponentType<P>
55+
| WrappedComponentType<P, C>
56+
| PanelFunctionComponentType<P, unknown>
57+
) &
58+
PanelStaticMetaData;
2059

2160
export function isWrappedComponent<
2261
P extends PanelProps,

packages/dashboard/src/layout/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export { default as LayoutUtils } from './LayoutUtils';
33
export { default as GLPropTypes } from './GLPropTypes';
44

55
export * from './hooks';
6+
export { default as usePanelRegistration } from './usePanelRegistration';
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/* eslint-disable max-classes-per-file */
2+
import React from 'react';
3+
import { renderHook } from '@testing-library/react-hooks';
4+
import usePanelRegistration from './usePanelRegistration';
5+
import { PanelProps } from '../DashboardPlugin';
6+
7+
/* eslint-disable react/prefer-stateless-function */
8+
class ClassCOMPONENT extends React.Component<PanelProps> {
9+
static COMPONENT = 'ClassCOMPONENT';
10+
}
11+
class ClassDisplayName extends React.Component<PanelProps> {
12+
static displayName = 'ClassDisplayName';
13+
}
14+
class ClassNoName extends React.Component<PanelProps> {}
15+
/* eslint-enable react/prefer-stateless-function */
16+
17+
function FnCOMPONENT() {
18+
return null;
19+
}
20+
FnCOMPONENT.COMPONENT = 'FnCOMPONENT';
21+
function FnDisplayName() {
22+
return null;
23+
}
24+
FnDisplayName.displayName = 'FnDisplayName';
25+
function FnNoName() {
26+
return null;
27+
}
28+
29+
const deregister = jest.fn();
30+
const registerComponent = jest.fn();
31+
const hydrate = jest.fn();
32+
const dehydrate = jest.fn();
33+
34+
beforeEach(() => {
35+
jest.clearAllMocks();
36+
registerComponent.mockReturnValue(deregister);
37+
});
38+
39+
it.each([
40+
[ClassCOMPONENT.COMPONENT, ClassCOMPONENT],
41+
[ClassDisplayName.displayName, ClassDisplayName],
42+
[FnCOMPONENT.COMPONENT, FnCOMPONENT],
43+
[FnDisplayName.displayName, FnDisplayName],
44+
])(
45+
'should register components with COMPONENT or displayName attributes and deregister on unmount: "%s"',
46+
(_label, ComponentType) => {
47+
const { unmount } = renderHook(() =>
48+
usePanelRegistration(registerComponent, ComponentType, hydrate, dehydrate)
49+
);
50+
51+
const name =
52+
'COMPONENT' in ComponentType
53+
? ComponentType.COMPONENT
54+
: ComponentType.displayName;
55+
56+
expect(registerComponent).toHaveBeenCalledWith(
57+
name,
58+
ComponentType,
59+
hydrate,
60+
dehydrate
61+
);
62+
63+
expect(deregister).not.toHaveBeenCalled();
64+
65+
unmount();
66+
67+
expect(deregister).toHaveBeenCalled();
68+
}
69+
);
70+
71+
it.each([
72+
['MockClassNoName', ClassNoName],
73+
['MockFnNoName', FnNoName],
74+
])(
75+
'should throw an error if no COMPONENT or displayName attribute exists: "%s"',
76+
(_label, ComponentType) => {
77+
const { result } = renderHook(() =>
78+
usePanelRegistration(registerComponent, ComponentType, hydrate, dehydrate)
79+
);
80+
81+
expect(result.error).toEqual(
82+
new Error(
83+
'ComponentType must have a `COMPONENT` or `displayName` attribute.'
84+
)
85+
);
86+
}
87+
);
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React from 'react';
2+
import {
3+
DashboardPluginComponentProps,
4+
PanelComponentType,
5+
PanelDehydrateFunction,
6+
PanelHydrateFunction,
7+
PanelProps,
8+
} from '../DashboardPlugin';
9+
10+
/**
11+
* Registers a given panel component. Also runs a `useEffect` that will
12+
* automatically de-register then panel on unmount.
13+
* @param registerComponent
14+
* @param ComponentType
15+
* @param hydrate
16+
* @param dehydrate
17+
*/
18+
export default function usePanelRegistration<
19+
P extends PanelProps,
20+
C extends React.ComponentType<P>
21+
>(
22+
registerComponent: DashboardPluginComponentProps['registerComponent'],
23+
ComponentType: PanelComponentType<P, C>,
24+
hydrate?: PanelHydrateFunction<P>,
25+
dehydrate?: PanelDehydrateFunction
26+
) {
27+
const name = ComponentType.COMPONENT ?? ComponentType.displayName;
28+
29+
if (name == null) {
30+
throw new Error(
31+
'ComponentType must have a `COMPONENT` or `displayName` attribute.'
32+
);
33+
}
34+
35+
React.useEffect(() => {
36+
const deregister = registerComponent(
37+
name,
38+
ComponentType,
39+
hydrate,
40+
dehydrate
41+
);
42+
43+
return () => {
44+
deregister();
45+
};
46+
}, [ComponentType, dehydrate, hydrate, name, registerComponent]);
47+
}

0 commit comments

Comments
 (0)