Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions packages/dashboard/src/DashboardPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { Component, ComponentType } from 'react';
import {
Component,
ComponentType,
ForwardRefExoticComponent,
PropsWithoutRef,
RefAttributes,
} from 'react';
import { ConnectedComponent } from 'react-redux';
import GoldenLayout from '@deephaven/golden-layout';
import type {
Expand All @@ -8,6 +14,34 @@ import type {
} from '@deephaven/golden-layout';
import PanelManager from './PanelManager';

/**
* Alias for the return type of React.forwardRef()
*/
export type ForwardRefComponentType<P, R> = ForwardRefExoticComponent<
PropsWithoutRef<P> & RefAttributes<R>
>;

/**
* Panel components can provide static props that provide meta data about the
* panel.
*/
export interface PanelStaticMetaData {
/**
* Should be set to the same name as the component type.
* @deprecated Use `displayName` instead.
*/
COMPONENT?: string;

/** Title of the panel. */
TITLE?: string;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is used here and in PanelComponentType, may want to separate it out and add some JSDocs about what each is for

}

/**
* Panels defined as functional components have to use React.forwardRef.
*/
export type PanelFunctionComponentType<P, R> = ForwardRefComponentType<P, R> &
PanelStaticMetaData;

export type WrappedComponentType<
P extends PanelProps,
C extends ComponentType<P>
Expand All @@ -16,7 +50,12 @@ export type WrappedComponentType<
export type PanelComponentType<
P extends PanelProps = PanelProps,
C extends ComponentType<P> = ComponentType<P>
> = ComponentType<P> | WrappedComponentType<P, C>;
> = (
| ComponentType<P>
| WrappedComponentType<P, C>
| PanelFunctionComponentType<P, unknown>
) &
PanelStaticMetaData;

export function isWrappedComponent<
P extends PanelProps,
Expand Down
1 change: 1 addition & 0 deletions packages/dashboard/src/layout/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { default as LayoutUtils } from './LayoutUtils';
export { default as GLPropTypes } from './GLPropTypes';

export * from './hooks';
export { default as usePanelRegistration } from './usePanelRegistration';
87 changes: 87 additions & 0 deletions packages/dashboard/src/layout/usePanelRegistration.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* eslint-disable max-classes-per-file */
import React from 'react';
import { renderHook } from '@testing-library/react-hooks';
import usePanelRegistration from './usePanelRegistration';
import { PanelProps } from '../DashboardPlugin';

/* eslint-disable react/prefer-stateless-function */
class ClassCOMPONENT extends React.Component<PanelProps> {
static COMPONENT = 'ClassCOMPONENT';
}
class ClassDisplayName extends React.Component<PanelProps> {
static displayName = 'ClassDisplayName';
}
class ClassNoName extends React.Component<PanelProps> {}
/* eslint-enable react/prefer-stateless-function */

function FnCOMPONENT() {
return null;
}
FnCOMPONENT.COMPONENT = 'FnCOMPONENT';
function FnDisplayName() {
return null;
}
FnDisplayName.displayName = 'FnDisplayName';
function FnNoName() {
return null;
}

const deregister = jest.fn();
const registerComponent = jest.fn();
const hydrate = jest.fn();
const dehydrate = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
registerComponent.mockReturnValue(deregister);
});

it.each([
[ClassCOMPONENT.COMPONENT, ClassCOMPONENT],
[ClassDisplayName.displayName, ClassDisplayName],
[FnCOMPONENT.COMPONENT, FnCOMPONENT],
[FnDisplayName.displayName, FnDisplayName],
])(
'should register components with COMPONENT or displayName attributes and deregister on unmount: "%s"',
(_label, ComponentType) => {
const { unmount } = renderHook(() =>
usePanelRegistration(registerComponent, ComponentType, hydrate, dehydrate)
);

const name =
'COMPONENT' in ComponentType
? ComponentType.COMPONENT
: ComponentType.displayName;

expect(registerComponent).toHaveBeenCalledWith(
name,
ComponentType,
hydrate,
dehydrate
);

expect(deregister).not.toHaveBeenCalled();

unmount();

expect(deregister).toHaveBeenCalled();
}
);

it.each([
['MockClassNoName', ClassNoName],
['MockFnNoName', FnNoName],
])(
'should throw an error if no COMPONENT or displayName attribute exists: "%s"',
(_label, ComponentType) => {
const { result } = renderHook(() =>
usePanelRegistration(registerComponent, ComponentType, hydrate, dehydrate)
);

expect(result.error).toEqual(
new Error(
'ComponentType must have a `COMPONENT` or `displayName` attribute.'
)
);
}
);
47 changes: 47 additions & 0 deletions packages/dashboard/src/layout/usePanelRegistration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import {
DashboardPluginComponentProps,
PanelComponentType,
PanelDehydrateFunction,
PanelHydrateFunction,
PanelProps,
} from '../DashboardPlugin';

/**
* Registers a given panel component. Also runs a `useEffect` that will
* automatically de-register then panel on unmount.
* @param registerComponent
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might not want this as part of the hook. Bender and I chatted about plugins the other day and there's a lot of rough edges and things that need to be cleaned up around their implementation. Basically we should be able to write our existing plugins all as external plugins in an ideal world

hydrate and dehydrate are the same. IMO they are logic that belong in the component, not at the registration point. Our existing hydration code is in AppMainContainer which separates it from where the hydration is actually used.

I don't think they're changes that should be in this PR, but just something to note that this will probably change signatures as we address those points

* @param ComponentType
* @param hydrate
* @param dehydrate
*/
export default function usePanelRegistration<
P extends PanelProps,
C extends React.ComponentType<P>
>(
registerComponent: DashboardPluginComponentProps['registerComponent'],
ComponentType: PanelComponentType<P, C>,
hydrate?: PanelHydrateFunction<P>,
dehydrate?: PanelDehydrateFunction
) {
const name = ComponentType.COMPONENT ?? ComponentType.displayName;

if (name == null) {
throw new Error(
'ComponentType must have a `COMPONENT` or `displayName` attribute.'
);
}

React.useEffect(() => {
const deregister = registerComponent(
name,
ComponentType,
hydrate,
dehydrate
);

return () => {
deregister();
};
}, [ComponentType, dehydrate, hydrate, name, registerComponent]);
}