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: Widget plugins #1564

Merged
merged 15 commits into from
Oct 19, 2023
2 changes: 2 additions & 0 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions packages/console/src/Console.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import {
CommandHistoryStorage,
CommandHistoryStorageItem,
} from './command-history';
import { ObjectIcon } from './common';

const log = Log.module('Console');

Expand Down Expand Up @@ -114,7 +113,8 @@ function defaultSupportsType(): boolean {
}

function defaultIconForType(type: string): ReactElement {
return <ObjectIcon type={type} />;
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}

export class Console extends PureComponent<ConsoleProps, ConsoleState> {
Expand Down
59 changes: 31 additions & 28 deletions packages/console/src/console-history/ConsoleHistoryItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ const log = Log.module('ConsoleHistoryItem');
interface ConsoleHistoryItemProps {
item: ConsoleHistoryActionItem;
language: string;
openObject(object: VariableDefinition): void;
openObject: (object: VariableDefinition) => void;
disabled?: boolean;
supportsType(type: string): boolean;
iconForType(type: string): ReactElement;
// TODO: #1573 Remove this eslint disable
// eslint-disable-next-line react/no-unused-prop-types
supportsType: (type: string) => boolean;
iconForType: (type: string) => ReactElement;
}

class ConsoleHistoryItem extends PureComponent<
Expand Down Expand Up @@ -55,7 +57,7 @@ class ConsoleHistoryItem extends PureComponent<
}

