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
29 changes: 15 additions & 14 deletions packages/dashboard-core-plugins/src/WidgetLoaderPlugin.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ function TestWidgetTwo() {
return <div>TestWidgetTwo</div>;
}

function TestPanel() {
const TestPanel = React.forwardRef<unknown>((props, ref) => {
React.useImperativeHandle(ref, () => ({}));
return <div>TestPanel</div>;
Comment thread
vbabich marked this conversation as resolved.
}
});
TestPanel.displayName = 'TestPanel';

class TestForwardRef extends React.PureComponent<WidgetComponentProps> {
render() {
Expand Down Expand Up @@ -460,18 +462,17 @@ describe('middleware plugin chaining', () => {
});

it('chains panel middleware around base panelComponent', async () => {
function TestPanelMiddleware({
Component,
...props
}: WidgetMiddlewarePanelProps) {
return (
<div data-testid="panel-middleware">
<span>PanelMiddleware</span>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<Component {...props} />
</div>
);
}
const TestPanelMiddleware = React.forwardRef<
unknown,
WidgetMiddlewarePanelProps
>(({ Component, ...props }, ref) => (
<div data-testid="panel-middleware">
<span>PanelMiddleware</span>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<Component ref={ref} {...props} />
</div>
));
TestPanelMiddleware.displayName = 'TestPanelMiddleware';

const panelMiddleware: WidgetMiddlewarePlugin = {
name: 'panel-middleware',
Expand Down
22 changes: 20 additions & 2 deletions packages/plugin/src/PluginTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,17 @@ export interface WidgetMiddlewarePanelProps<T = unknown>
/**
* The next panel component in the middleware chain.
* Middleware should render this component to continue the chain.
*
* This is ref-capable: middleware that transparently wraps a single inner
* panel should forward its own `ref` to this component. Golden-layout binds
* a ref to the registered panel to persist class-component state into its
* `componentState`; if a middleware swallows the ref, the wrapped panel's
* state (sorts, filters, column moves, etc.) is never serialized and is lost
* on reload.
*/
Component: React.ComponentType<WidgetPanelProps<T>>;
Component: React.ForwardRefExoticComponent<
WidgetPanelProps<T> & React.RefAttributes<unknown>
>;
}

/**
Expand Down Expand Up @@ -207,8 +216,17 @@ export interface WidgetMiddlewarePlugin<T = unknown> extends Plugin {
/**
* The middleware panel component that wraps the base panel component.
* If omitted, only the component middleware will be applied.
*
* Must be a `React.forwardRef` component: middleware that transparently
* wraps a single inner panel has to forward its own `ref` to the wrapped
* `Component`. Golden-layout binds a ref to the registered panel to persist
* class-component state into its `componentState`; if a middleware swallows
* the ref, the wrapped panel's state (sorts, filters, column moves, etc.) is
* never serialized and is lost on reload.
*/
panelComponent?: React.ComponentType<WidgetMiddlewarePanelProps<T>>;
panelComponent?: React.ForwardRefExoticComponent<
WidgetMiddlewarePanelProps<T> & React.RefAttributes<unknown>
>;
Comment thread
vbabich marked this conversation as resolved.
}

/**
Expand Down
182 changes: 177 additions & 5 deletions packages/plugin/src/PluginUtils.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import {
registerPlugin,
createChainedComponent,
createChainedPanelComponent,
createPanelMiddleware,
createWidgetMiddleware,
getWidgetDashboardPlugin,
} from './PluginUtils';

Expand Down Expand Up @@ -814,15 +816,16 @@ describe('createChainedPanelComponent', () => {
);
}

function PanelComp({ Component, ...props }: WidgetMiddlewarePanelProps) {
return (
const PanelComp = React.forwardRef<unknown, WidgetMiddlewarePanelProps>(
({ Component, ...props }, ref) => (
<div data-testid={`${name}-panel`}>
<span>{name}</span>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<Component {...props} />
<Component ref={ref} {...props} />
</div>
);
}
)
);
PanelComp.displayName = `${name}PanelComp`;

return {
name,
Expand Down Expand Up @@ -923,4 +926,173 @@ describe('createChainedPanelComponent', () => {
expect(screen.getByTestId('table-panel-mw-panel')).toBeInTheDocument();
expect(screen.getByTestId('base-panel')).toBeInTheDocument();
});

it('forwards the golden-layout ref through middleware to the base panel', () => {
// A ref-capable base panel, mirroring a class panel golden-layout binds a
// ref to for componentState persistence.
const RefBasePanel = React.forwardRef<{ marker: string }, WidgetPanelProps>(
(props, ref) => {
React.useImperativeHandle(ref, () => ({
marker: 'base-panel-instance',
}));
return <div data-testid="ref-base-panel">RefBasePanel</div>;
}
);
Comment thread
vbabich marked this conversation as resolved.
RefBasePanel.displayName = 'RefBasePanel';

const mw = makePanelMiddleware('ref-mw');
const Chained = createChainedPanelComponent(RefBasePanel, [
mw,
]) as React.ForwardRefExoticComponent<
WidgetPanelProps & React.RefAttributes<unknown>
>;

const panelProps = {
fetch: jest.fn(),
metadata: { type: 'test-type' },
localDashboardId: 'test',
glContainer: {} as WidgetPanelProps['glContainer'],
glEventHub: {} as WidgetPanelProps['glEventHub'],
};

const ref = React.createRef<{ marker: string }>();
render(
// eslint-disable-next-line react/jsx-props-no-spreading
<Chained ref={ref} {...panelProps} />
);

// The ref must reach the base panel through the middleware layer, otherwise
// golden-layout cannot persist the wrapped panel's state.
expect(screen.getByTestId('ref-mw-panel')).toBeInTheDocument();
expect(ref.current).toEqual({ marker: 'base-panel-instance' });
});
});

describe('createPanelMiddleware', () => {
it('forwards the ref to the wrapped Component', () => {
const RefBasePanelBase = React.forwardRef<
{ marker: string },
WidgetPanelProps
>((props, ref) => {
React.useImperativeHandle(ref, () => ({ marker: 'base-instance' }));
return <div data-testid="base-panel">RefBasePanel</div>;
});
Comment thread
vbabich marked this conversation as resolved.
RefBasePanelBase.displayName = 'RefBasePanel';
const RefBasePanel = RefBasePanelBase as React.ForwardRefExoticComponent<
WidgetPanelProps & React.RefAttributes<unknown>
>;

const Middleware = createPanelMiddleware(() => ({}), 'TestPanelMiddleware');

const ref = React.createRef<{ marker: string }>();
render(
<Middleware
ref={ref}
Component={RefBasePanel}
fetch={jest.fn()}
metadata={{ type: 'test-type' }}
localDashboardId="test"
glContainer={{} as WidgetPanelProps['glContainer']}
glEventHub={{} as WidgetPanelProps['glEventHub']}
/>
);

expect(screen.getByTestId('base-panel')).toBeInTheDocument();
expect(ref.current).toEqual({ marker: 'base-instance' });
});

it('injects props and wraps the wrapped Component', () => {
let received: Record<string, unknown> | undefined;
const BasePanelBase = React.forwardRef<unknown, WidgetPanelProps>(
(props, ref) => {
received = props as unknown as Record<string, unknown>;
return <div data-testid="base-panel">BasePanel</div>;
}
);
Comment thread
vbabich marked this conversation as resolved.
BasePanelBase.displayName = 'BasePanel';
const BasePanel = BasePanelBase as React.ForwardRefExoticComponent<
WidgetPanelProps & React.RefAttributes<unknown>
>;

const marker = jest.fn();
const Middleware = createPanelMiddleware(() => ({
inject: { extraProp: marker },
wrap: child => <div data-testid="wrapper">{child}</div>,
}));

render(
<Middleware
Component={BasePanel}
fetch={jest.fn()}
metadata={{ type: 'test-type' }}
localDashboardId="test"
glContainer={{} as WidgetPanelProps['glContainer']}
glEventHub={{} as WidgetPanelProps['glEventHub']}
/>
);

expect(screen.getByTestId('wrapper')).toBeInTheDocument();
expect(screen.getByTestId('base-panel')).toBeInTheDocument();
expect(received?.extraProp).toBe(marker);
});

it('passes the incoming props (minus Component) to the body hook', () => {
const useBody = jest.fn(() => ({}));
const BasePanelBase = React.forwardRef<unknown, WidgetPanelProps>(() => (
<div data-testid="base-panel">BasePanel</div>
));
BasePanelBase.displayName = 'BasePanel';
const BasePanel = BasePanelBase as React.ForwardRefExoticComponent<
WidgetPanelProps & React.RefAttributes<unknown>
>;
const Middleware = createPanelMiddleware(useBody);

render(
<Middleware
Component={BasePanel}
fetch={jest.fn()}
metadata={{ type: 'test-type' }}
localDashboardId="test"
glContainer={{} as WidgetPanelProps['glContainer']}
glEventHub={{} as WidgetPanelProps['glEventHub']}
/>
);

expect(useBody).toHaveBeenCalledTimes(1);
const bodyProps = useBody.mock.calls[0][0] as Record<string, unknown>;
expect(bodyProps).not.toHaveProperty('Component');
expect(bodyProps).toMatchObject({
localDashboardId: 'test',
metadata: { type: 'test-type' },
});
});
});

describe('createWidgetMiddleware', () => {
it('injects props and wraps the wrapped Component', () => {
let received: Record<string, unknown> | undefined;
function BaseWidget(props: WidgetComponentProps): JSX.Element {
received = props as unknown as Record<string, unknown>;
return <div data-testid="base-widget">BaseWidget</div>;
}
BaseWidget.displayName = 'BaseWidget';

const marker = jest.fn();
const Middleware = createWidgetMiddleware(() => ({
inject: { extraProp: marker },
wrap: child => <div data-testid="wrapper">{child}</div>,
}));

render(
<Middleware
Component={BaseWidget}
fetch={jest.fn()}
metadata={{ type: 'test-type' }}
/>
);

expect(screen.getByTestId('wrapper')).toBeInTheDocument();
expect(screen.getByTestId('base-widget')).toBeInTheDocument();
expect(received?.extraProp).toBe(marker);
});
});
Loading
Loading