Skip to content

Commit 2571fad

Browse files
authored
feat: Core plugins refactor, XComponent framework (#2150)
- 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
1 parent 58ee88d commit 2571fad

39 files changed

Lines changed: 1073 additions & 333 deletions

package-lock.json

Lines changed: 5 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/code-studio/src/main/AppMainContainer.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
DashboardUtils,
3131
DEFAULT_DASHBOARD_ID,
3232
DehydratedDashboardPanelProps,
33+
emitPanelOpen,
3334
getAllDashboardsData,
3435
getDashboardData,
3536
listenForCreateDashboard,
@@ -75,8 +76,9 @@ import {
7576
copyToClipboard,
7677
PromiseUtils,
7778
EMPTY_ARRAY,
79+
assertNotNull,
7880
} from '@deephaven/utils';
79-
import GoldenLayout from '@deephaven/golden-layout';
81+
import GoldenLayout, { EventHub } from '@deephaven/golden-layout';
8082
import type { ItemConfig } from '@deephaven/golden-layout';
8183
import { type PluginModuleMap, getDashboardPlugins } from '@deephaven/plugin';
8284
import {
@@ -394,10 +396,15 @@ export class AppMainContainer extends Component<
394396
this.emitLayoutEvent(PanelEvent.REOPEN_LAST);
395397
}
396398

397-
emitLayoutEvent(event: string, ...args: unknown[]): void {
399+
getActiveEventHub(): EventHub {
398400
const { activeTabKey } = this.state;
399401
const layout = this.dashboardLayouts.get(activeTabKey);
400-
layout?.eventHub.emit(event, ...args);
402+
assertNotNull(layout, 'No active layout found');
403+
return layout.eventHub;
404+
}
405+
406+
emitLayoutEvent(event: string, ...args: unknown[]): void {
407+
this.getActiveEventHub().emit(event, ...args);
401408
}
402409

403410
handleCancelResetLayoutPrompt(): void {
@@ -702,10 +709,10 @@ export class AppMainContainer extends Component<
702709
dragEvent?: WindowMouseEvent
703710
): void {
704711
const { connection } = this.props;
705-
this.emitLayoutEvent(PanelEvent.OPEN, {
712+
emitPanelOpen(this.getActiveEventHub(), {
713+
widget: getVariableDescriptor(widget),
706714
dragEvent,
707715
fetch: async () => connection?.getObject(widget),
708-
widget: getVariableDescriptor(widget),
709716
});
710717
}
711718

packages/code-studio/src/styleguide/StyleGuide.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import SpectrumComparison from './SpectrumComparison';
3939
import Pickers from './Pickers';
4040
import ListViews from './ListViews';
4141
import ErrorViews from './ErrorViews';
42+
import XComponents from './XComponents';
4243

4344
const stickyProps = {
4445
position: 'sticky',
@@ -134,13 +135,14 @@ function StyleGuide(): React.ReactElement {
134135
<Charts />
135136
<ContextMenuRoot />
136137
<RandomAreaPlotAnimation />
138+
<ErrorViews />
139+
<XComponents />
137140

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

141144
<SampleMenuCategory data-menu-category="Spectrum Comparison" />
142145
<SpectrumComparison />
143-
<ErrorViews />
144146
</div>
145147
</div>
146148
);
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import React, { useState } from 'react';
2+
import {
3+
XComponentMapProvider,
4+
createXComponent,
5+
Button,
6+
} from '@deephaven/components';
7+
import SampleSection from './SampleSection';
8+
9+
type FooComponentProps = { value: string };
10+
11+
function FooComponent({ value }: FooComponentProps) {
12+
return (
13+
<Button kind="primary" onClick={() => undefined}>
14+
{value}
15+
</Button>
16+
);
17+
}
18+
FooComponent.displayName = 'FooComponent';
19+
20+
// Create an XComponent from FooComponent to allow for replacement
21+
const XFooComponent = createXComponent(FooComponent);
22+
23+
function NestedFooComponent({ value }: FooComponentProps) {
24+
// We're using the XComponent version so this panel can be replaced if it is mapped from a parent context to a replacement
25+
return <XFooComponent value={`${value}.${value}`} />;
26+
}
27+
28+
function MultiFooComponent({ value }: FooComponentProps) {
29+
// Show multiple instances getting replaced
30+
return (
31+
<div>
32+
<XFooComponent value={value} />
33+
<XFooComponent value={value} />
34+
</div>
35+
);
36+
}
37+
38+
// What we're replacing the XFooComponent with.
39+
function ReverseFooComponent({ value }: FooComponentProps) {
40+
return (
41+
<Button kind="danger" onClick={() => undefined}>
42+
{value.split('').reverse().join('')}
43+
</Button>
44+
);
45+
}
46+
47+
/**
48+
* Some examples showing usage of XComponents.
49+
*/
50+
export function XComponents(): JSX.Element {
51+
const [value, setValue] = useState('hello');
52+
53+
return (
54+
<SampleSection name="xcomponents">
55+
<h2 className="ui-title">XComponents</h2>
56+
<p>
57+
XComponents are a way to replace a component with another component
58+
without needing to pass props all the way down the component tree. This
59+
can be useful in cases where we have a component deep down in the
60+
component tree that we want to replace with a different component, but
61+
don&apos;t want to have to provide props at the top level just to hook
62+
into that.
63+
<br />
64+
Below is a component that is simply a button displaying the text
65+
inputted in the input field. We will replace this component with a new
66+
component that reverses the text, straight up, then in a nested
67+
scenario, and then multiple instances.
68+
</p>
69+
<div className="form-group">
70+
<label htmlFor="xcomponentsInput">
71+
Input Value:
72+
<input
73+
type="text"
74+
className="form-control"
75+
id="xcomponentsInput"
76+
value={value}
77+
onChange={e => setValue(e.target.value)}
78+
/>
79+
</label>
80+
</div>
81+
<div className="row">
82+
<div className="col">
83+
<small>Original Component</small>
84+
<div>
85+
<XFooComponent value={value} />
86+
</div>
87+
88+
<small>Replaced with Reverse</small>
89+
<div>
90+
<XComponentMapProvider
91+
value={new Map([[XFooComponent, ReverseFooComponent]])}
92+
>
93+
<XFooComponent value={value} />
94+
</XComponentMapProvider>
95+
</div>
96+
</div>
97+
<div className="col">
98+
<small>Nested component replaced</small>
99+
<div>
100+
<XComponentMapProvider
101+
value={new Map([[XFooComponent, ReverseFooComponent]])}
102+
>
103+
{/* The `FooComponent` that gets replaced is from within the `NestedFooComponent` */}
104+
<NestedFooComponent value={value} />
105+
</XComponentMapProvider>
106+
</div>
107+
</div>
108+
<div className="col">
109+
<small>Multiple Components replaced</small>
110+
<div>
111+
<XComponentMapProvider
112+
value={new Map([[XFooComponent, ReverseFooComponent]])}
113+
>
114+
<MultiFooComponent value={value} />
115+
</XComponentMapProvider>
116+
</div>
117+
</div>
118+
</div>
119+
</SampleSection>
120+
);
121+
}
122+
123+
export default XComponents;

packages/components/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,12 @@
5353
},
5454
"peerDependencies": {
5555
"react": ">=16.8.0",
56-
"react-dom": ">=16.8.0"
56+
"react-dom": ">=16.8.0",
57+
"react-is": ">=16.8.0"
5758
},
5859
"devDependencies": {
59-
"@deephaven/mocks": "file:../mocks"
60+
"@deephaven/mocks": "file:../mocks",
61+
"react-redux": "^7.2.4"
6062
},
6163
"files": [
6264
"dist",
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React, { PropsWithChildren } from 'react';
2+
// We only use react-redux from tests in @deephaven/components, so it is only added as a devDependency
3+
import { connect } from 'react-redux';
4+
import {
5+
canHaveRef,
6+
isClassComponent,
7+
isWrappedComponent,
8+
isForwardRefComponentType,
9+
} from './ComponentUtils';
10+
11+
function TestComponent() {
12+
return <div>Test</div>;
13+
}
14+
15+
class TestClass extends React.PureComponent<PropsWithChildren<never>> {
16+
render() {
17+
return <div>Test</div>;
18+
}
19+
}
20+
21+
test('isForwardRefComponent', () => {
22+
expect(isForwardRefComponentType(TestComponent)).toBe(false);
23+
expect(isForwardRefComponentType(React.forwardRef(TestComponent))).toBe(true);
24+
expect(isForwardRefComponentType(TestClass)).toBe(false);
25+
expect(isForwardRefComponentType(connect(null, null)(TestComponent))).toBe(
26+
false
27+
);
28+
expect(isForwardRefComponentType(connect(null, null)(TestClass))).toBe(false);
29+
});
30+
31+
test('isClassComponent', () => {
32+
expect(isClassComponent(TestComponent)).toBe(false);
33+
expect(isClassComponent(TestClass)).toBe(true);
34+
expect(isClassComponent(React.forwardRef(TestComponent))).toBe(false);
35+
expect(isClassComponent(connect(null, null)(TestComponent))).toBe(false);
36+
expect(isClassComponent(connect(null, null)(TestClass))).toBe(true);
37+
});
38+
39+
test('isWrappedComponent', () => {
40+
expect(isWrappedComponent(TestComponent)).toBe(false);
41+
expect(isWrappedComponent(TestClass)).toBe(false);
42+
expect(isWrappedComponent(connect(null, null)(TestComponent))).toBe(true);
43+
expect(isWrappedComponent(React.forwardRef(TestComponent))).toBe(false);
44+
expect(isWrappedComponent(connect(null, null)(TestClass))).toBe(true);
45+
});
46+
47+
test('canHaveRef', () => {
48+
const forwardedType = React.forwardRef(TestComponent);
49+
50+
expect(canHaveRef(TestComponent)).toBe(false);
51+
expect(canHaveRef(forwardedType)).toBe(true);
52+
expect(canHaveRef(TestClass)).toBe(true);
53+
expect(canHaveRef(connect(null, null)(TestClass))).toBe(true);
54+
expect(
55+
canHaveRef(connect(null, null, null, { forwardRef: true })(TestClass))
56+
).toBe(true);
57+
});

0 commit comments

Comments
 (0)