Skip to content

Commit

Permalink
feat: Core plugins refactor, XComponent framework (#2150)
Browse files Browse the repository at this point in the history
- Add XComponent (eXtendable Component) framework to allow components to
be wrapped/replaced
  - Similar to Swizzling in Docusaurus
- Useful for components that we need to replace at the Enterprise level
(e.g. `WidgetPanelTooltip` needs to display the query name in
Enterprise)
  - Added to the StyleGuide
- Pass a `VariableDescriptor` to `WidgetPanel` and `WidgetPanelTooltip`
- Allows for more information to be included, e.g. a
`QueryVariableDescriptor` which extends `VariableDescriptor`
- Refactor the Core plugins to be consistent in how they handle the
`PanelEvent.OPEN` event
- Will allow Enterprise to use these plugins straight up and remove
their duplicated panel code
- Add functions for `PanelEvent.OPEN`
- Instead of using `PanelEvent.OPEN` directly and not getting any type
safety, add functions for emitting, listening, and a hook to enforce
type safety
  • Loading branch information
mofojed authored Jul 17, 2024
1 parent 58ee88d commit 2571fad
Show file tree
Hide file tree
Showing 39 changed files with 1,073 additions and 333 deletions.
20 changes: 5 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 12 additions & 5 deletions packages/code-studio/src/main/AppMainContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
DashboardUtils,
DEFAULT_DASHBOARD_ID,
DehydratedDashboardPanelProps,
emitPanelOpen,
getAllDashboardsData,
getDashboardData,
listenForCreateDashboard,
Expand Down Expand Up @@ -75,8 +76,9 @@ import {
copyToClipboard,
PromiseUtils,
EMPTY_ARRAY,
assertNotNull,
} from '@deephaven/utils';
import GoldenLayout from '@deephaven/golden-layout';
import GoldenLayout, { EventHub } from '@deephaven/golden-layout';
import type { ItemConfig } from '@deephaven/golden-layout';
import { type PluginModuleMap, getDashboardPlugins } from '@deephaven/plugin';
import {
Expand Down Expand Up @@ -394,10 +396,15 @@ export class AppMainContainer extends Component<
this.emitLayoutEvent(PanelEvent.REOPEN_LAST);
}

emitLayoutEvent(event: string, ...args: unknown[]): void {
getActiveEventHub(): EventHub {
const { activeTabKey } = this.state;
const layout = this.dashboardLayouts.get(activeTabKey);
layout?.eventHub.emit(event, ...args);
assertNotNull(layout, 'No active layout found');
return layout.eventHub;
}

emitLayoutEvent(event: string, ...args: unknown[]): void {
this.getActiveEventHub().emit(event, ...args);
}

handleCancelResetLayoutPrompt(): void {
Expand Down Expand Up @@ -702,10 +709,10 @@ export class AppMainContainer extends Component<
dragEvent?: WindowMouseEvent
): void {
const { connection } = this.props;
this.emitLayoutEvent(PanelEvent.OPEN, {
emitPanelOpen(this.getActiveEventHub(), {
widget: getVariableDescriptor(widget),
dragEvent,
fetch: async () => connection?.getObject(widget),
widget: getVariableDescriptor(widget),
});
}

Expand Down
4 changes: 3 additions & 1 deletion packages/code-studio/src/styleguide/StyleGuide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import SpectrumComparison from './SpectrumComparison';
import Pickers from './Pickers';
import ListViews from './ListViews';
import ErrorViews from './ErrorViews';
import XComponents from './XComponents';

const stickyProps = {
position: 'sticky',
Expand Down Expand Up @@ -134,13 +135,14 @@ function StyleGuide(): React.ReactElement {
<Charts />
<ContextMenuRoot />
<RandomAreaPlotAnimation />
<ErrorViews />
<XComponents />

<SampleMenuCategory data-menu-category="Spectrum Components" />
<SpectrumComponents />

<SampleMenuCategory data-menu-category="Spectrum Comparison" />
<SpectrumComparison />
<ErrorViews />
</div>
</div>
);
Expand Down
123 changes: 123 additions & 0 deletions packages/code-studio/src/styleguide/XComponents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React, { useState } from 'react';
import {
XComponentMapProvider,
createXComponent,
Button,
} from '@deephaven/components';
import SampleSection from './SampleSection';

type FooComponentProps = { value: string };

function FooComponent({ value }: FooComponentProps) {
return (
<Button kind="primary" onClick={() => undefined}>
{value}
</Button>
);
}
FooComponent.displayName = 'FooComponent';

// Create an XComponent from FooComponent to allow for replacement
const XFooComponent = createXComponent(FooComponent);

function NestedFooComponent({ value }: FooComponentProps) {
// We're using the XComponent version so this panel can be replaced if it is mapped from a parent context to a replacement
return <XFooComponent value={`${value}.${value}`} />;
}

function MultiFooComponent({ value }: FooComponentProps) {
// Show multiple instances getting replaced
return (
<div>
<XFooComponent value={value} />
<XFooComponent value={value} />
</div>
);
}

// What we're replacing the XFooComponent with.
function ReverseFooComponent({ value }: FooComponentProps) {
return (
<Button kind="danger" onClick={() => undefined}>
{value.split('').reverse().join('')}
</Button>
);
}

/**
* Some examples showing usage of XComponents.
*/
export function XComponents(): JSX.Element {
const [value, setValue] = useState('hello');

return (
<SampleSection name="xcomponents">
<h2 className="ui-title">XComponents</h2>
<p>
XComponents are a way to replace a component with another component
without needing to pass props all the way down the component tree. This
can be useful in cases where we have a component deep down in the
component tree that we want to replace with a different component, but
don&apos;t want to have to provide props at the top level just to hook
into that.
<br />
Below is a component that is simply a button displaying the text
inputted in the input field. We will replace this component with a new
component that reverses the text, straight up, then in a nested
scenario, and then multiple instances.
</p>
<div className="form-group">
<label htmlFor="xcomponentsInput">
Input Value:
<input
type="text"
className="form-control"
id="xcomponentsInput"
value={value}
onChange={e => setValue(e.target.value)}
/>
</label>
</div>
<div className="row">
<div className="col">
<small>Original Component</small>
<div>
<XFooComponent value={value} />
</div>

<small>Replaced with Reverse</small>
<div>
<XComponentMapProvider
value={new Map([[XFooComponent, ReverseFooComponent]])}
>
<XFooComponent value={value} />
</XComponentMapProvider>
</div>
</div>
<div className="col">
<small>Nested component replaced</small>
<div>
<XComponentMapProvider
value={new Map([[XFooComponent, ReverseFooComponent]])}
>
{/* The `FooComponent` that gets replaced is from within the `NestedFooComponent` */}
<NestedFooComponent value={value} />
</XComponentMapProvider>
</div>
</div>
<div className="col">
<small>Multiple Components replaced</small>
<div>
<XComponentMapProvider
value={new Map([[XFooComponent, ReverseFooComponent]])}
>
<MultiFooComponent value={value} />
</XComponentMapProvider>
</div>
</div>
</div>
</SampleSection>
);
}

export default XComponents;
6 changes: 4 additions & 2 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,12 @@
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
"react-dom": ">=16.8.0",
"react-is": ">=16.8.0"
},
"devDependencies": {
"@deephaven/mocks": "file:../mocks"
"@deephaven/mocks": "file:../mocks",
"react-redux": "^7.2.4"
},
"files": [
"dist",
Expand Down
57 changes: 57 additions & 0 deletions packages/components/src/ComponentUtils.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { PropsWithChildren } from 'react';
// We only use react-redux from tests in @deephaven/components, so it is only added as a devDependency
import { connect } from 'react-redux';
import {
canHaveRef,
isClassComponent,
isWrappedComponent,
isForwardRefComponentType,
} from './ComponentUtils';

function TestComponent() {
return <div>Test</div>;
}

class TestClass extends React.PureComponent<PropsWithChildren<never>> {
render() {
return <div>Test</div>;
}
}

test('isForwardRefComponent', () => {
expect(isForwardRefComponentType(TestComponent)).toBe(false);
expect(isForwardRefComponentType(React.forwardRef(TestComponent))).toBe(true);
expect(isForwardRefComponentType(TestClass)).toBe(false);
expect(isForwardRefComponentType(connect(null, null)(TestComponent))).toBe(
false
);
expect(isForwardRefComponentType(connect(null, null)(TestClass))).toBe(false);
});

test('isClassComponent', () => {
expect(isClassComponent(TestComponent)).toBe(false);
expect(isClassComponent(TestClass)).toBe(true);
expect(isClassComponent(React.forwardRef(TestComponent))).toBe(false);
expect(isClassComponent(connect(null, null)(TestComponent))).toBe(false);
expect(isClassComponent(connect(null, null)(TestClass))).toBe(true);
});

test('isWrappedComponent', () => {
expect(isWrappedComponent(TestComponent)).toBe(false);
expect(isWrappedComponent(TestClass)).toBe(false);
expect(isWrappedComponent(connect(null, null)(TestComponent))).toBe(true);
expect(isWrappedComponent(React.forwardRef(TestComponent))).toBe(false);
expect(isWrappedComponent(connect(null, null)(TestClass))).toBe(true);
});

test('canHaveRef', () => {
const forwardedType = React.forwardRef(TestComponent);

expect(canHaveRef(TestComponent)).toBe(false);
expect(canHaveRef(forwardedType)).toBe(true);
expect(canHaveRef(TestClass)).toBe(true);
expect(canHaveRef(connect(null, null)(TestClass))).toBe(true);
expect(
canHaveRef(connect(null, null, null, { forwardRef: true })(TestClass))
).toBe(true);
});
Loading

0 comments on commit 2571fad

Please sign in to comment.