render(): ReactElement {
const { disabled, item, language, supportsType, iconForType } = this.props;
const { disabled, item, language, iconForType } = this.props;
const { disabledObjects, result } = item;
const hasCommand = item.command != null && item.command !== '';

Expand All @@ -79,30 +81,31 @@ class ConsoleHistoryItem extends PureComponent<

if (changes) {
const { created, updated } = changes;
[...created, ...updated]
// .filter(object => supportsType(object.type))
.forEach(object => {
hasButtons = true;
const { title } = object;
const key = `${title}`;
const btnDisabled =
disabled === undefined ||
disabled ||
(disabledObjects ?? []).indexOf(key) >= 0;
const element = (
<Button
key={key}
kind={supportsType(object.type) ? 'primary' : 'tertiary'}
onClick={() => this.handleObjectClick(object)}
className="btn-console-object"
disabled={btnDisabled}
icon={iconForType(object.type)}
>
{title}
</Button>
);
resultElements.push(element);
});
// TODO: #1573 filter for supported types or change button kind
// based on if type is supported. Possibly a warn state for widgets
// that the UI doesn't have anything registered to support.
[...created, ...updated].forEach(object => {
hasButtons = true;
const { title } = object;
const key = `${title}`;
const btnDisabled =
disabled === undefined ||
disabled ||
(disabledObjects ?? []).indexOf(key) >= 0;
const element = (
<Button
key={key}
kind="primary"
onClick={() => this.handleObjectClick(object)}
className="btn-console-object"
disabled={btnDisabled}
icon={iconForType(object.type)}
>
{title}
</Button>
);
resultElements.push(element);
});
}

// If the error has an associated command, we'll actually get a separate ERROR item printed out, so only print an error if there isn't an associated command
Expand Down
3 changes: 2 additions & 1 deletion packages/dashboard-core-plugins/src/PandasPlugin.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { DashboardPanelProps } from '@deephaven/dashboard';
import { WidgetComponentProps } from '@deephaven/plugin';
import { forwardRef, useMemo } from 'react';
import { PandasPanel } from './panels';
import useHydrateGrid from './useHydrateGrid';

export const PandasPlugin = forwardRef(
(props: DashboardPanelProps, ref: React.Ref<PandasPanel>) => {
(props: WidgetComponentProps, ref: React.Ref<PandasPanel>) => {
const hydrate = useHydrateGrid<DashboardPanelProps>();
const { localDashboardId } = props;
const hydratedProps = useMemo(
Expand Down
1 change: 1 addition & 0 deletions packages/dashboard-core-plugins/src/PandasPluginConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PandasPlugin from './PandasPlugin';

const PandasPluginConfig: WidgetPlugin = {
name: 'PandasPlugin',
wrapWidget: false,
type: PluginType.WIDGET_PLUGIN,
component: PandasPlugin,
supportedTypes: 'pandas.DataFrame',
Expand Down
105 changes: 99 additions & 6 deletions packages/dashboard-core-plugins/src/WidgetLoaderPlugin.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { useMemo, useCallback, type ComponentType, useEffect } from 'react';
import {
useMemo,
useCallback,
type ComponentType,
useEffect,
forwardRef,
useState,
} from 'react';
import type { ReactComponentConfig } from '@deephaven/golden-layout';
import shortid from 'shortid';
import {
Expand All @@ -9,24 +16,100 @@ import {
PanelOpenEventDetail,
LayoutUtils,
useListener,
PanelProps,
canHaveRef,
} from '@deephaven/dashboard';
import { usePlugins } from '@deephaven/app-utils';
import { isWidgetPlugin } from '@deephaven/plugin';
import { isWidgetPlugin, type WidgetPlugin } from '@deephaven/plugin';
import Log from '@deephaven/log';
import { WidgetPanel } from './panels';

const log = Log.module('WidgetLoaderPlugin');

function wrapWidgetPlugin(plugin: WidgetPlugin) {
function Wrapper(props: PanelProps, ref: React.ForwardedRef<unknown>) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const C = plugin.component as any;
const { metadata } = props;
const [componentPanel, setComponentPanel] = useState<ComponentType>();
const refCallback = useCallback(
(e: ComponentType) => {
setComponentPanel(e);
if (typeof ref === 'function') {
ref(e);
} else if (ref != null) {
// eslint-disable-next-line no-param-reassign
ref.current = e;
}
},
[ref]
);

const hasRef = canHaveRef(C);

return (
<WidgetPanel
widgetName={metadata?.name}
widgetType={plugin.name}
componentPanel={componentPanel}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
{hasRef ? (
<C
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
ref={refCallback}
/>
) : (
<C
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
)}
)
</WidgetPanel>
);
}

Wrapper.displayName = `WidgetLoaderPlugin(${
plugin.component.displayName ?? plugin.name
})`;

return forwardRef(Wrapper);
}

/**
* Widget to automatically open any supported WidgetPlugin types as panels
* if the widget is emitted from the server as the result of executed code.
*
* Does not open panels for widgets that are not supported by any plugins.
* Does not open panels for widgets that are a component of a larger widget or UI element.
*
* @param props Dashboard plugin props
* @returns React element
*/
export function WidgetLoaderPlugin(
mattrunyon marked this conversation as resolved.
Show resolved Hide resolved
props: DashboardPluginComponentProps
): JSX.Element | null {
const plugins = usePlugins();
const supportedTypes = useMemo(() => {
const typeMap = new Map<string, ComponentType>();
const typeMap = new Map<string, WidgetPlugin>();
plugins.forEach(plugin => {
if (!isWidgetPlugin(plugin)) {
return;
}

[plugin.supportedTypes].flat().forEach(supportedType => {
if (supportedType != null && supportedType !== '') {
typeMap.set(supportedType, plugin.component);
if (typeMap.has(supportedType)) {
log.warn(
`Multiple WidgetPlugins handling type ${supportedType}. Replacing ${typeMap.get(
supportedType
)?.name} with ${plugin.name} to handle ${supportedType}`
);
}
typeMap.set(supportedType, plugin);
}
});
});
Expand All @@ -46,7 +129,7 @@ export function WidgetLoaderPlugin(
}: PanelOpenEventDetail) => {
const { id: widgetId, type } = widget;
const name = widget.title ?? widget.name;
const component = supportedTypes.get(type);
const { component } = supportedTypes.get(type) ?? {};
if (component == null) {
return;
}
Expand Down Expand Up @@ -76,7 +159,17 @@ export function WidgetLoaderPlugin(
useEffect(() => {
const deregisterFns = [...plugins.values()]
.filter(isWidgetPlugin)
.map(plugin => registerComponent(plugin.name, plugin.component));
.map(plugin => {
const { wrapWidget = true } = plugin;
if (wrapWidget) {
return registerComponent(plugin.name, wrapWidgetPlugin(plugin));
}
return registerComponent(
plugin.name,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
plugin.component as ComponentType<any>
);
});

return () => {
deregisterFns.forEach(deregister => deregister());
Expand Down
2 changes: 0 additions & 2 deletions packages/dashboard-core-plugins/src/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
GridPlugin,
LinkerPlugin,
MarkdownPlugin,
PandasPlugin,
} from '.';

function makeConnection(): IdeConnection {
Expand All @@ -40,7 +39,6 @@ it('handles mounting and unmount core plugins properly', () => {
<ConsolePlugin />
<LinkerPlugin />
<MarkdownPlugin />
<PandasPlugin hydrate={() => undefined} />
</Dashboard>
</Provider>
</PluginsContext.Provider>
Expand Down
7 changes: 6 additions & 1 deletion packages/dashboard-core-plugins/src/panels/ConsolePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Console,
ConsoleConstants,
HeapUsage,
ObjectIcon,
} from '@deephaven/console';
import { DashboardPanelProps, PanelEvent } from '@deephaven/dashboard';
import type { IdeSession, VariableDefinition } from '@deephaven/jsapi-types';
Expand Down Expand Up @@ -324,7 +325,11 @@ export class ConsolePanel extends PureComponent<
iconForType(type: string): JSX.Element {
const { plugins } = this.props;
const plugin = [...plugins.values()].find(p => pluginSupportsType(p, type));
return getIconForType(plugin, type);
if (plugin != null) {
return getIconForType(plugin, type);
}
// TODO: #1573 Remove this default and always return getIconForType
return <ObjectIcon type={type} />;
}

render(): ReactElement {
Expand Down
6 changes: 2 additions & 4 deletions packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,10 @@ export interface IrisGridPanelProps extends DashboardPanelProps {
onStateChange?: (irisGridState: IrisGridState, gridState: GridState) => void;
onPanelStateUpdate?: (panelState: PanelState) => void;

/**
* Override the default worker used by IrisGrid to download CSVs.
*/
/** Override the default worker used by IrisGrid to download CSVs. */
getDownloadWorker?: () => Promise<ServiceWorker>;

// Load a plugin defined by the table
/** Load a plugin defined by the table */
loadPlugin: (pluginName: string) => TablePluginComponent;

theme?: IrisGridThemeType;
Expand Down
18 changes: 14 additions & 4 deletions packages/dashboard-core-plugins/src/panels/Panel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, {
Component,
ComponentType,
FocusEvent,
FocusEventHandler,
PureComponent,
Expand Down Expand Up @@ -32,7 +33,7 @@ import RenameDialog from './RenameDialog';
const log = Log.module('Panel');

interface PanelProps {
componentPanel: Component;
componentPanel?: ComponentType | Component;
children: ReactNode;
glContainer: Container;
glEventHub: EventEmitter;
Expand Down Expand Up @@ -125,7 +126,7 @@ class Panel extends PureComponent<PanelProps, PanelState> {
}

componentDidMount(): void {
const { componentPanel, glContainer, glEventHub } = this.props;
const { glContainer, glEventHub } = this.props;

glContainer.on('resize', this.handleResize);
glContainer.on('show', this.handleBeforeShow);
Expand All @@ -141,8 +142,15 @@ class Panel extends PureComponent<PanelProps, PanelState> {
InputFilterEvent.CLEAR_ALL_FILTERS,
this.handleClearAllFilters
);
}

glEventHub.emit(PanelEvent.MOUNT, componentPanel);
componentDidUpdate(prevProps: PanelProps): void {
const { componentPanel, glEventHub } = this.props;

// componentPanel ref could start undefined w/ WidgetLoaderPlugin wrapping panels
if (prevProps.componentPanel == null && componentPanel != null) {
glEventHub.emit(PanelEvent.MOUNT, componentPanel);
}
}

componentWillUnmount(): void {
Expand All @@ -163,7 +171,9 @@ class Panel extends PureComponent<PanelProps, PanelState> {
this.handleClearAllFilters
);

glEventHub.emit(PanelEvent.UNMOUNT, componentPanel);
if (componentPanel != null) {
glEventHub.emit(PanelEvent.UNMOUNT, componentPanel);
}
}

handleTab(tab: Tab): void {
Expand Down
Loading