-
Notifications
You must be signed in to change notification settings - Fork 33
feat: Table plugin support for GridWidgetPlugin #2478
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; |
| 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({ | ||
| 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; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm surprised this didn't already exist.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like we have 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 | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.