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
57 changes: 44 additions & 13 deletions packages/dashboard-core-plugins/src/GridWidgetPlugin.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useMemo, useRef } from 'react';
import { useCallback, useMemo, useRef, useState } from 'react';
import {
type WidgetComponentProps,
usePersistentState,
Expand All @@ -18,13 +18,14 @@ import { useSelector } from 'react-redux';
import { getSettings, type RootState } from '@deephaven/redux';
import { LoadingOverlay } from '@deephaven/components';
import { useLayoutManager, useListener } from '@deephaven/dashboard';
import { getErrorMessage } from '@deephaven/utils';
import { assertNotNull, getErrorMessage } from '@deephaven/utils';
import { useApi } from '@deephaven/jsapi-bootstrap';
import { type GridState } from '@deephaven/grid';
import { type GridRange, type GridState } from '@deephaven/grid';
import { useIrisGridModel } from './useIrisGridModel';
import useDashboardColumnFilters from './useDashboardColumnFilters';
import { InputFilterEvent } from './events';
import useGridLinker from './useGridLinker';
import { useTablePlugin } from './useTablePlugin';

export function GridWidgetPlugin({
fetch,
Expand All @@ -33,6 +34,8 @@ export function GridWidgetPlugin({
const { eventHub } = useLayoutManager();

const fetchResult = useIrisGridModel(fetch);
const model =
fetchResult.status === 'success' ? fetchResult.model : undefined;

const dh = useApi();
const irisGridUtils = useMemo(() => new IrisGridUtils(dh), [dh]);
Expand Down Expand Up @@ -90,19 +93,19 @@ export function GridWidgetPlugin({
);

const inputFilters = useDashboardColumnFilters(
fetchResult.status === 'success' ? fetchResult.model.columns : null,
fetchResult.status === 'success' &&
isIrisGridTableModelTemplate(fetchResult.model)
? fetchResult.model.table
model?.columns ?? null,
model != null && isIrisGridTableModelTemplate(model)
? model.table
: undefined
);

const irisGridRef = useRef<IrisGridType | null>(null);

const linkerProps = useGridLinker(
fetchResult.status === 'success' ? fetchResult.model : null,
irisGridRef.current
);
const { alwaysFetchColumns: linkerAlwaysFetchColumns, ...linkerProps } =
useGridLinker(
fetchResult.status === 'success' ? fetchResult.model : null,
irisGridRef.current
);

const handleClearAllFilters = useCallback(() => {
if (irisGridRef.current == null) {
Expand All @@ -117,6 +120,28 @@ export function GridWidgetPlugin({
handleClearAllFilters
);

const [selection, setSelection] = useState<readonly GridRange[]>([]);

const {
Plugin,
customFilters,
alwaysFetchColumns: filterFetchColumns,
onContextMenu,
} = useTablePlugin({
model,
irisGridRef,
irisGridUtils,
selectedRanges: selection,
});

const alwaysFetchColumns = useMemo(() => {
const columnSet = new Set([
...linkerAlwaysFetchColumns,
...filterFetchColumns,
]);
return [...columnSet];
}, [linkerAlwaysFetchColumns, filterFetchColumns]);

if (fetchResult.status === 'loading') {
return <LoadingOverlay isLoading />;
}
Expand All @@ -130,20 +155,26 @@ export function GridWidgetPlugin({
);
}

const { model } = fetchResult;
assertNotNull(model, 'Model should be defined when fetch is successful');

return (
<IrisGrid
ref={irisGridRef}
model={model}
settings={settings}
onStateChange={handleIrisGridChange}
onSelectionChanged={setSelection}
onContextMenu={onContextMenu}
inputFilters={inputFilters}
customFilters={customFilters}
// eslint-disable-next-line react/jsx-props-no-spreading
{...linkerProps}
alwaysFetchColumns={alwaysFetchColumns}
// eslint-disable-next-line react/jsx-props-no-spreading
{...hydratedState}
/>
>
{Plugin}
</IrisGrid>
);
}

Expand Down
90 changes: 90 additions & 0 deletions packages/dashboard-core-plugins/src/TablePluginWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { forwardRef, useMemo } from 'react';
import {
type TablePluginProps,
type TablePluginElement,
} from '@deephaven/plugin';
import { type IrisGridType } from '@deephaven/iris-grid';
import {
LayoutUtils,
useLayoutManager,
usePanelId,
} from '@deephaven/dashboard';
import useLoadTablePlugin from './useLoadTablePlugin';

export const TablePluginWrapper = forwardRef(
(
{
name,
model,
filter,
fetchColumns,
selectedRanges,
irisGridRef,
pluginState,
onStateChange,
}: Pick<
TablePluginProps,
| 'model'
| 'filter'
| 'fetchColumns'
| 'selectedRanges'
| 'pluginState'
| 'onStateChange'
> & {
name: string;
irisGridRef: React.MutableRefObject<IrisGridType | null>;
},
ref: React.Ref<TablePluginElement>
): JSX.Element | null => {
const loadPlugin = useLoadTablePlugin();
const Plugin = useMemo(() => loadPlugin(name), [loadPlugin, name]);

const layoutManager = useLayoutManager();
const panelId = usePanelId();
const panelName = useMemo(() => {
if (panelId == null) {
return 'unknown';
}

const panelItem = LayoutUtils.getContentItemById(
layoutManager.root,
panelId
);

return panelItem?.config.title ?? 'unknown';
}, [layoutManager.root, panelId]);

const panel = useMemo(
() => ({
irisGrid: irisGridRef,
getTableName: () => panelName,
}),
[irisGridRef, panelName]
);

return (
<div className="iris-grid-plugin">
<Plugin
ref={ref}
filter={filter}
fetchColumns={fetchColumns}
model={model}
table={model.table}
tableName={panelName}
selectedRanges={selectedRanges}
onStateChange={onStateChange}
pluginState={pluginState}
// Mimic the panel containing `irisGrid.current` for backwards compatibility
// since we don't have an IrisGridPanel to use here.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
panel={panel}
/>
</div>
);
}
);

TablePluginWrapper.displayName = 'TablePluginWrapper';

export default TablePluginWrapper;
110 changes: 110 additions & 0 deletions packages/dashboard-core-plugins/src/useTablePlugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { useCallback, useMemo, useRef, useState } from 'react';
import { usePersistentState, type TablePluginElement } from '@deephaven/plugin';
import {
type InputFilter,
type IrisGridModel,
type IrisGridProps,
type IrisGridUtils,
isIrisGridTableModelTemplate,
type IrisGridType,
type IrisGridContextMenuData,
} from '@deephaven/iris-grid';
import { type GridRange } from '@deephaven/grid';
import { TablePluginWrapper } from './TablePluginWrapper';

interface UseTablePluginProps {
/**
* The IrisGrid model for this plugin.
* Currently only IrisGridTableModelTemplate types are supported.
* Other IrisGrid model types will be ignored for now.
*/
model: IrisGridModel | undefined;
/**
* A reference to the IrisGrid component instance.
*/
irisGridRef: React.MutableRefObject<IrisGridType | null>;
/**
* A IrisGridUtils instance.
*/
irisGridUtils: IrisGridUtils;
/**
* The currently selected ranges in the grid.
*/
selectedRanges: readonly GridRange[] | undefined;
}

/**
* Hook to get a TablePlugin component and the IrisGrid props derived from the plugin.
* The returned props should be passed to the IrisGrid component or merged with other sources
* of the same props.
* @param props The properties for the table plugin. The props object itself does not need to be memoized,
* but the values inside it should be stable to avoid unnecessary re-renders.
* @returns Object containing `Plugin` key which is the Plugin component.
* The remaining object keys are IrisGrid props associated with the plugin.
*/
export function useTablePlugin({
Comment thread
mattrunyon marked this conversation as resolved.
model,
irisGridRef,
irisGridUtils,
selectedRanges,
}: UseTablePluginProps): {
Plugin: JSX.Element | null;
} & Pick<
IrisGridProps,
'customFilters' | 'alwaysFetchColumns' | 'onContextMenu'
> {
const [pluginFilters, setPluginFilters] = useState<InputFilter[]>([]);
const customFilters = useMemo(
() =>
model != null && isIrisGridTableModelTemplate(model)
? irisGridUtils.getFiltersFromInputFilters(
model.table.columns,
pluginFilters,
model.formatter.timeZone
)
: [],
[model, irisGridUtils, pluginFilters]
);
const [alwaysFetchColumns, setAlwaysFetchColumns] = useState<string[]>([]);
const pluginRef = useRef<TablePluginElement | null>(null);
const [pluginState, setPluginState] = usePersistentState<unknown>(undefined, {
version: 1,
// pluginName will be undefined on first call when re-hydrating,
// so use a constant type to avoid re-hydration issues with the persistent state type
type: 'GridWidgetTablePluginState',
});

const Plugin = useMemo(
() =>
model != null &&
isIrisGridTableModelTemplate(model) &&
model.table.pluginName != null ? (
<TablePluginWrapper
ref={pluginRef}
name={model.table.pluginName}
model={model}
filter={setPluginFilters}
fetchColumns={setAlwaysFetchColumns}
selectedRanges={selectedRanges}
irisGridRef={irisGridRef}
pluginState={pluginState}
onStateChange={setPluginState}
/>
) : null,
[model, selectedRanges, irisGridRef, pluginState, setPluginState]
);

const onContextMenu = useCallback(
(data: IrisGridContextMenuData) => pluginRef.current?.getMenu?.(data) ?? [],
[]
);

return {
Plugin,
customFilters,
alwaysFetchColumns,
onContextMenu,
};
}

export default useTablePlugin;
38 changes: 38 additions & 0 deletions packages/dashboard/src/layout/LayoutUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,41 @@ describe('isEqual', () => {
).toBe(false);
});
});

describe('getContentItemById', () => {
it('finds item with the specified ID', () => {
const root = makeContentItem('column');
const needle1 = Object.assign(makeContentItem('component'), {
config: { id: 'needle1' },
});
const needle2 = Object.assign(makeContentItem('component'), {
config: { id: 'needle2' },
});
root.addChild(needle1 as ContentItem);
root.addChild(needle2 as ContentItem);

const found = LayoutUtils.getContentItemById(
root as ContentItem,
'needle2'
);
expect(found).toEqual(needle2);
});

it('returns null if item with the specified ID not found', () => {
const root = makeContentItem('column');
const needle1 = Object.assign(makeContentItem('component'), {
config: { id: 'needle1' },
});
const needle2 = Object.assign(makeContentItem('component'), {
config: { id: 'needle2' },
});
root.addChild(needle1 as ContentItem);
root.addChild(needle2 as ContentItem);

const found = LayoutUtils.getContentItemById(
root as ContentItem,
'noItemFound'
);
expect(found).toBeNull();
});
});
30 changes: 30 additions & 0 deletions packages/dashboard/src/layout/LayoutUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,36 @@ class LayoutUtils {
return null;
}

/**
* Gets a content item by its ID
* @param item Golden layout content item to search for the content item. Typically the root.
* @param searchId the ID
*/
static getContentItemById(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised this didn't already exist.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was too. It looks like we always just used the panel or glContainer to find the item since we only accessed the GL content item from the panel component.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we have PanelManager.getContainerByPanelId which is close. I'm going to leave this util I think because the closest we can do is the logic from that which gets the stack for the item with config containing the id, then gets the item from the stack

This also lets you get a row or column which wouldn't work with the other logic (since they're not in a stack), but I can't think of a useful reason for that.

item: ContentItem,
searchId: string | string[]
): ContentItem | null {
if (item.config?.id === searchId) {
return item;
}

if (item.contentItems == null) {
return null;
}

for (let i = 0; i < item.contentItems.length; i += 1) {
const contentItem = this.getContentItemById(
item.contentItems[i],
searchId
);
if (contentItem) {
return contentItem;
}
}

return null;
}

/**
* Gets the first stack which contains a contentItem with the given config values
* @param item Golden layout content item to search for the stack
Expand Down
Loading
Loading