diff --git a/packages/code-studio/src/styleguide/MockIrisGridTreeModel.ts b/packages/code-studio/src/styleguide/MockIrisGridTreeModel.ts index 111ca875fd..b279b8823f 100644 --- a/packages/code-studio/src/styleguide/MockIrisGridTreeModel.ts +++ b/packages/code-studio/src/styleguide/MockIrisGridTreeModel.ts @@ -324,7 +324,7 @@ class MockIrisGridTreeModel throw new Error('Not defined in mock'); } - valuesTable(column: Column): Promise { + valuesTable(columns: Column | Column[]): Promise { throw new Error('Not defined in mock'); } diff --git a/packages/dashboard-core-plugins/src/panels/ChartPanel.tsx b/packages/dashboard-core-plugins/src/panels/ChartPanel.tsx index 9c7ef52871..8b6793d3a3 100644 --- a/packages/dashboard-core-plugins/src/panels/ChartPanel.tsx +++ b/packages/dashboard-core-plugins/src/panels/ChartPanel.tsx @@ -113,8 +113,8 @@ export interface GLChartPanelState { sorts: unknown; }; irisGridPanelState?: { - partitionColumn: string; - partition: unknown; + partitionColumns: string[]; + partitions: unknown[]; }; table?: string; figure?: string; diff --git a/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx b/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx index 94a32bcaa4..117be35cbc 100644 --- a/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx +++ b/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx @@ -39,6 +39,7 @@ import { IrisGridState, ChartBuilderSettings, DehydratedIrisGridState, + DehydratedIrisGridPanelState, ColumnHeaderGroup, IrisGridContextMenuData, IrisGridTableModel, @@ -78,7 +79,7 @@ import type { TablePluginComponent, TablePluginElement, } from '@deephaven/plugin'; -import { ConsoleEvent, InputFilterEvent, IrisGridEvent } from '../events'; +import { InputFilterEvent, IrisGridEvent } from '../events'; import { getInputFiltersForDashboard, getLinksForDashboard, @@ -114,12 +115,7 @@ export interface PanelState { movedRows: MoveOperation[]; }; irisGridState: DehydratedIrisGridState; - irisGridPanelState: { - partitionColumn: ColumnName | null; - partition: string | null; - isSelectingPartition: boolean; - advancedSettings: [AdvancedSettingsType, boolean][]; - }; + irisGridPanelState: DehydratedIrisGridPanelState; pluginState: unknown; } @@ -127,10 +123,12 @@ export interface PanelState { // even though they can't be undefined in the dehydrated state. // This can happen when loading the state saved before the properties were added. type LoadedPanelState = PanelState & { - irisGridPanelState: PanelState['irisGridPanelState'] & - Partial< - Pick - >; + irisGridPanelState: PanelState['irisGridPanelState'] & { + partitions?: (string | null)[]; + partitionColumns?: ColumnName[]; + partition?: string | null; + partitionColumn?: ColumnName | null; + }; }; export interface OwnProps extends DashboardPanelProps { @@ -190,8 +188,8 @@ interface IrisGridPanelState { movedColumns: readonly MoveOperation[]; movedRows: readonly MoveOperation[]; isSelectingPartition: boolean; - partition: string | null; - partitionColumn: Column | null; + partitions: (string | null)[]; + partitionColumns: Column[]; rollupConfig?: UIRollupConfig; showSearchBar: boolean; searchValue: string; @@ -252,7 +250,6 @@ export class IrisGridPanel extends PureComponent< this.handleError = this.handleError.bind(this); this.handleGridStateChange = this.handleGridStateChange.bind(this); this.handlePluginStateChange = this.handlePluginStateChange.bind(this); - this.handlePartitionAppend = this.handlePartitionAppend.bind(this); this.handleCreateChart = this.handleCreateChart.bind(this); this.handleResize = this.handleResize.bind(this); this.handleShow = this.handleShow.bind(this); @@ -298,8 +295,8 @@ export class IrisGridPanel extends PureComponent< movedColumns: [], movedRows: [], isSelectingPartition: false, - partition: null, - partitionColumn: null, + partitions: [], + partitionColumns: [], rollupConfig: undefined, showSearchBar: false, searchValue: '', @@ -460,14 +457,14 @@ export class IrisGridPanel extends PureComponent< ( model: IrisGridModel, isSelectingPartition: boolean, - partition: string | null, - partitionColumn: Column | null, + partitions: (string | null)[], + partitionColumns: Column[], advancedSettings: Map ) => IrisGridUtils.dehydrateIrisGridPanelState(model, { isSelectingPartition, - partition, - partitionColumn, + partitions, + partitionColumns, advancedSettings, }) ); @@ -713,14 +710,6 @@ export class IrisGridPanel extends PureComponent< glEventHub.emit(InputFilterEvent.TABLE_CHANGED, this, table); } - handlePartitionAppend(column: Column, value: unknown): void { - const { glEventHub } = this.props; - const { name } = column; - const tableName = this.getTableName(); - const command = `${tableName} = ${tableName}.where("${name}=\`${value}\`")`; - glEventHub.emit(ConsoleEvent.SEND_COMMAND, command, false, true); - } - /** * Create a chart with the specified settings * @param settings The settings from the chart builder @@ -1044,8 +1033,8 @@ export class IrisGridPanel extends PureComponent< } const { isSelectingPartition, - partition, - partitionColumn, + partitions, + partitionColumns, advancedSettings, } = IrisGridUtils.hydrateIrisGridPanelState(model, irisGridPanelState); assertNotNull(this.irisGridUtils); @@ -1090,8 +1079,8 @@ export class IrisGridPanel extends PureComponent< isSelectingPartition, movedColumns, movedRows, - partition, - partitionColumn, + partitions, + partitionColumns, quickFilters, reverseType, rollupConfig, @@ -1123,8 +1112,8 @@ export class IrisGridPanel extends PureComponent< model, panelState: oldPanelState, isSelectingPartition, - partition, - partitionColumn, + partitions, + partitionColumns, advancedSettings, } = this.state; const { @@ -1159,8 +1148,8 @@ export class IrisGridPanel extends PureComponent< this.getDehydratedIrisGridPanelState( model, isSelectingPartition, - partition, - partitionColumn, + partitions, + partitionColumns, advancedSettings ), this.getDehydratedIrisGridState( @@ -1247,8 +1236,8 @@ export class IrisGridPanel extends PureComponent< model, movedColumns, movedRows, - partition, - partitionColumn, + partitions, + partitionColumns, quickFilters, reverseType, rollupConfig, @@ -1329,8 +1318,8 @@ export class IrisGridPanel extends PureComponent< isStuckToRight={isStuckToRight} movedColumns={movedColumns} movedRows={movedRows} - partition={partition} - partitionColumn={partitionColumn} + partitions={partitions} + partitionColumns={partitionColumns} quickFilters={quickFilters} reverseType={reverseType} rollupConfig={rollupConfig} @@ -1348,7 +1337,6 @@ export class IrisGridPanel extends PureComponent< onCreateChart={this.handleCreateChart} onDataSelected={this.handleDataSelected} onError={this.handleError} - onPartitionAppend={this.handlePartitionAppend} onStateChange={this.handleGridStateChange} onContextMenu={this.handleContextMenu} onAdvancedSettingsChange={this.handleAdvancedSettingsChange} diff --git a/packages/iris-grid/src/IrisGrid.tsx b/packages/iris-grid/src/IrisGrid.tsx index a999e1afc3..4f8163a178 100644 --- a/packages/iris-grid/src/IrisGrid.tsx +++ b/packages/iris-grid/src/IrisGrid.tsx @@ -283,10 +283,9 @@ export interface IrisGridProps { onError: (error: unknown) => void; onDataSelected: (index: ModelIndex, map: Record) => void; onStateChange: (irisGridState: IrisGridState, gridState: GridState) => void; - onPartitionAppend?: (partitionColumn: Column, value: string) => void; onAdvancedSettingsChange: AdvancedSettingsMenuCallback; - partition: string | null; - partitionColumn: Column | null; + partitions: (string | null)[]; + partitionColumns: Column[]; sorts: readonly Sort[]; reverseType: ReverseType; quickFilters: ReadonlyQuickFilterMap | null; @@ -347,8 +346,8 @@ export interface IrisGridState { keyHandlers: readonly KeyHandler[]; mouseHandlers: readonly GridMouseHandler[]; - partition: string | null; - partitionColumn: Column | null; + partitions: (string | null)[]; + partitionColumns: Column[]; partitionTable: Table | null; partitionFilters: readonly FilterCondition[]; // setAdvancedFilter and setQuickFilter mutate the arguments @@ -463,8 +462,8 @@ export class IrisGrid extends Component { onError: (): void => undefined, onStateChange: (): void => undefined, onAdvancedSettingsChange: (): void => undefined, - partition: null, - partitionColumn: null, + partitions: [], + partitionColumns: [], quickFilters: EMPTY_MAP, selectDistinctColumns: EMPTY_ARRAY, sorts: EMPTY_ARRAY, @@ -577,7 +576,6 @@ export class IrisGrid extends Component { this.handleCancelDownloadTable = this.handleCancelDownloadTable.bind(this); this.handleDownloadCanceled = this.handleDownloadCanceled.bind(this); this.handleDownloadCompleted = this.handleDownloadCompleted.bind(this); - this.handlePartitionAppend = this.handlePartitionAppend.bind(this); this.handlePartitionChange = this.handlePartitionChange.bind(this); this.handlePartitionFetchAll = this.handlePartitionFetchAll.bind(this); this.handlePartitionDone = this.handlePartitionDone.bind(this); @@ -675,8 +673,8 @@ export class IrisGrid extends Component { model, movedColumns: movedColumnsProp, movedRows: movedRowsProp, - partition, - partitionColumn, + partitions, + partitionColumns, rollupConfig, userColumnWidths, userRowHeights, @@ -752,8 +750,8 @@ export class IrisGrid extends Component { keyHandlers, mouseHandlers, - partition, - partitionColumn, + partitions, + partitionColumns, partitionTable: null, partitionFilters: [], // setAdvancedFilter and setQuickFilter mutate the arguments @@ -841,15 +839,16 @@ export class IrisGrid extends Component { } componentDidMount(): void { - const { partitionColumn, model } = this.props; - const column = - partitionColumn ?? model.columns.find(c => c.isPartitionColumn); + const { partitionColumns, model } = this.props; + const columns = partitionColumns.length + ? partitionColumns + : model.columns.filter(c => c.isPartitionColumn); if ( model.isFilterRequired && model.isValuesTableAvailable && - column != null + columns.length ) { - this.loadPartitionsTable(column); + this.loadPartitionsTable(columns); } else { this.initState(); } @@ -1896,27 +1895,27 @@ export class IrisGrid extends Component { this.initFormatter(); } - async loadPartitionsTable(partitionColumn: Column): Promise { + async loadPartitionsTable(partitionColumns: Column[]): Promise { const { model } = this.props; this.setState({ isSelectingPartition: true }); try { const partitionTable = await this.pending.add( - model.valuesTable(partitionColumn), + model.valuesTable(partitionColumns), resolved => resolved.close() ); - const column = partitionTable.columns[0]; - const sort = column.sort().desc(); - partitionTable.applySort([sort]); - partitionTable.setViewport(0, 0, [column]); + const columns = partitionTable.columns.slice(0, partitionColumns.length); + const sorts = columns.map(column => column.sort().desc()); + partitionTable.applySort(sorts); + partitionTable.setViewport(0, 0, columns); const data = await this.pending.add(partitionTable.getViewportData()); if (data.rows.length > 0) { const row = data.rows[0]; - const value = row.get(column); + const values = columns.map(column => row.get(column)); - this.updatePartition(value, partitionColumn); + this.updatePartition(values, partitionColumns); this.setState({ isSelectingPartition: true }); } else { @@ -1924,7 +1923,7 @@ export class IrisGrid extends Component { this.setState({ isSelectingPartition: false }); this.handlePartitionFetchAll(); } - this.setState({ partitionTable, partitionColumn }, () => { + this.setState({ partitionTable, partitionColumns }, () => { this.initState(); }); } catch (error) { @@ -1932,31 +1931,41 @@ export class IrisGrid extends Component { } } - updatePartition(partition: string, partitionColumn: Column): void { - if (TableUtils.isCharType(partitionColumn.type) && partition === '') { - return; - } + updatePartition( + partitions: (string | null)[], + partitionColumns: Column[] + ): void { + const partitionFilters = []; - const { model } = this.props; + for (let i = 0; i < partitionColumns.length; i += 1) { + const partition = partitions[i]; + const partitionColumn = partitionColumns[i]; - const partitionText = TableUtils.isCharType(partitionColumn.type) - ? model.displayString( - partition, - partitionColumn.type, - partitionColumn.name - ) - : partition; - const partitionFilter = this.tableUtils.makeQuickFilterFromComponent( - partitionColumn, - partitionText - ); - if (partitionFilter === null) { - return; + if ( + partition !== null && + !(TableUtils.isCharType(partitionColumn.type) && partition === '') + ) { + const { model } = this.props; + + const partitionText = TableUtils.isCharType(partitionColumn.type) + ? model.displayString( + partition, + partitionColumn.type, + partitionColumn.name + ) + : partition; + const partitionFilter = this.tableUtils.makeQuickFilterFromComponent( + partitionColumn, + partitionText + ); + if (partitionFilter !== null) { + partitionFilters.push(partitionFilter); + } + } } - const partitionFilters = [partitionFilter]; this.setState({ - partition, + partitions, partitionFilters, }); } @@ -2362,21 +2371,12 @@ export class IrisGrid extends Component { this.isAnimating = false; } - handlePartitionAppend(value: string): void { - const { onPartitionAppend } = this.props; - const { partitionColumn } = this.state; - if (partitionColumn == null) { - return; - } - onPartitionAppend?.(partitionColumn, value); - } - - handlePartitionChange(partition: string): void { - const { partitionColumn } = this.state; - if (partitionColumn == null) { + handlePartitionChange(partitions: (string | null)[]): void { + const { partitionColumns } = this.state; + if (partitionColumns.length === 0) { return; } - this.updatePartition(partition, partitionColumn); + this.updatePartition(partitions, partitionColumns); } handlePartitionFetchAll(): void { @@ -3900,7 +3900,6 @@ export class IrisGrid extends Component { onAdvancedSettingsChange, canDownloadCsv, onCreateChart, - onPartitionAppend, } = this.props; const { metricCalculator, @@ -3922,10 +3921,10 @@ export class IrisGrid extends Component { hoverSelectColumn, quickFilters, advancedFilters, - partition, + partitions, partitionFilters, partitionTable, - partitionColumn, + partitionColumns, searchFilter, selectDistinctColumns, @@ -4444,27 +4443,24 @@ export class IrisGrid extends Component { unmountOnExit >
- {partitionTable && partitionColumn && partition != null && ( - model.displayString(value, type, stringName)} - column={partitionColumn} - partition={partition} - onChange={this.handlePartitionChange} - onFetchAll={this.handlePartitionFetchAll} - onAppend={ - onPartitionAppend !== undefined - ? this.handlePartitionAppend - : undefined - } - onDone={this.handlePartitionDone} - /> - )} + {partitionTable && + partitionColumns.length && + partitions.length && ( + model.displayString(value, type, stringName)} + columns={partitionColumns} + partitions={partitions} + onChange={this.handlePartitionChange} + onFetchAll={this.handlePartitionFetchAll} + onDone={this.handlePartitionDone} + /> + )}
; /** - * @param column The column to get the distinct values for - * @returns A table partitioned on the column specified + * @param column The columns to get the distinct values for + * @returns A table partitioned on the specified columns in the order given in */ - abstract valuesTable(column: Column): Promise; + abstract valuesTable(columns: Column | Column[]): Promise
; /** * Close this model. It can no longer be used after being closed diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.test.tsx b/packages/iris-grid/src/IrisGridPartitionSelector.test.tsx index 52b82eaeea..0c70cfea6a 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.test.tsx +++ b/packages/iris-grid/src/IrisGridPartitionSelector.test.tsx @@ -1,23 +1,26 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, fireEvent } from '@testing-library/react'; import dh from '@deephaven/jsapi-shim'; import IrisGridPartitionSelector from './IrisGridPartitionSelector'; import IrisGridTestUtils from './IrisGridTestUtils'; function makeIrisGridPartitionSelector( table = new IrisGridTestUtils(dh).makeTable(), + columns = [new IrisGridTestUtils(dh).makeColumn()], onChange = jest.fn(), onDone = jest.fn(), - getFormattedString = jest.fn(value => `${value}`) + getFormattedString = jest.fn(value => `${value}`), + onAppend = undefined ) { return render( ); } @@ -25,3 +28,45 @@ function makeIrisGridPartitionSelector( it('unmounts successfully without crashing', () => { makeIrisGridPartitionSelector(); }); + +it('calls onDone when close button is clicked', () => { + const onDone = jest.fn(); + const component = makeIrisGridPartitionSelector( + undefined, + undefined, + undefined, + onDone + ); + + const closeButton = component.getAllByRole('button')[2]; + fireEvent.click(closeButton); + expect(onDone).toHaveBeenCalled(); +}); + +it('should display multiple selectors to match columns', () => { + const columns = [ + new IrisGridTestUtils(dh).makeColumn(), + new IrisGridTestUtils(dh).makeColumn(), + ]; + const component = makeIrisGridPartitionSelector(undefined, columns); + + const selectors = component.getAllByRole('textbox'); + expect(selectors).toHaveLength(2); +}); + +it('calls handlePartitionChange when PartitionSelectorSearch value changes', () => { + const handlePartitionChange = jest.spyOn( + IrisGridPartitionSelector.prototype, + 'handlePartitionChange' + ); + const component = makeIrisGridPartitionSelector(); + + const partitionSelectorSearch = component.getByRole('textbox'); + fireEvent.change(partitionSelectorSearch, { target: { value: 'test' } }); + expect(handlePartitionChange).toHaveBeenCalledWith( + 0, + expect.objectContaining({ + target: expect.objectContaining({ value: 'test' }), + }) + ); +}); diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.tsx b/packages/iris-grid/src/IrisGridPartitionSelector.tsx index a031dffab3..7b9b3cd77e 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.tsx +++ b/packages/iris-grid/src/IrisGridPartitionSelector.tsx @@ -17,15 +17,15 @@ interface IrisGridPartitionSelectorProps { dh: DhType; getFormattedString: (value: T, type: string, name: string) => string; table: Table; - column: Column; - partition: string; - onAppend?: (partition: string) => void; + columns: Column[]; + partitions: (string | null)[]; onFetchAll: () => void; onDone: (event?: React.MouseEvent) => void; - onChange: (partition: string) => void; + onChange: (partitions: (string | null)[]) => void; } interface IrisGridPartitionSelectorState { - partition: string; + partitions: (string | null)[]; + partitionTables: Table[] | null; } class IrisGridPartitionSelector extends Component< IrisGridPartitionSelectorProps, @@ -35,13 +35,12 @@ class IrisGridPartitionSelector extends Component< onChange: (): void => undefined, onFetchAll: (): void => undefined, onDone: (): void => undefined, - partition: '', + partitions: [], }; constructor(props: IrisGridPartitionSelectorProps) { super(props); - this.handleAppendClick = this.handleAppendClick.bind(this); this.handleCloseClick = this.handleCloseClick.bind(this); this.handleIgnoreClick = this.handleIgnoreClick.bind(this); this.handlePartitionChange = this.handlePartitionChange.bind(this); @@ -51,30 +50,38 @@ class IrisGridPartitionSelector extends Component< this.handleSearchOpened = this.handleSearchOpened.bind(this); this.handleSearchClosed = this.handleSearchClosed.bind(this); - this.searchMenu = null; - this.selectorSearch = null; + const { dh, columns, partitions } = props; + this.tableUtils = new TableUtils(dh); + this.searchMenu = columns.map(() => null); + this.selectorSearch = columns.map(() => null); - const { partition } = props; this.state = { - partition, + partitions, + partitionTables: null, }; } + async componentDidMount(): Promise { + const { columns, table } = this.props; + const { partitions } = this.state; + + const partitionTables = await Promise.all( + columns.map(async (_, i) => table.selectDistinct(columns.slice(0, i + 1))) + ); + this.updatePartitionFilters(partitions, partitionTables); + } + componentWillUnmount(): void { + const { partitionTables } = this.state; + partitionTables?.forEach(table => table.close()); this.debounceUpdate.cancel(); } - searchMenu: DropdownMenu | null; + tableUtils: TableUtils; - selectorSearch: PartitionSelectorSearch | null; + searchMenu: (DropdownMenu | null)[]; - handleAppendClick(): void { - log.debug2('handleAppendClick'); - - const { onAppend } = this.props; - const { partition } = this.state; - onAppend?.(partition); - } + selectorSearch: (PartitionSelectorSearch | null)[]; handleCloseClick(): void { log.debug2('handleCloseClick'); @@ -88,35 +95,54 @@ class IrisGridPartitionSelector extends Component< this.sendFetchAll(); } - handlePartitionChange(event: React.ChangeEvent): void { + handlePartitionChange( + index: number, + event: React.ChangeEvent + ): void { log.debug2('handlePartitionChange'); - const { column } = this.props; + const { columns } = this.props; + const { partitions, partitionTables } = this.state; const { value: partition } = event.target; + const newPartitions = [...partitions]; + newPartitions[index] = + TableUtils.isCharType(columns[index].type) && partition.length > 0 + ? partition.charCodeAt(0).toString() + : partition; + if (partitionTables) { + this.updatePartitionFilters(newPartitions, partitionTables); + } + this.setState({ - partition: - TableUtils.isCharType(column.type) && partition.length > 0 - ? partition.charCodeAt(0).toString() - : partition, + partitions: newPartitions, }); this.debounceUpdate(); } - handlePartitionSelect(partition: string): void { - if (this.searchMenu) { - this.searchMenu.closeMenu(); + handlePartitionSelect(index: number, partition: string): void { + const { partitions, partitionTables } = this.state; + const selectedMenu = this.searchMenu[index]; + if (selectedMenu) { + selectedMenu.closeMenu(); + } + + const newPartitions = [...partitions]; + newPartitions[index] = partition; + if (partitionTables) { + this.updatePartitionFilters(newPartitions, partitionTables); } - this.setState({ partition }, () => { + this.setState({ partitions: newPartitions }, () => { this.sendUpdate(); }); } - handlePartitionListResized(): void { - if (this.searchMenu) { - this.searchMenu.scheduleUpdate(); + handlePartitionListResized(index: number): void { + const selectedMenu = this.searchMenu[index]; + if (selectedMenu) { + selectedMenu.scheduleUpdate(); } } @@ -126,9 +152,10 @@ class IrisGridPartitionSelector extends Component< table.applyFilter([]); } - handleSearchOpened(): void { - if (this.selectorSearch) { - this.selectorSearch.focus(); + handleSearchOpened(index: number): void { + const selectedSearch = this.selectorSearch[index]; + if (selectedSearch) { + selectedSearch.focus(); } } @@ -147,8 +174,8 @@ class IrisGridPartitionSelector extends Component< log.debug2('sendUpdate'); const { onChange } = this.props; - const { partition } = this.state; - onChange(partition); + const { partitions } = this.state; + onChange(partitions); } sendFetchAll(): void { @@ -160,38 +187,89 @@ class IrisGridPartitionSelector extends Component< onFetchAll(); } + getDisplayValue(column: Column, index: number): string { + const { partitions } = this.state; + const partition = partitions[index]; + if (partition == null) { + return ''; + } + if (TableUtils.isCharType(column.type) && partition.toString().length > 0) { + return String.fromCharCode(parseInt(partition, 10)); + } + return IrisGridUtils.convertValueToText(partition, column.type); + } + + async updatePartitionFilters( + partitions: (string | null)[], + partitionTables: Table[] + ): Promise { + const { columns, getFormattedString } = this.props; + + const partitionFilters = []; + for (let i = 0; i < columns.length - 1; i += 1) { + const partition = partitions[i]; + const partitionColumn = columns[i]; + + partitionTables[i]?.applyFilter(partitionFilters); + if ( + partition !== null && + !(TableUtils.isCharType(partitionColumn.type) && partition === '') + ) { + const partitionText = TableUtils.isCharType(partitionColumn.type) + ? getFormattedString( + partition as T, + partitionColumn.type, + partitionColumn.name + ) + : partition; + const partitionFilter = this.tableUtils.makeQuickFilterFromComponent( + partitionColumn, + partitionText + ); + if (partitionFilter !== null) { + partitionFilters.push(partitionFilter); + } + } + } + partitionTables[partitionTables.length - 1]?.applyFilter(partitionFilters); + this.setState({ partitionTables }); + } + render(): JSX.Element { - const { column, dh, getFormattedString, onAppend, onDone, table } = - this.props; - const { partition } = this.state; - const partitionSelectorSearch = ( - { - this.selectorSearch = selectorSearch; - }} - /> + const { columns, dh, getFormattedString, onDone } = this.props; + const { partitionTables } = this.state; + + const partitionSelectorSearch = columns.map( + (column, index) => + partitionTables && ( + + this.handlePartitionSelect(index, partition) + } + onListResized={() => this.handlePartitionListResized(index)} + ref={selectorSearch => { + this.selectorSearch[index] = selectorSearch; + }} + /> + ) ); - return ( -
+ const partitionSelectors = columns.map((column, index) => ( + <>
- Filtering "{column.name}" partition to + {column.name}:
0 - ? String.fromCharCode(parseInt(partition, 10)) - : IrisGridUtils.convertValueToText(partition, column.type) - } - onChange={this.handlePartitionChange} + value={this.getDisplayValue(column, index)} + onChange={e => { + this.handlePartitionChange(index, e); + }} className="form-control input-partition" />
@@ -200,16 +278,25 @@ class IrisGridPartitionSelector extends Component< Partitions { - this.searchMenu = searchMenu; + this.searchMenu[index] = searchMenu; + }} + actions={[ + { menuElement: partitionSelectorSearch[index] ?? undefined }, + ]} + onMenuOpened={() => { + this.handleSearchOpened(index); }} - actions={[{ menuElement: partitionSelectorSearch }]} - onMenuOpened={this.handleSearchOpened} onMenuClosed={this.handleSearchClosed} />
+ + )); + return ( +
+ {partitionSelectors} - {onAppend !== undefined && ( - - )}
{ + async valuesTable(columns: Column | Column[]): Promise
{ let table = null; try { table = await this.table.copy(); table.applyFilter([]); table.applySort([]); - return table.selectDistinct([column]); + return table.selectDistinct(Array.isArray(columns) ? columns : [columns]); } finally { if (table != null) { table.close(); diff --git a/packages/iris-grid/src/IrisGridTestUtils.ts b/packages/iris-grid/src/IrisGridTestUtils.ts index 7eede804a0..364b630503 100644 --- a/packages/iris-grid/src/IrisGridTestUtils.ts +++ b/packages/iris-grid/src/IrisGridTestUtils.ts @@ -93,7 +93,12 @@ class IrisGridTestUtils { columns = this.makeColumns(), size = 1000000000, sort = [], - layoutHints = {} as LayoutHints, + layoutHints = {}, + }: { + columns?: Column[]; + size?: number; + sort?: readonly Sort[]; + layoutHints?: LayoutHints; } = {}): Table { // eslint-disable-next-line @typescript-eslint/no-explicit-any const table = new (this.dh as any).Table({ columns, size, sort }); diff --git a/packages/iris-grid/src/IrisGridUtils.test.ts b/packages/iris-grid/src/IrisGridUtils.test.ts index 558bda527c..a9507c8952 100644 --- a/packages/iris-grid/src/IrisGridUtils.test.ts +++ b/packages/iris-grid/src/IrisGridUtils.test.ts @@ -10,35 +10,12 @@ import IrisGridTestUtils from './IrisGridTestUtils'; import IrisGridUtils, { DehydratedSort, LegacyDehydratedSort, + isPanelStateV1, } from './IrisGridUtils'; const irisGridUtils = new IrisGridUtils(dh); const irisGridTestUtils = new IrisGridTestUtils(dh); -function makeFilter() { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return new (dh as any).FilterCondition(); -} - -function makeColumns(count = 30) { - const columns: Column[] = []; - - for (let i = 0; i < count; i += 1) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const column = new (dh as any).Column({ index: i, name: `name_${i}` }); - columns.push(column); - } - - return columns; -} - -function makeTable({ - columns = makeColumns(), - sort = [] as Sort[], -} = {}): Table { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return new (dh as any).Table({ columns, sort }); -} function makeColumn(index: number): Column { return irisGridTestUtils.makeColumn( `${index}`, @@ -47,6 +24,16 @@ function makeColumn(index: number): Column { ); } +function makeTable({ + columns = irisGridTestUtils.makeColumns(10, 'name_'), + sort = [] as Sort[], +} = {}): Table { + return irisGridTestUtils.makeTable({ + columns, + sort, + }); +} + describe('quickfilters tests', () => { it('exports/imports empty list', () => { const table = irisGridTestUtils.makeTable(); @@ -62,10 +49,10 @@ describe('quickfilters tests', () => { }); it('exports/imports quickFilters', () => { - const table = makeTable(); + const table = irisGridTestUtils.makeTable(); const column = 9; const text = '>1000'; - const filter = makeFilter(); + const filter = irisGridTestUtils.makeFilter(); const quickFilters = new Map([[column, { text, filter }]]); const exportedFilters = IrisGridUtils.dehydrateQuickFilters(quickFilters); @@ -93,7 +80,7 @@ describe('quickfilters tests', () => { describe('advanced filter tests', () => { it('exports/imports empty list', () => { - const table = makeTable(); + const table = irisGridTestUtils.makeTable(); const filters = new Map(); const exportedFilters = irisGridUtils.dehydrateAdvancedFilters( table.columns, @@ -112,7 +99,7 @@ describe('advanced filter tests', () => { it('exports advanced filters', () => { const table = makeTable(); const column = 7; - const filter = makeFilter(); + const filter = irisGridTestUtils.makeFilter(); const options = { filterItems: [{ selectedType: '', value: '', key: 0 }], filterOperators: [], @@ -151,7 +138,7 @@ describe('advanced filter tests', () => { describe('sort exporting/importing', () => { it('exports/imports empty sort', () => { const sort = []; - const table = makeTable({ sort }); + const table = irisGridTestUtils.makeTable({ sort }); const exportedSort = IrisGridUtils.dehydrateSort(sort); expect(exportedSort).toEqual([]); @@ -160,7 +147,7 @@ describe('sort exporting/importing', () => { }); it('should export (dehydrate) sorts', () => { - const columns = makeColumns(); + const columns = irisGridTestUtils.makeColumns(10, 'name_'); const sort = [columns[3].sort(), columns[7].sort().abs().desc()]; const dehydratedSorts = IrisGridUtils.dehydrateSort(sort); @@ -171,9 +158,9 @@ describe('sort exporting/importing', () => { }); describe('should import (hydrate) sorts', () => { - const columns = makeColumns(); + const columns = irisGridTestUtils.makeColumns(10, 'name_'); const sort = [columns[3].sort(), columns[7].sort().abs().desc()]; - const table = makeTable({ columns, sort }); + const table = irisGridTestUtils.makeTable({ columns, sort }); const dehydratedSorts = IrisGridUtils.dehydrateSort(sort); @@ -210,7 +197,7 @@ describe('sort exporting/importing', () => { describe('pendingDataMap hydration/dehydration', () => { it('dehydrates/hydrates empty map', () => { const pendingDataMap = new Map(); - const columns = makeColumns(); + const columns = irisGridTestUtils.makeColumns(10, 'name_'); const dehydratedMap = irisGridUtils.dehydratePendingDataMap( columns, pendingDataMap @@ -242,7 +229,7 @@ describe('pendingDataMap hydration/dehydration', () => { }, ], ]); - const columns = makeColumns(); + const columns = irisGridTestUtils.makeColumns(10, 'name_'); const dehydratedMap = irisGridUtils.dehydratePendingDataMap( columns, pendingDataMap @@ -488,7 +475,7 @@ describe('validate copy ranges', () => { describe('changeFilterColumnNamesToIndexes', () => { const DEFAULT_FILTER = {}; - const columns = makeColumns(10); + const columns = irisGridTestUtils.makeColumns(10, 'name_'); it('Replaces column names with indexes', () => { const filters = [ { name: 'name_1', filter: DEFAULT_FILTER }, @@ -658,8 +645,8 @@ describe('dehydration methods', () => { 'dehydrateIrisGridPanelState', IrisGridUtils.dehydrateIrisGridPanelState(irisGridTestUtils.makeModel(), { isSelectingPartition: false, - partition: null, - partitionColumn: null, + partitions: [], + partitionColumns: [], advancedSettings: new Map(), }), ], @@ -680,3 +667,138 @@ describe('dehydration methods', () => { ).toBe(true); }); }); + +describe('hydration methods', () => { + const model = irisGridTestUtils.makeModel( + irisGridTestUtils.makeTable({ + columns: irisGridTestUtils.makeColumns(5, 'name_'), + }) + ); + + it.each([ + [ + 'hydrateIrisGridPanelStateV1', + { + isSelectingPartition: false, + partition: null, + partitionColumn: 'INVALID', + advancedSettings: [], + }, + ], + [ + 'hydrateIrisGridPanelStateV2', + { + isSelectingPartition: false, + partitions: [null], + partitionColumns: ['INVALID'], + advancedSettings: [], + }, + ], + ])('%s invalid column error', (_label, panelState) => { + expect(() => + IrisGridUtils.hydrateIrisGridPanelState(model, panelState) + ).toThrow('Invalid partition column INVALID'); + }); + + it.each([ + [ + 'hydrateIrisGridPanelStateV1 null partition column', + { + isSelectingPartition: false, + partition: null, + partitionColumn: null, + advancedSettings: [], + }, + ], + [ + 'hydrateIrisGridPanelStateV1 null partition', + { + isSelectingPartition: false, + partition: null, + partitionColumn: 'name_0', + advancedSettings: [], + }, + ], + [ + 'hydrateIrisGridPanelStateV1 unselected partition', + { + isSelectingPartition: false, + partition: 'a', + partitionColumn: 'name_0', + advancedSettings: [], + }, + ], + [ + 'hydrateIrisGridPanelStateV1 one selected partition', + { + isSelectingPartition: true, + partition: 'a', + partitionColumn: 'name_0', + advancedSettings: [], + }, + ], + [ + 'hydrateIrisGridPanelStateV2 no partition columns', + { + isSelectingPartition: false, + partitions: [], + partitionColumns: [], + advancedSettings: [], + }, + ], + [ + 'hydrateIrisGridPanelStateV2 two unselected columns', + { + isSelectingPartition: true, + partitions: [null, null], + partitionColumns: ['name_0', 'name_1'], + advancedSettings: [], + }, + ], + [ + 'hydrateIrisGridPanelStateV2 two selected columns', + { + isSelectingPartition: true, + partitions: ['a', 'b'], + partitionColumns: ['name_0', 'name_1'], + advancedSettings: [], + }, + ], + [ + 'hydrateIrisGridPanelStateV2 mixed selection columns', + { + isSelectingPartition: true, + partitions: [null, 'b', null], + partitionColumns: ['name_0', 'name_1', 'name_2'], + advancedSettings: [], + }, + ], + [ + 'hydrateIrisGridPanelStateV2 mixed selection columns', + { + isSelectingPartition: true, + partitions: ['a', null, 'b'], + partitionColumns: ['name_0', 'name_1', 'name_2'], + advancedSettings: [], + }, + ], + ])('%s partitions and columns match', (_label, panelState) => { + const result = IrisGridUtils.hydrateIrisGridPanelState(model, panelState); + expect(result.isSelectingPartition).toBe(panelState.isSelectingPartition); + if (isPanelStateV1(panelState)) { + expect(result.partitions).toEqual([panelState.partition]); + if (panelState.partitionColumn !== null) { + expect(result.partitionColumns[0].name).toBe( + panelState.partitionColumn + ); + } else { + expect(result.partitionColumns).toEqual([]); + } + } else { + expect(result.partitions).toEqual(panelState.partitions); + panelState.partitionColumns.forEach((partition, index) => { + expect(result.partitionColumns[index].name === partition).toBeTruthy(); + }); + } + }); +}); diff --git a/packages/iris-grid/src/IrisGridUtils.ts b/packages/iris-grid/src/IrisGridUtils.ts index b9c7b72f9b..cdbcab5b74 100644 --- a/packages/iris-grid/src/IrisGridUtils.ts +++ b/packages/iris-grid/src/IrisGridUtils.ts @@ -116,8 +116,8 @@ export interface TableSettings { advancedFilters?: readonly DehydratedAdvancedFilter[]; inputFilters?: readonly InputFilter[]; sorts?: readonly (DehydratedSort | LegacyDehydratedSort)[]; - partition?: unknown; - partitionColumn?: ColumnName | null; + partitions?: unknown[]; + partitionColumns?: ColumnName[]; } export interface DehydratedIrisGridState { @@ -143,6 +143,40 @@ export interface DehydratedIrisGridState { columnHeaderGroups?: readonly ColumnGroup[]; } +export interface DehydratedIrisGridPanelStateV1 { + isSelectingPartition: boolean; + partition: string | null; + partitionColumn: ColumnName | null; + advancedSettings: [AdvancedSettingsType, boolean][]; +} + +export interface DehydratedIrisGridPanelStateV2 { + isSelectingPartition: boolean; + partitions: (string | null)[]; + partitionColumns: ColumnName[]; + advancedSettings: [AdvancedSettingsType, boolean][]; +} + +export type DehydratedIrisGridPanelState = + | DehydratedIrisGridPanelStateV1 + | DehydratedIrisGridPanelStateV2; + +export function isPanelStateV1( + state: DehydratedIrisGridPanelState +): state is DehydratedIrisGridPanelStateV1 { + return ( + (state as DehydratedIrisGridPanelStateV1).partitionColumn !== undefined + ); +} + +export function isPanelStateV2( + state: DehydratedIrisGridPanelState +): state is DehydratedIrisGridPanelStateV2 { + return Array.isArray( + (state as DehydratedIrisGridPanelStateV2).partitionColumns + ); +} + /** * Checks if an index is valid for the given array * @param x The index to check @@ -275,28 +309,25 @@ class IrisGridUtils { irisGridPanelState: { // This needs to be changed after IrisGridPanel is done isSelectingPartition: boolean; - partition: string | null; - partitionColumn: Column | null; + partitions: (string | null)[]; + partitionColumns: Column[]; advancedSettings: Map; } - ): { - isSelectingPartition: boolean; - partition: string | null; - partitionColumn: ColumnName | null; - advancedSettings: [AdvancedSettingsType, boolean][]; - } { + ): DehydratedIrisGridPanelState { const { isSelectingPartition, - partition, - partitionColumn, + partitions, + partitionColumns, advancedSettings, } = irisGridPanelState; // Return value will be serialized, should not contain undefined return { isSelectingPartition, - partition, - partitionColumn: partitionColumn != null ? partitionColumn.name : null, + partitions, + partitionColumns: partitionColumns.map( + partitionColumn => partitionColumn.name + ), advancedSettings: [...advancedSettings], }; } @@ -309,34 +340,36 @@ class IrisGridUtils { */ static hydrateIrisGridPanelState( model: IrisGridModel, - irisGridPanelState: { - // This needs to be changed after IrisGridPanel is done - isSelectingPartition: boolean; - partition: string | null | undefined; - partitionColumn: ColumnName | null | undefined; - advancedSettings: [AdvancedSettingsType, boolean][]; - } + irisGridPanelState: DehydratedIrisGridPanelState ): { isSelectingPartition: boolean; - partition: string | null; - partitionColumn: Column | null; + partitions: (string | null)[]; + partitionColumns: Column[]; advancedSettings: Map; } { - const { - isSelectingPartition, - partition, - partitionColumn, - advancedSettings, - } = irisGridPanelState; + const { isSelectingPartition, advancedSettings } = irisGridPanelState; + + const { partitionColumns, partitions } = isPanelStateV2(irisGridPanelState) + ? irisGridPanelState + : { + partitionColumns: + irisGridPanelState.partitionColumn !== null + ? [irisGridPanelState.partitionColumn] + : [], + partitions: [irisGridPanelState.partition], + }; const { columns } = model; return { isSelectingPartition, - partition: partition ?? null, - partitionColumn: - partitionColumn != null - ? IrisGridUtils.getColumnByName(columns, partitionColumn) ?? null - : null, + partitions, + partitionColumns: partitionColumns.map(partitionColumn => { + const column = IrisGridUtils.getColumnByName(columns, partitionColumn); + if (column === undefined) { + throw new Error(`Invalid partition column ${partitionColumn}`); + } + return column; + }), advancedSettings: new Map([ ...AdvancedSettings.DEFAULTS, ...advancedSettings, @@ -386,29 +419,34 @@ class IrisGridUtils { static extractTableSettings( panelState: { irisGridState: { advancedFilters: AF; quickFilters: QF; sorts: S }; - irisGridPanelState: { - partitionColumn: ColumnName | null; - partition: unknown; - }; + irisGridPanelState: DehydratedIrisGridPanelState; }, inputFilters: InputFilter[] = [] ): { - partitionColumn: ColumnName | null; - partition: unknown; + partitionColumns: ColumnName[]; + partitions: unknown[]; advancedFilters: AF; inputFilters: InputFilter[]; quickFilters: QF; sorts: S; } { const { irisGridPanelState, irisGridState } = panelState; - const { partitionColumn, partition } = irisGridPanelState; + const { partitionColumns, partitions } = isPanelStateV2(irisGridPanelState) + ? irisGridPanelState + : { + partitionColumns: + irisGridPanelState.partitionColumn !== null + ? [irisGridPanelState.partitionColumn] + : [], + partitions: [irisGridPanelState.partition], + }; const { advancedFilters, quickFilters, sorts } = irisGridState; return { advancedFilters, inputFilters, - partition, - partitionColumn, + partitions, + partitionColumns, quickFilters, sorts, }; @@ -1636,17 +1674,25 @@ class IrisGridUtils { } let filters = [...quickFilters, ...advancedFilters]; - const { partition, partitionColumn: partitionColumnName } = tableSettings; - if (partition != null && partitionColumnName != null) { - const partitionColumn = IrisGridUtils.getColumnByName( - columns, - partitionColumnName + const { partitions, partitionColumns: partitionColumnNames } = + tableSettings; + if ( + partitions && + partitions.length && + partitionColumnNames && + partitionColumnNames?.length + ) { + const partitionColumns = partitionColumnNames.map(partitionColumnName => + IrisGridUtils.getColumnByName(columns, partitionColumnName) ); - if (partitionColumn) { - const partitionFilter = partitionColumn - .filter() - .eq(this.dh.FilterValue.ofString(partition)); - filters = [partitionFilter, ...filters]; + for (let i = 0; i < partitionColumns.length; i += 1) { + if (partitionColumns[i] !== undefined && partitions[i] != null) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const partitionFilter = partitionColumns[i]!.filter().eq( + this.dh.FilterValue.ofString(partitions[i]) + ); + filters = [partitionFilter, ...filters]; + } } } filters = [...inputFilters, ...filters]; diff --git a/packages/iris-grid/src/PartitionSelectorSearch.test.tsx b/packages/iris-grid/src/PartitionSelectorSearch.test.tsx index fdb8f2f679..4a3c889cb9 100644 --- a/packages/iris-grid/src/PartitionSelectorSearch.test.tsx +++ b/packages/iris-grid/src/PartitionSelectorSearch.test.tsx @@ -15,6 +15,7 @@ function makePartitionSelectorSearch({ } = {}) { return render( { + column: Column; dh: DhType; getFormattedString: (value: T, type: string, name: string) => string; table: Table; @@ -202,14 +203,14 @@ class PartitionSelectorSearch extends Component< } handleTableUpdate(event: CustomEvent): void { + const { column } = this.props; const data = event.detail; - const { offset } = data; + const { offset, rows } = data; const items = [] as Item[]; const { getFormattedString, table } = this.props; - const column = table.columns[0]; - for (let r = 0; r < data.rows.length; r += 1) { - const row = data.rows[r]; + for (let r = 0; r < rows.length; r += 1) { + const row = rows[r]; const value = row.get(column); const displayValue = getFormattedString(value, column.type, column.name); items.push({ @@ -226,10 +227,10 @@ class PartitionSelectorSearch extends Component< handleTextChange(event: React.ChangeEvent): void { log.debug2('handleTextChange'); - const { table } = this.props; + const { column } = this.props; const { value: text } = event.target; - if (text !== '' && TableUtils.isIntegerType(table.columns[0].type)) { + if (text !== '' && TableUtils.isIntegerType(column.type)) { this.setState({ text: parseInt(text, 10).toString() }); } else { this.setState({ text }); @@ -279,12 +280,11 @@ class PartitionSelectorSearch extends Component< } updateFilter(): void { - const { initialPageSize, table } = this.props; + const { column, initialPageSize, table } = this.props; const { text } = this.state; const filterText = text.trim(); const filters = []; if (filterText.length > 0) { - const column = table.columns[0]; const filter = this.tableUtils.makeQuickFilterFromComponent( column, TableUtils.isStringType(column.type) ? `~${filterText}` : filterText @@ -304,7 +304,7 @@ class PartitionSelectorSearch extends Component< } render(): JSX.Element { - const { table } = this.props; + const { column } = this.props; const { isLoading, itemCount, items, offset, text } = this.state; const listHeight = @@ -312,9 +312,7 @@ class PartitionSelectorSearch extends Component< ItemList.DEFAULT_ROW_HEIGHT + // Adjust for ListItem vertical padding - .375rem ~ 5.25px 11; - const inputType = TableUtils.isNumberType(table.columns[0].type) - ? 'number' - : 'text'; + const inputType = TableUtils.isNumberType(column.type) ? 'number' : 'text'; return (