Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Core plugins refactor, XComponent framework #2150

Merged
merged 4 commits into from
Jul 17, 2024
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
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
Loading