From 9c9421071078930efa07cc192b502a883bc1225e Mon Sep 17 00:00:00 2001 From: georgecwan Date: Mon, 23 Oct 2023 13:56:56 -0700 Subject: [PATCH 01/33] Create IrisGridPartitionedTableModel --- packages/console/src/common/ConsoleUtils.ts | 3 +- packages/console/src/common/ObjectIcon.tsx | 1 + .../dashboard-core-plugins/src/GridPlugin.tsx | 1 + packages/iris-grid/src/EmptyIrisGridModel.ts | 191 ++++++++ packages/iris-grid/src/IrisGrid.tsx | 2 +- packages/iris-grid/src/IrisGridModel.ts | 29 +- .../iris-grid/src/IrisGridModelFactory.ts | 15 +- .../src/IrisGridPartitionedTableModel.tsx | 437 ++++++++++++++++++ packages/iris-grid/src/IrisGridProxyModel.ts | 32 +- packages/iris-grid/src/IrisGridTableModel.ts | 6 +- .../src/IrisGridTableModelTemplate.ts | 22 +- .../iris-grid/src/IrisGridTreeTableModel.ts | 5 +- packages/iris-grid/src/IrisGridUtils.ts | 4 +- packages/iris-grid/src/index.ts | 3 +- packages/jsapi-types/src/dh.types.ts | 23 + packages/jsapi-utils/src/TableUtils.ts | 7 + 16 files changed, 756 insertions(+), 25 deletions(-) create mode 100644 packages/iris-grid/src/EmptyIrisGridModel.ts create mode 100644 packages/iris-grid/src/IrisGridPartitionedTableModel.tsx diff --git a/packages/console/src/common/ConsoleUtils.ts b/packages/console/src/common/ConsoleUtils.ts index 4f679c1451..bebd6816ed 100644 --- a/packages/console/src/common/ConsoleUtils.ts +++ b/packages/console/src/common/ConsoleUtils.ts @@ -56,7 +56,8 @@ class ConsoleUtils { return ( type === dh.VariableType.TABLE || type === dh.VariableType.TREETABLE || - type === dh.VariableType.HIERARCHICALTABLE + type === dh.VariableType.HIERARCHICALTABLE || + type === dh.VariableType.PARTITIONEDTABLE ); } diff --git a/packages/console/src/common/ObjectIcon.tsx b/packages/console/src/common/ObjectIcon.tsx index 3143947fa4..35f93f6484 100644 --- a/packages/console/src/common/ObjectIcon.tsx +++ b/packages/console/src/common/ObjectIcon.tsx @@ -14,6 +14,7 @@ function ObjectIcon({ type }: ObjectIconProps): JSX.Element { case dh.VariableType.TABLEMAP: case dh.VariableType.TREETABLE: case dh.VariableType.HIERARCHICALTABLE: + case dh.VariableType.PARTITIONEDTABLE: return ; case dh.VariableType.FIGURE: return ; diff --git a/packages/dashboard-core-plugins/src/GridPlugin.tsx b/packages/dashboard-core-plugins/src/GridPlugin.tsx index d2742bb7a2..5a860dc4a8 100644 --- a/packages/dashboard-core-plugins/src/GridPlugin.tsx +++ b/packages/dashboard-core-plugins/src/GridPlugin.tsx @@ -20,6 +20,7 @@ export function GridPlugin( dh.VariableType.TABLE, dh.VariableType.TREETABLE, dh.VariableType.HIERARCHICALTABLE, + dh.VariableType.PARTITIONEDTABLE, ], [dh] ); diff --git a/packages/iris-grid/src/EmptyIrisGridModel.ts b/packages/iris-grid/src/EmptyIrisGridModel.ts new file mode 100644 index 0000000000..ced1cbd0e7 --- /dev/null +++ b/packages/iris-grid/src/EmptyIrisGridModel.ts @@ -0,0 +1,191 @@ +/* eslint class-methods-use-this: "off" */ +import { + GridRange, + ModelIndex, + MoveOperation, + VisibleIndex, +} from '@deephaven/grid'; +import { + Column, + ColumnStatistics, + CustomColumn, + dh as DhType, + FilterCondition, + Format, + RollupConfig, + Row, + Sort, + Table, +} from '@deephaven/jsapi-types'; +import { ColumnName, Formatter } from '@deephaven/jsapi-utils'; +import IrisGridModel from './IrisGridModel'; +import ColumnHeaderGroup from './ColumnHeaderGroup'; +import { + PendingDataErrorMap, + PendingDataMap, + UITotalsTableConfig, +} from './CommonTypes'; + +class EmptyIrisGridModel extends IrisGridModel { + constructor(dh: DhType) { + super(dh); + + this.dh = dh; + this.formatter = new Formatter(dh); + } + + dh: DhType; + + table: Table | null = null; + + columns: Column[] = []; + + filter: FilterCondition[] = []; + + partition: unknown[] = []; + + formatter: Formatter; + + sort: Sort[] = []; + + customColumns: ColumnName[] = []; + + formatColumns: CustomColumn[] = []; + + rollupConfig: RollupConfig | null = null; + + totalsConfig: UITotalsTableConfig | null = null; + + selectDistinctColumns: ColumnName[] = []; + + pendingDataMap: PendingDataMap = new Map(); + + pendingRowCount = 0; + + columnHeaderGroups: ColumnHeaderGroup[] = []; + + get rowCount(): number { + return 0; + } + + get columnCount(): number { + return 0; + } + + textForCell(column: number, row: number): string { + return ''; + } + + textForColumnHeader(column: ModelIndex, depth?: number): string | undefined { + return undefined; + } + + getColumnIndexByName(name: string): number | undefined { + return undefined; + } + + get initialMovedColumns(): readonly MoveOperation[] { + return []; + } + + get initialMovedRows(): readonly MoveOperation[] { + return []; + } + + get initialColumnHeaderGroups(): readonly ColumnHeaderGroup[] { + return []; + } + + get groupedColumns(): readonly Column[] { + return []; + } + + formatForCell(column: ModelIndex, row: ModelIndex): Format | undefined { + return undefined; + } + + valueForCell(column: ModelIndex, row: ModelIndex): unknown { + return undefined; + } + + displayString( + value: unknown, + columnType: string, + columnName?: ColumnName + ): string { + return ''; + } + + updateFrozenColumns(columns: readonly ColumnName[]): void { + // Do nothing + } + + export(): Promise { + throw new Error('Method not implemented.'); + } + + columnStatistics(column: Column): Promise { + throw new Error('Method not implemented.'); + } + + get pendingDataErrors(): PendingDataErrorMap { + return new Map(); + } + + commitPending(): Promise { + return Promise.resolve(); + } + + setViewport( + top: VisibleIndex, + bottom: VisibleIndex, + columns?: Column[] + ): void { + // Do nothing + } + + snapshot(ranges: readonly GridRange[]): Promise { + return Promise.resolve([]); + } + + textSnapshot( + ranges: readonly GridRange[], + includeHeaders?: boolean, + formatValue?: (value: unknown, column: Column, row?: Row) => string + ): Promise { + return Promise.resolve(''); + } + + valuesTable(column: Column): Promise
{ + throw new Error('Method not implemented.'); + } + + delete(ranges: readonly GridRange[]): Promise { + return Promise.resolve(); + } + + seekRow( + startRow: number, + column: Column, + valueType: unknown, + value: unknown, + insensitive?: boolean, + contains?: boolean, + isBackwards?: boolean + ): Promise { + return Promise.resolve(0); + } + + get columnHeaderGroupMap(): ReadonlyMap { + return new Map(); + } + + getColumnHeaderParentGroup( + modelIndex: ModelIndex, + depth: number + ): ColumnHeaderGroup | undefined { + return undefined; + } +} + +export default EmptyIrisGridModel; diff --git a/packages/iris-grid/src/IrisGrid.tsx b/packages/iris-grid/src/IrisGrid.tsx index a999e1afc3..5632095c86 100644 --- a/packages/iris-grid/src/IrisGrid.tsx +++ b/packages/iris-grid/src/IrisGrid.tsx @@ -845,7 +845,7 @@ export class IrisGrid extends Component { const column = partitionColumn ?? model.columns.find(c => c.isPartitionColumn); if ( - model.isFilterRequired && + model.isPartitionRequired && model.isValuesTableAvailable && column != null ) { diff --git a/packages/iris-grid/src/IrisGridModel.ts b/packages/iris-grid/src/IrisGridModel.ts index 8c09d7b882..a635d13fdf 100644 --- a/packages/iris-grid/src/IrisGridModel.ts +++ b/packages/iris-grid/src/IrisGridModel.ts @@ -30,8 +30,8 @@ import { PendingDataMap, PendingDataErrorMap, } from './CommonTypes'; -import ColumnHeaderGroup from './ColumnHeaderGroup'; import { IrisGridThemeType } from './IrisGridTheme'; +import ColumnHeaderGroup from './ColumnHeaderGroup'; type IrisGridModelEventNames = (typeof IrisGridModel.EVENT)[keyof typeof IrisGridModel.EVENT]; @@ -191,6 +191,16 @@ abstract class IrisGridModel< */ abstract set filter(filter: readonly FilterCondition[]); + /** + * @returns The partitions set on this model + */ + abstract get partition(): readonly unknown[]; + + /** + * @param partition The partitions to set + */ + abstract set partition(partition: readonly unknown[]); + /** * @returns The formatter used when formatting data */ @@ -307,12 +317,21 @@ abstract class IrisGridModel< } /** + * @deprecated Replaced with isPartitionRequired() * @returns True if this model requires a filter to be set */ get isFilterRequired(): boolean { return false; } + /** + * Replaces isPartitionRequired() + * @returns True if this model requires a partition to be set + */ + get isPartitionRequired(): boolean { + return false; + } + get isReversible(): boolean { return true; } @@ -458,12 +477,12 @@ abstract class IrisGridModel< * Set the indices of the viewport * @param top Top of viewport * @param bottom Bottom of viewport - * @param columns The columns in the viewport. `null` for all columns + * @param columns The columns in the viewport. `undefined` for all columns */ abstract setViewport( top: VisibleIndex, bottom: VisibleIndex, - columns: Column[] | null + columns?: Column[] ): void; /** @@ -530,10 +549,10 @@ abstract class IrisGridModel< abstract get columnHeaderGroups(): readonly ColumnHeaderGroup[]; - abstract get columnHeaderGroupMap(): ReadonlyMap; - abstract set columnHeaderGroups(groups: readonly ColumnHeaderGroup[]); + abstract get columnHeaderGroupMap(): ReadonlyMap; + abstract getColumnHeaderParentGroup( modelIndex: ModelIndex, depth: number diff --git a/packages/iris-grid/src/IrisGridModelFactory.ts b/packages/iris-grid/src/IrisGridModelFactory.ts index 455cb51f0c..1421b04962 100644 --- a/packages/iris-grid/src/IrisGridModelFactory.ts +++ b/packages/iris-grid/src/IrisGridModelFactory.ts @@ -1,4 +1,9 @@ -import type { dh as DhType, Table, TreeTable } from '@deephaven/jsapi-types'; +import type { + dh as DhType, + Table, + TreeTable, + PartitionedTable, +} from '@deephaven/jsapi-types'; import { Formatter, TableUtils } from '@deephaven/jsapi-utils'; import IrisGridModel from './IrisGridModel'; import IrisGridProxyModel from './IrisGridProxyModel'; @@ -14,11 +19,15 @@ class IrisGridModelFactory { */ static async makeModel( dh: DhType, - table: Table | TreeTable, + table: Table | TreeTable | PartitionedTable, formatter = new Formatter(dh) ): Promise { let inputTable = null; - if (!TableUtils.isTreeTable(table) && table.hasInputTable) { + if ( + !TableUtils.isTreeTable(table) && + !TableUtils.isPartitionedTable(table) && + table.hasInputTable + ) { inputTable = await table.inputTable(); } return new IrisGridProxyModel(dh, table, formatter, inputTable); diff --git a/packages/iris-grid/src/IrisGridPartitionedTableModel.tsx b/packages/iris-grid/src/IrisGridPartitionedTableModel.tsx new file mode 100644 index 0000000000..44c703840b --- /dev/null +++ b/packages/iris-grid/src/IrisGridPartitionedTableModel.tsx @@ -0,0 +1,437 @@ +import { + DataBarOptions, + GridRange, + ModelIndex, + MoveOperation, + VisibleIndex, +} from '@deephaven/grid'; +import type { + Column, + ColumnStatistics, + CustomColumn, + dh as DhType, + FilterCondition, + Format, + LayoutHints, + PartitionedTable, + RollupConfig, + Row, + Sort, + Table, + ValueTypeUnion, +} from '@deephaven/jsapi-types'; +import { Formatter } from '@deephaven/jsapi-utils'; +import { EventShimCustomEvent } from '@deephaven/utils'; +import Log from '@deephaven/log'; +import { + ColumnName, + PendingDataMap, + PendingDataErrorMap, + UITotalsTableConfig, +} from './CommonTypes'; +import ColumnHeaderGroup from './ColumnHeaderGroup'; +import IrisGridModel from './IrisGridModel'; +import IrisGridTableModel from './IrisGridTableModel'; +import EmptyIrisGridModel from './EmptyIrisGridModel'; +import { IrisGridThemeType } from './IrisGridTheme'; + +const log = Log.module('IrisGridPartitionedTableModel'); + +class IrisGridPartitionedTableModel extends IrisGridModel { + private irisFormatter: Formatter; + + private partitionedTable: PartitionedTable; + // Track every getTable and close them all at the end (test opening a closed getTable) + // Test getTable(key) and hold a reference to it, call close on PartitionedTable, check if table is still open + + private model: IrisGridTableModel | EmptyIrisGridModel; + + private partitionKeys: unknown[]; + + /** + * @param dh JSAPI instance + * @param table Partitioned table to be used in the model + * @param formatter The formatter to use when getting formats + */ + constructor( + dh: DhType, + partitionedTable: PartitionedTable, + formatter = new Formatter(dh) + ) { + super(dh); + this.partitionedTable = partitionedTable; + this.irisFormatter = formatter; + this.partitionKeys = this.partitionedTable.getKeys().values().next().value; + this.model = new EmptyIrisGridModel(dh); + } + + get rowCount(): number { + return this.model.rowCount; + } + + get columnCount(): number { + return this.model.columnCount; + } + + textForCell(column: number, row: number): string { + return this.model.textForCell(column, row); + } + + textForColumnHeader(column: ModelIndex, depth?: number): string | undefined { + return this.model.textForColumnHeader(column, depth); + } + + get columns(): readonly Column[] { + return this.model.columns; + } + + getColumnIndexByName(name: string): ModelIndex | undefined { + return this.model.getColumnIndexByName(name); + } + + get originalColumns(): readonly Column[] { + return this.model.originalColumns; + } + + get initialMovedColumns(): readonly MoveOperation[] { + return this.model.initialMovedColumns; + } + + get initialMovedRows(): readonly MoveOperation[] { + return this.model.initialMovedRows; + } + + get initialColumnHeaderGroups(): readonly ColumnHeaderGroup[] { + return this.model.initialColumnHeaderGroups; + } + + get groupedColumns(): readonly Column[] { + return this.model.groupedColumns; + } + + formatForCell(column: number, row: number): Format | undefined { + return this.model.formatForCell(column, row); + } + + valueForCell(column: ModelIndex, row: ModelIndex): unknown { + return this.model.valueForCell(column, row); + } + + get filter(): FilterCondition[] { + return this.model.filter; + } + + set filter(value: FilterCondition[]) { + this.model.filter = value; + } + + get partition(): unknown[] { + return this.partitionKeys; + } + + set partition(value: unknown[]) { + log.debug2('set partition', value); + this.partitionKeys = value; + this.updateTable(); + } + + get formatter(): Formatter { + return this.irisFormatter; + } + + set formatter(formatter: Formatter) { + this.irisFormatter = formatter; + } + + displayString( + value: unknown, + columnType: string, + columnName?: string | undefined + ): string { + return this.model.displayString(value, columnType, columnName); + } + + get sort(): Sort[] { + return this.model.sort; + } + + set sort(value: Sort[]) { + this.model.sort = value; + } + + get customColumns(): ColumnName[] { + return this.model.customColumns; + } + + set customColumns(customColumns: ColumnName[]) { + this.model.customColumns = customColumns; + } + + get formatColumns(): CustomColumn[] { + return this.model.formatColumns; + } + + set formatColumns(formatColumns: CustomColumn[]) { + this.model.formatColumns = formatColumns; + } + + updateFrozenColumns(columns: ColumnName[]): void { + this.model.updateFrozenColumns(columns); + } + + get rollupConfig(): RollupConfig | null { + return this.model.rollupConfig; + } + + set rollupConfig(rollupConfig: RollupConfig | null) { + this.model.rollupConfig = rollupConfig; + } + + get totalsConfig(): UITotalsTableConfig | null { + return this.model.totalsConfig; + } + + set totalsConfig(totalsConfig: UITotalsTableConfig | null) { + this.model.totalsConfig = totalsConfig; + } + + get layoutHints(): LayoutHints | null { + return this.model.layoutHints; + } + + get frontColumns(): readonly ColumnName[] { + return this.model.frontColumns; + } + + get backColumns(): readonly ColumnName[] { + return this.model.backColumns; + } + + get frozenColumns(): readonly ColumnName[] { + return this.model.frozenColumns; + } + + isColumnFrozen(index: ModelIndex): boolean { + return this.model.isColumnFrozen(index); + } + + isColumnSortable(index: ModelIndex): boolean { + return this.model.isColumnSortable(index); + } + + get isFilterRequired(): boolean { + return this.model.isFilterRequired; + } + + static get isPartitionRequired(): boolean { + return true; + } + + get isReversible(): boolean { + return this.model.isReversible; + } + + export(): Promise
{ + return this.model.export(); + } + + get isColumnStatisticsAvailable(): boolean { + return this.model.isColumnStatisticsAvailable; + } + + get description(): string { + return this.model.description; + } + + columnStatistics(column: Column): Promise { + return this.model.columnStatistics(column); + } + + get isCustomColumnsAvailable(): boolean { + return this.model.isCustomColumnsAvailable; + } + + get isFormatColumnsAvailable(): boolean { + return this.model.isFormatColumnsAvailable; + } + + get isExportAvailable(): boolean { + return this.model.isExportAvailable; + } + + get isValuesTableAvailable(): boolean { + return this.model.isValuesTableAvailable; + } + + get isChartBuilderAvailable(): boolean { + return this.model.isChartBuilderAvailable; + } + + get isRollupAvailable(): boolean { + return this.model.isRollupAvailable; + } + + get isSelectDistinctAvailable(): boolean { + return this.model.isSelectDistinctAvailable; + } + + get isTotalsAvailable(): boolean { + return this.model.isTotalsAvailable; + } + + get selectDistinctColumns(): ColumnName[] { + return this.model.selectDistinctColumns; + } + + set selectDistinctColumns(names: ColumnName[]) { + this.model.selectDistinctColumns = names; + } + + get pendingDataMap(): PendingDataMap { + return this.model.pendingDataMap; + } + + set pendingDataMap(map: PendingDataMap) { + this.model.pendingDataMap = map; + } + + get pendingRowCount(): number { + return this.model.pendingRowCount; + } + + set pendingRowCount(count: number) { + this.model.pendingRowCount = count; + } + + get pendingDataErrors(): PendingDataErrorMap { + return this.model.pendingDataErrors; + } + + commitPending(): Promise { + return this.model.commitPending(); + } + + isFilterable(columnIndex: ModelIndex): boolean { + return this.model.isFilterable(columnIndex); + } + + setViewport( + top: VisibleIndex, + bottom: VisibleIndex, + columns?: Column[] + ): void { + this.model.setViewport(top, bottom, columns); + } + + snapshot(ranges: readonly GridRange[]): Promise { + return this.model.snapshot(ranges); + } + + textSnapshot( + ranges: readonly GridRange[], + includeHeaders?: boolean, + formatValue?: (value: unknown, column: Column, row?: Row) => string + ): Promise { + return this.model.textSnapshot(ranges, includeHeaders, formatValue); + } + + valuesTable(column: Column): Promise
{ + return this.model.valuesTable(column); + } + + close(): void { + // TODO + this.partitionedTable.close(); + // close model table + // close keytables, subscription table + } + + isRowMovable(): boolean { + return this.model.isRowMovable(); + } + + delete(ranges: readonly GridRange[]): Promise { + return this.model.delete(ranges); + } + + seekRow( + startRow: number, + column: Column, + valueType: ValueTypeUnion, + value: unknown, + insensitive?: boolean, + contains?: boolean, + isBackwards?: boolean + ): Promise { + return this.model.seekRow( + startRow, + column, + valueType, + value, + insensitive, + contains, + isBackwards + ); + } + + get isSeekRowAvailable(): boolean { + return this.model.isSeekRowAvailable; + } + + get columnHeaderGroups(): ColumnHeaderGroup[] { + return this.model.columnHeaderGroups; + } + + set columnHeaderGroups(groups: ColumnHeaderGroup[]) { + this.model.columnHeaderGroups = groups; + } + + get columnHeaderGroupMap(): ReadonlyMap { + return this.model.columnHeaderGroupMap; + } + + getColumnHeaderParentGroup( + modelIndex: ModelIndex, + depth: number + ): ColumnHeaderGroup | undefined { + return this.model.getColumnHeaderParentGroup(modelIndex, depth); + } + + dataBarOptionsForCell( + column: number, + row: number, + theme: IrisGridThemeType + ): DataBarOptions { + return this.model.dataBarOptionsForCell(column, row, theme); + } + + async initializeModel(): Promise { + const initTable = await this.partitionedTable.getTable(this.partitionKeys); + this.model = new IrisGridTableModel(this.dh, initTable, this.irisFormatter); + this.dispatchEvent( + new EventShimCustomEvent(IrisGridModel.EVENT.COLUMNS_CHANGED, { + detail: this.model.columns, + }) + ); + this.dispatchEvent( + new EventShimCustomEvent(IrisGridModel.EVENT.TABLE_CHANGED, { + detail: this.model.table, + }) + ); + return Promise.resolve(this.model); + } + + async updateTable(): Promise { + this.model.table = + this.partitionKeys.length > 0 + ? await this.partitionedTable.getTable(this.partitionKeys) + : await this.partitionedTable.getMergedTable(); + this.dispatchEvent( + new EventShimCustomEvent(IrisGridModel.EVENT.TABLE_CHANGED, { + detail: this.model.table, + }) + ); + } +} + +export default IrisGridPartitionedTableModel; diff --git a/packages/iris-grid/src/IrisGridProxyModel.ts b/packages/iris-grid/src/IrisGridProxyModel.ts index e5142502f2..e2fe348a3f 100644 --- a/packages/iris-grid/src/IrisGridProxyModel.ts +++ b/packages/iris-grid/src/IrisGridProxyModel.ts @@ -19,6 +19,7 @@ import type { Sort, Table, TreeTable, + PartitionedTable, ValueTypeUnion, } from '@deephaven/jsapi-types'; import { @@ -29,6 +30,7 @@ import { MoveOperation, } from '@deephaven/grid'; import IrisGridTableModel from './IrisGridTableModel'; +import IrisGridPartitionedTableModel from './IrisGridPartitionedTableModel'; import IrisGridTreeTableModel from './IrisGridTreeTableModel'; import IrisGridModel from './IrisGridModel'; import { @@ -45,13 +47,16 @@ const log = Log.module('IrisGridProxyModel'); function makeModel( dh: DhType, - table: Table | TreeTable, + table: Table | TreeTable | PartitionedTable, formatter?: Formatter, inputTable?: InputTable | null ): IrisGridModel { if (TableUtils.isTreeTable(table)) { return new IrisGridTreeTableModel(dh, table, formatter); } + if (TableUtils.isPartitionedTable(table)) { + return new IrisGridPartitionedTableModel(dh, table, formatter); + } return new IrisGridTableModel(dh, table, formatter, inputTable); } @@ -81,7 +86,7 @@ class IrisGridProxyModel extends IrisGridModel { constructor( dh: DhType, - table: Table | TreeTable, + table: Table | TreeTable | PartitionedTable, formatter = new Formatter(dh), inputTable: InputTable | null = null ) { @@ -96,6 +101,8 @@ class IrisGridProxyModel extends IrisGridModel { this.modelPromise = null; this.rollup = null; this.selectDistinct = []; + + this.initializePartitionModel(); } close(): void { @@ -476,6 +483,14 @@ class IrisGridProxyModel extends IrisGridModel { this.model.filter = filter; } + get partition(): readonly unknown[] { + return this.model.partition; + } + + set partition(partition: readonly unknown[]) { + this.model.partition = partition; + } + get formatter(): Formatter { return this.model.formatter; } @@ -601,6 +616,10 @@ class IrisGridProxyModel extends IrisGridModel { return this.model.isFilterRequired; } + get isPartitionRequired(): boolean { + return this.model.isPartitionRequired; + } + get isEditable(): boolean { return isEditableGridModel(this.model) && this.model.isEditable; } @@ -617,7 +636,7 @@ class IrisGridProxyModel extends IrisGridModel { isFilterable: IrisGridTableModel['isFilterable'] = (...args) => this.model.isFilterable(...args); - setViewport = (top: number, bottom: number, columns: Column[]): void => + setViewport = (top: number, bottom: number, columns?: Column[]): void => this.model.setViewport(top, bottom, columns); snapshot: IrisGridModel['snapshot'] = (...args) => @@ -731,6 +750,13 @@ class IrisGridProxyModel extends IrisGridModel { get isSeekRowAvailable(): boolean { return this.model.isSeekRowAvailable; } + + initializePartitionModel(): void { + const { model } = this; + if (model instanceof IrisGridPartitionedTableModel) { + this.setNextModel(model.initializeModel()); + } + } } export default IrisGridProxyModel; diff --git a/packages/iris-grid/src/IrisGridTableModel.ts b/packages/iris-grid/src/IrisGridTableModel.ts index 96e275ebfd..5c671b371c 100644 --- a/packages/iris-grid/src/IrisGridTableModel.ts +++ b/packages/iris-grid/src/IrisGridTableModel.ts @@ -236,6 +236,10 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { return this.table.isUncoalesced; } + get isPartitionRequired(): boolean { + return this.table.isUncoalesced; + } + isFilterable(columnIndex: ModelIndex): boolean { return this.getCachedFilterableColumnSet(this.columns).has(columnIndex); } @@ -269,7 +273,7 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { return this.frozenColumns.includes(this.columns[modelIndex].name); } - async delete(ranges: GridRange[]): Promise { + async delete(ranges: readonly GridRange[]): Promise { if (!this.isDeletableRanges(ranges)) { throw new Error(`Undeletable ranges ${ranges}`); } diff --git a/packages/iris-grid/src/IrisGridTableModelTemplate.ts b/packages/iris-grid/src/IrisGridTableModelTemplate.ts index 095d45a08c..d4a9888392 100644 --- a/packages/iris-grid/src/IrisGridTableModelTemplate.ts +++ b/packages/iris-grid/src/IrisGridTableModelTemplate.ts @@ -62,7 +62,7 @@ import { import { IrisGridThemeType } from './IrisGridTheme'; import ColumnHeaderGroup, { isColumnHeaderGroup } from './ColumnHeaderGroup'; -const log = Log.module('IrisGridTableModel'); +const log = Log.module('IrisGridTableModelTemplate'); const SET_VIEWPORT_THROTTLE = 150; const APPLY_VIEWPORT_THROTTLE = 0; @@ -166,7 +166,7 @@ class IrisGridTableModelTemplate< viewport: { top: VisibleIndex; bottom: VisibleIndex; - columns: Column[]; + columns?: Column[]; } | null; viewportData: UIViewportData | null; @@ -1224,6 +1224,14 @@ class IrisGridTableModelTemplate< this.applyViewport(); } + get partition(): unknown[] { + return []; + } + + set partition(partition: unknown[]) { + // Do nothing + } + get formatter(): Formatter { return this.irisFormatter; } @@ -1328,7 +1336,7 @@ class IrisGridTableModelTemplate< } setViewport = throttle( - (top: VisibleIndex, bottom: VisibleIndex, columns: Column[]) => { + (top: VisibleIndex, bottom: VisibleIndex, columns?: Column[]) => { if (bottom < top) { log.error('Invalid viewport', top, bottom); return; @@ -1381,7 +1389,7 @@ class IrisGridTableModelTemplate< applyBufferedViewport( viewportTop: number, viewportBottom: number, - columns: Column[] + columns?: Column[] ): void { log.debug2('applyBufferedViewport', viewportTop, viewportBottom, columns); if (this.subscription == null) { @@ -1398,7 +1406,7 @@ class IrisGridTableModelTemplate< } async snapshot( - ranges: GridRange[], + ranges: readonly GridRange[], includeHeaders = false, formatValue: (value: unknown, column: Column) => unknown = value => value, consolidateRanges = true @@ -1519,7 +1527,7 @@ class IrisGridTableModelTemplate< * @returns A formatted string of all the data, columns separated by `\t` and rows separated by `\n` */ async textSnapshot( - ranges: GridRange[], + ranges: readonly GridRange[], includeHeaders = false, formatValue: ( value: unknown, @@ -1699,7 +1707,7 @@ class IrisGridTableModelTemplate< return ranges.every(range => this.isEditableRange(range)); } - isDeletableRanges(ranges: GridRange[]): boolean { + isDeletableRanges(ranges: readonly GridRange[]): boolean { return ranges.every(range => this.isDeletableRange(range)); } diff --git a/packages/iris-grid/src/IrisGridTreeTableModel.ts b/packages/iris-grid/src/IrisGridTreeTableModel.ts index 003d58177c..cddb843a67 100644 --- a/packages/iris-grid/src/IrisGridTreeTableModel.ts +++ b/packages/iris-grid/src/IrisGridTreeTableModel.ts @@ -64,7 +64,10 @@ class IrisGridTreeTableModel extends IrisGridTableModelTemplate< ): Promise { assertNotNull(this.viewport); assertNotNull(this.viewportData); - const { columns } = this.viewport; + const { columns } = + this.viewport.columns === undefined + ? this + : (this.viewport as { columns: Column[] }); const result = []; if (includeHeaders != null && includeHeaders) { diff --git a/packages/iris-grid/src/IrisGridUtils.ts b/packages/iris-grid/src/IrisGridUtils.ts index b9c7b72f9b..ac5d45de90 100644 --- a/packages/iris-grid/src/IrisGridUtils.ts +++ b/packages/iris-grid/src/IrisGridUtils.ts @@ -652,9 +652,9 @@ class IrisGridUtils { hiddenColumns: readonly VisibleIndex[] = [], alwaysFetchColumnNames: readonly ColumnName[] = [], bufferPages = 0 - ): Column[] | null { + ): Column[] | undefined { if (left == null || right == null) { - return null; + return undefined; } const columnsCenter = IrisGridUtils.getVisibleColumnsInRange( diff --git a/packages/iris-grid/src/index.ts b/packages/iris-grid/src/index.ts index 0d5efc174a..8cee9b1bd5 100644 --- a/packages/iris-grid/src/index.ts +++ b/packages/iris-grid/src/index.ts @@ -11,10 +11,11 @@ export { default as SHORTCUTS } from './IrisGridShortcuts'; export { default as IrisGridModel } from './IrisGridModel'; export { default as IrisGridTableModel } from './IrisGridTableModel'; export * from './IrisGridTableModel'; +export { default as IrisGridPartitionedTableModel } from './IrisGridPartitionedTableModel'; export { default as IrisGridTreeTableModel } from './IrisGridTreeTableModel'; export { default as IrisGridTableModelTemplate } from './IrisGridTableModelTemplate'; -export * from './IrisGridTableModelTemplate'; export * from './IrisGridTreeTableModel'; +export * from './IrisGridTableModelTemplate'; export { default as IrisGridModelFactory } from './IrisGridModelFactory'; export { createDefaultIrisGridTheme } from './IrisGridTheme'; export type { IrisGridThemeType } from './IrisGridTheme'; diff --git a/packages/jsapi-types/src/dh.types.ts b/packages/jsapi-types/src/dh.types.ts index 2985b4ca30..cb539cf2c1 100644 --- a/packages/jsapi-types/src/dh.types.ts +++ b/packages/jsapi-types/src/dh.types.ts @@ -191,6 +191,9 @@ export interface IdeSession extends Evented { getObject( definition: VariableDefinition ): Promise; + getObject( + definition: VariableDefinition + ): Promise; getObject(definition: VariableDefinition): Promise; onLogMessage(logHandler: (logItem: LogItem) => void): () => void; runCode(code: string): Promise; @@ -912,6 +915,13 @@ export interface ColumnStatistics { getType(name: string): string; } +export interface PartitionedTableStatic { + readonly EVENT_KEYADDED: string; + readonly EVENT_DISCONNECT: string; + readonly EVENT_RECONNECT: string; + readonly EVENT_RECONNECTFAILED: string; +} + export interface TreeTableStatic { readonly EVENT_UPDATED: string; readonly EVENT_DISCONNECT: string; @@ -947,6 +957,16 @@ export interface TableTemplate extends Evented { close(): void; } +export interface PartitionedTable extends Evented, PartitionedTableStatic { + readonly size: number; + + getTable(key: object): Promise
; + getMergedTable(): Promise
; + getKeys(): Set; + + close(): void; +} + export interface TreeTable extends TableTemplate, TreeTableStatic { readonly isIncludeConstituents: boolean; readonly groupedColumns: Column[]; @@ -1079,6 +1099,9 @@ export interface IdeConnection getObject( definition: VariableDefinition ): Promise; + getObject( + definition: VariableDefinition + ): Promise; getObject(definition: VariableDefinition): Promise; subscribeToFieldUpdates( param: (changes: VariableChanges) => void diff --git a/packages/jsapi-utils/src/TableUtils.ts b/packages/jsapi-utils/src/TableUtils.ts index 6edc4dfc26..9ea05bdd5c 100644 --- a/packages/jsapi-utils/src/TableUtils.ts +++ b/packages/jsapi-utils/src/TableUtils.ts @@ -13,6 +13,7 @@ import type { FilterValue, LongWrapper, RemoverFn, + PartitionedTable, Sort, Table, TreeTable, @@ -746,6 +747,12 @@ export class TableUtils { } } + static isPartitionedTable(table: unknown): table is PartitionedTable { + return ( + table != null && (table as PartitionedTable).getMergedTable !== undefined + ); + } + static isTreeTable(table: unknown): table is TreeTable { return ( table != null && From 12b638ccd79bf26bee60c5190f57c6a814d7ad88 Mon Sep 17 00:00:00 2001 From: georgecwan Date: Mon, 6 Nov 2023 14:19:51 -0800 Subject: [PATCH 02/33] Handle partitions and partitionColumns with model --- packages/iris-grid/src/EmptyIrisGridModel.ts | 2 + packages/iris-grid/src/IrisGrid.tsx | 80 ++++++++----------- packages/iris-grid/src/IrisGridModel.ts | 16 +++- .../src/IrisGridPartitionSelector.tsx | 10 +-- .../src/IrisGridPartitionedTableModel.tsx | 35 +++++--- packages/iris-grid/src/IrisGridProxyModel.ts | 10 ++- packages/iris-grid/src/IrisGridTableModel.ts | 21 +++++ .../src/IrisGridTableModelTemplate.ts | 26 +++--- 8 files changed, 125 insertions(+), 75 deletions(-) diff --git a/packages/iris-grid/src/EmptyIrisGridModel.ts b/packages/iris-grid/src/EmptyIrisGridModel.ts index ced1cbd0e7..13b75544e9 100644 --- a/packages/iris-grid/src/EmptyIrisGridModel.ts +++ b/packages/iris-grid/src/EmptyIrisGridModel.ts @@ -44,6 +44,8 @@ class EmptyIrisGridModel extends IrisGridModel { partition: unknown[] = []; + partitionColumns: Column[] = []; + formatter: Formatter; sort: Sort[] = []; diff --git a/packages/iris-grid/src/IrisGrid.tsx b/packages/iris-grid/src/IrisGrid.tsx index ca45bf7764..a695dd85a0 100644 --- a/packages/iris-grid/src/IrisGrid.tsx +++ b/packages/iris-grid/src/IrisGrid.tsx @@ -346,8 +346,6 @@ export interface IrisGridState { keyHandlers: readonly KeyHandler[]; mouseHandlers: readonly GridMouseHandler[]; - partitions: (string | null)[]; - partitionColumns: Column[]; partitionTable: Table | null; partitionFilters: readonly FilterCondition[]; // setAdvancedFilter and setQuickFilter mutate the arguments @@ -463,7 +461,6 @@ export class IrisGrid extends Component { onStateChange: (): void => undefined, onAdvancedSettingsChange: (): void => undefined, partitions: [], - partitionColumns: [], quickFilters: EMPTY_MAP, selectDistinctColumns: EMPTY_ARRAY, sorts: EMPTY_ARRAY, @@ -673,8 +670,6 @@ export class IrisGrid extends Component { model, movedColumns: movedColumnsProp, movedRows: movedRowsProp, - partitions, - partitionColumns, rollupConfig, userColumnWidths, userRowHeights, @@ -750,8 +745,6 @@ export class IrisGrid extends Component { keyHandlers, mouseHandlers, - partitions, - partitionColumns, partitionTable: null, partitionFilters: [], // setAdvancedFilter and setQuickFilter mutate the arguments @@ -839,16 +832,19 @@ export class IrisGrid extends Component { } componentDidMount(): void { - const { partitionColumns, model } = this.props; - const columns = partitionColumns.length + const { partitions, partitionColumns, model } = this.props; + model.partitionColumns = partitionColumns.length ? partitionColumns : model.columns.filter(c => c.isPartitionColumn); + model.partition = partitionColumns.length + ? partitions + : Array(model.partitionColumns.length).fill(null); if ( model.isPartitionRequired && model.isValuesTableAvailable && - columns.length + model.partitionColumns.length ) { - this.loadPartitionsTable(columns); + this.loadPartitionsTable(); } else { this.initState(); } @@ -1895,8 +1891,10 @@ export class IrisGrid extends Component { this.initFormatter(); } - async loadPartitionsTable(partitionColumns: Column[]): Promise { + async loadPartitionsTable(): Promise { const { model } = this.props; + const { partitionColumns } = model; + this.setState({ isSelectingPartition: true }); try { @@ -1915,7 +1913,7 @@ export class IrisGrid extends Component { const row = data.rows[0]; const values = columns.map(column => row.get(column)); - this.updatePartition(values, partitionColumns); + this.updatePartition(values); this.setState({ isSelectingPartition: true }); } else { @@ -1923,7 +1921,7 @@ export class IrisGrid extends Component { this.setState({ isSelectingPartition: false }); this.handlePartitionFetchAll(); } - this.setState({ partitionTable, partitionColumns }, () => { + this.setState({ partitionTable }, () => { this.initState(); }); } catch (error) { @@ -1931,10 +1929,11 @@ export class IrisGrid extends Component { } } - updatePartition( - partitions: (string | null)[], - partitionColumns: Column[] - ): void { + updatePartition(partitions: (string | null)[]): void { + const { model } = this.props; + const { partitionColumns } = model; + + model.partition = partitions; const partitionFilters = []; for (let i = 0; i < partitionColumns.length; i += 1) { @@ -1945,8 +1944,6 @@ export class IrisGrid extends Component { partition !== null && !(TableUtils.isCharType(partitionColumn.type) && partition === '') ) { - const { model } = this.props; - const partitionText = TableUtils.isCharType(partitionColumn.type) ? model.displayString( partition, @@ -1965,7 +1962,6 @@ export class IrisGrid extends Component { } this.setState({ - partitions, partitionFilters, }); } @@ -2372,11 +2368,7 @@ export class IrisGrid extends Component { } handlePartitionChange(partitions: (string | null)[]): void { - const { partitionColumns } = this.state; - if (partitionColumns.length === 0) { - return; - } - this.updatePartition(partitions, partitionColumns); + this.updatePartition(partitions); } handlePartitionFetchAll(): void { @@ -3921,10 +3913,8 @@ export class IrisGrid extends Component { hoverSelectColumn, quickFilters, advancedFilters, - partitions, partitionFilters, partitionTable, - partitionColumns, searchFilter, selectDistinctColumns, @@ -4443,24 +4433,22 @@ export class IrisGrid extends Component { unmountOnExit >
- {partitionTable && - partitionColumns.length && - partitions.length && ( - model.displayString(value, type, stringName)} - columns={partitionColumns} - partitions={partitions} - onChange={this.handlePartitionChange} - onFetchAll={this.handlePartitionFetchAll} - onDone={this.handlePartitionDone} - /> - )} + {partitionTable && ( + model.displayString(value, type, stringName)} + columns={model.partitionColumns} + partitions={model.partition as readonly (string | null)[]} + onChange={this.handlePartitionChange} + onFetchAll={this.handlePartitionFetchAll} + onDone={this.handlePartitionDone} + /> + )}
; + abstract valuesTable(columns: Column | readonly Column[]): Promise
; /** * Close this model. It can no longer be used after being closed diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.tsx b/packages/iris-grid/src/IrisGridPartitionSelector.tsx index 7b9b3cd77e..805b0265b6 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.tsx +++ b/packages/iris-grid/src/IrisGridPartitionSelector.tsx @@ -17,14 +17,14 @@ interface IrisGridPartitionSelectorProps { dh: DhType; getFormattedString: (value: T, type: string, name: string) => string; table: Table; - columns: Column[]; - partitions: (string | null)[]; + columns: readonly Column[]; + partitions: readonly (string | null)[]; onFetchAll: () => void; onDone: (event?: React.MouseEvent) => void; onChange: (partitions: (string | null)[]) => void; } interface IrisGridPartitionSelectorState { - partitions: (string | null)[]; + partitions: readonly (string | null)[]; partitionTables: Table[] | null; } class IrisGridPartitionSelector extends Component< @@ -175,7 +175,7 @@ class IrisGridPartitionSelector extends Component< const { onChange } = this.props; const { partitions } = this.state; - onChange(partitions); + onChange([...partitions]); } sendFetchAll(): void { @@ -200,7 +200,7 @@ class IrisGridPartitionSelector extends Component< } async updatePartitionFilters( - partitions: (string | null)[], + partitions: readonly (string | null)[], partitionTables: Table[] ): Promise { const { columns, getFormattedString } = this.props; diff --git a/packages/iris-grid/src/IrisGridPartitionedTableModel.tsx b/packages/iris-grid/src/IrisGridPartitionedTableModel.tsx index 44c703840b..a3e877087a 100644 --- a/packages/iris-grid/src/IrisGridPartitionedTableModel.tsx +++ b/packages/iris-grid/src/IrisGridPartitionedTableModel.tsx @@ -1,3 +1,4 @@ +/* eslint class-methods-use-this: "off" */ import { DataBarOptions, GridRange, @@ -125,16 +126,6 @@ class IrisGridPartitionedTableModel extends IrisGridModel { this.model.filter = value; } - get partition(): unknown[] { - return this.partitionKeys; - } - - set partition(value: unknown[]) { - log.debug2('set partition', value); - this.partitionKeys = value; - this.updateTable(); - } - get formatter(): Formatter { return this.irisFormatter; } @@ -405,9 +396,29 @@ class IrisGridPartitionedTableModel extends IrisGridModel { return this.model.dataBarOptionsForCell(column, row, theme); } - async initializeModel(): Promise { + get partitionColumns(): readonly Column[] { + // TODO + throw new Error('Method not implemented.'); + } + + set partitionColumns(columns: readonly Column[]) { + // Do nothing, partition columns of PartitionedTable can't be modified + } + + get partition(): unknown[] { + return this.partitionKeys; + } + + set partition(value: unknown[]) { + log.debug2('set partition', value); + this.partitionKeys = value; + this.updatePartitions(); + } + + async initializePartitionModel(): Promise { const initTable = await this.partitionedTable.getTable(this.partitionKeys); this.model = new IrisGridTableModel(this.dh, initTable, this.irisFormatter); + log.log('initializeModel', this.columns); this.dispatchEvent( new EventShimCustomEvent(IrisGridModel.EVENT.COLUMNS_CHANGED, { detail: this.model.columns, @@ -421,7 +432,7 @@ class IrisGridPartitionedTableModel extends IrisGridModel { return Promise.resolve(this.model); } - async updateTable(): Promise { + private async updatePartitions(): Promise { this.model.table = this.partitionKeys.length > 0 ? await this.partitionedTable.getTable(this.partitionKeys) diff --git a/packages/iris-grid/src/IrisGridProxyModel.ts b/packages/iris-grid/src/IrisGridProxyModel.ts index e2fe348a3f..819e984542 100644 --- a/packages/iris-grid/src/IrisGridProxyModel.ts +++ b/packages/iris-grid/src/IrisGridProxyModel.ts @@ -459,6 +459,14 @@ class IrisGridProxyModel extends IrisGridModel { return this.model.groupedColumns; } + get partitionColumns(): readonly Column[] { + return this.model.partitionColumns; + } + + set partitionColumns(columns: readonly Column[]) { + this.model.partitionColumns = [...columns]; + } + get description(): string { return this.model.description; } @@ -754,7 +762,7 @@ class IrisGridProxyModel extends IrisGridModel { initializePartitionModel(): void { const { model } = this; if (model instanceof IrisGridPartitionedTableModel) { - this.setNextModel(model.initializeModel()); + this.setNextModel(model.initializePartitionModel()); } } } diff --git a/packages/iris-grid/src/IrisGridTableModel.ts b/packages/iris-grid/src/IrisGridTableModel.ts index 5c671b371c..53096994dd 100644 --- a/packages/iris-grid/src/IrisGridTableModel.ts +++ b/packages/iris-grid/src/IrisGridTableModel.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-underscore-dangle */ /* eslint class-methods-use-this: "off" */ import memoize from 'memoize-one'; import { GridRange, ModelIndex } from '@deephaven/grid'; @@ -35,6 +36,10 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { formatColumnList: CustomColumn[]; + private _partition: unknown[] = []; + + private _partitionColumns: Column[] = []; + /** * @param dh JSAPI instance * @param table Iris data table to be used in the model @@ -185,6 +190,22 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { ); } + get partitionColumns(): Column[] { + return this._partitionColumns; + } + + set partitionColumns(partitionColumns: Column[]) { + this._partitionColumns = partitionColumns; + } + + get partition(): unknown[] { + return this._partition; + } + + set partition(partition: unknown[]) { + this._partition = partition; + } + set totalsConfig(totalsConfig: UITotalsTableConfig | null) { log.debug('set totalsConfig', totalsConfig); diff --git a/packages/iris-grid/src/IrisGridTableModelTemplate.ts b/packages/iris-grid/src/IrisGridTableModelTemplate.ts index b6a1179b27..afcba525b2 100644 --- a/packages/iris-grid/src/IrisGridTableModelTemplate.ts +++ b/packages/iris-grid/src/IrisGridTableModelTemplate.ts @@ -141,6 +141,22 @@ class IrisGridTableModelTemplate< throw new Error('Method not implemented.'); } + get partitionColumns(): readonly Column[] { + throw new Error('Method not implemented.'); + } + + set partitionColumns(columns: readonly Column[]) { + throw new Error('Method not implemented.'); + } + + get partition(): readonly unknown[] { + throw new Error('Method not implemented.'); + } + + set partition(partition: readonly unknown[]) { + throw new Error('Method not implemented.'); + } + /** * Returns an array of the columns in the model * The order of model columns should never change once established @@ -1224,14 +1240,6 @@ class IrisGridTableModelTemplate< this.applyViewport(); } - get partition(): unknown[] { - return []; - } - - set partition(partition: unknown[]) { - // Do nothing - } - get formatter(): Formatter { return this.irisFormatter; } @@ -1546,7 +1554,7 @@ class IrisGridTableModelTemplate< return data.map(row => row.join('\t')).join('\n'); } - async valuesTable(columns: Column | Column[]): Promise
{ + async valuesTable(columns: Column | readonly Column[]): Promise
{ let table = null; try { table = await this.table.copy(); From dffd2bbfd1998a54c5078aedd3bd20fee463fb70 Mon Sep 17 00:00:00 2001 From: georgecwan Date: Thu, 16 Nov 2023 08:03:04 -0800 Subject: [PATCH 03/33] Update IrisGridPartitionSelector for PartitionedTable --- packages/iris-grid/src/EmptyIrisGridModel.ts | 2 +- packages/iris-grid/src/IrisGrid.tsx | 133 ++-------- packages/iris-grid/src/IrisGridModel.ts | 7 + .../src/IrisGridPartitionSelector.scss | 9 +- .../src/IrisGridPartitionSelector.tsx | 230 ++++++++++++------ .../src/IrisGridPartitionedTableModel.tsx | 68 +++--- packages/iris-grid/src/IrisGridProxyModel.ts | 42 +++- packages/iris-grid/src/IrisGridTableModel.ts | 81 +++++- packages/jsapi-types/src/dh.types.ts | 5 +- 9 files changed, 334 insertions(+), 243 deletions(-) diff --git a/packages/iris-grid/src/EmptyIrisGridModel.ts b/packages/iris-grid/src/EmptyIrisGridModel.ts index 13b75544e9..9decc3cdc8 100644 --- a/packages/iris-grid/src/EmptyIrisGridModel.ts +++ b/packages/iris-grid/src/EmptyIrisGridModel.ts @@ -158,7 +158,7 @@ class EmptyIrisGridModel extends IrisGridModel { return Promise.resolve(''); } - valuesTable(column: Column): Promise
{ + valuesTable(columns: Column | readonly Column[]): Promise
{ throw new Error('Method not implemented.'); } diff --git a/packages/iris-grid/src/IrisGrid.tsx b/packages/iris-grid/src/IrisGrid.tsx index a695dd85a0..9e7f4bc8f4 100644 --- a/packages/iris-grid/src/IrisGrid.tsx +++ b/packages/iris-grid/src/IrisGrid.tsx @@ -346,8 +346,6 @@ export interface IrisGridState { keyHandlers: readonly KeyHandler[]; mouseHandlers: readonly GridMouseHandler[]; - partitionTable: Table | null; - partitionFilters: readonly FilterCondition[]; // setAdvancedFilter and setQuickFilter mutate the arguments // so we want to always use map copies from the state instead of props quickFilters: ReadonlyQuickFilterMap; @@ -574,8 +572,7 @@ export class IrisGrid extends Component { this.handleDownloadCanceled = this.handleDownloadCanceled.bind(this); this.handleDownloadCompleted = this.handleDownloadCompleted.bind(this); this.handlePartitionChange = this.handlePartitionChange.bind(this); - this.handlePartitionFetchAll = this.handlePartitionFetchAll.bind(this); - this.handlePartitionDone = this.handlePartitionDone.bind(this); + this.handlePartitionMerge = this.handlePartitionMerge.bind(this); this.handleColumnVisibilityChanged = this.handleColumnVisibilityChanged.bind(this); this.handleColumnVisibilityReset = @@ -745,8 +742,6 @@ export class IrisGrid extends Component { keyHandlers, mouseHandlers, - partitionTable: null, - partitionFilters: [], // setAdvancedFilter and setQuickFilter mutate the arguments // so we want to always use map copies from the state instead of props quickFilters: quickFilters ? new Map(quickFilters) : new Map(), @@ -832,21 +827,20 @@ export class IrisGrid extends Component { } componentDidMount(): void { - const { partitions, partitionColumns, model } = this.props; - model.partitionColumns = partitionColumns.length - ? partitionColumns - : model.columns.filter(c => c.isPartitionColumn); - model.partition = partitionColumns.length - ? partitions - : Array(model.partitionColumns.length).fill(null); - if ( - model.isPartitionRequired && - model.isValuesTableAvailable && - model.partitionColumns.length - ) { - this.loadPartitionsTable(); - } else { - this.initState(); + const { partitionColumns, model } = this.props; + try { + model.partitionColumns = partitionColumns.length + ? partitionColumns + : model.columns.filter(c => c.isPartitionColumn); + if (model.isPartitionRequired && model.partitionColumns.length) { + this.setState({ isSelectingPartition: true }, () => { + this.initState(); + }); + } else { + this.initState(); + } + } catch (error) { + this.handleTableLoadError(error); } this.startListening(model); } @@ -1318,11 +1312,9 @@ export class IrisGrid extends Component { customFilters: readonly FilterCondition[], quickFilters: ReadonlyQuickFilterMap, advancedFilters: ReadonlyAdvancedFilterMap, - partitionFilters: readonly FilterCondition[], searchFilter: FilterCondition | undefined ) => [ ...(customFilters ?? []), - ...(partitionFilters ?? []), ...IrisGridUtils.getFiltersFromFilterMap(quickFilters), ...IrisGridUtils.getFiltersFromFilterMap(advancedFilters), ...(searchFilter !== undefined ? [searchFilter] : []), @@ -1893,35 +1885,10 @@ export class IrisGrid extends Component { async loadPartitionsTable(): Promise { const { model } = this.props; - const { partitionColumns } = model; - - this.setState({ isSelectingPartition: true }); + log.log('loadPartitionsTable'); try { - const partitionTable = await this.pending.add( - model.valuesTable(partitionColumns), - resolved => resolved.close() - ); - - 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 values = columns.map(column => row.get(column)); - - this.updatePartition(values); - - this.setState({ isSelectingPartition: true }); - } else { - log.info('Table does not have any data, just fetching all'); - this.setState({ isSelectingPartition: false }); - this.handlePartitionFetchAll(); - } - this.setState({ partitionTable }, () => { + this.setState({ isSelectingPartition: true }, () => { this.initState(); }); } catch (error) { @@ -1929,43 +1896,6 @@ export class IrisGrid extends Component { } } - updatePartition(partitions: (string | null)[]): void { - const { model } = this.props; - const { partitionColumns } = model; - - model.partition = partitions; - const partitionFilters = []; - - for (let i = 0; i < partitionColumns.length; i += 1) { - const partition = partitions[i]; - const partitionColumn = partitionColumns[i]; - - if ( - partition !== null && - !(TableUtils.isCharType(partitionColumn.type) && partition === '') - ) { - 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); - } - } - } - - this.setState({ - partitionFilters, - }); - } - copyCell( columnIndex: GridRangeIndex, rowIndex: GridRangeIndex, @@ -2367,19 +2297,14 @@ export class IrisGrid extends Component { this.isAnimating = false; } - handlePartitionChange(partitions: (string | null)[]): void { - this.updatePartition(partitions); - } - - handlePartitionFetchAll(): void { - this.setState({ - partitionFilters: [], - isSelectingPartition: false, - }); + handlePartitionChange(partitions: readonly unknown[]): void { + const { model } = this.props; + model.partition = partitions; } - handlePartitionDone(): void { - this.setState({ isSelectingPartition: false }); + handlePartitionMerge(): void { + const { model } = this.props; + model.partition = []; } handleTableLoadError(error: unknown): void { @@ -3913,8 +3838,6 @@ export class IrisGrid extends Component { hoverSelectColumn, quickFilters, advancedFilters, - partitionFilters, - partitionTable, searchFilter, selectDistinctColumns, @@ -3976,7 +3899,6 @@ export class IrisGrid extends Component { customFilters, quickFilters, advancedFilters, - partitionFilters, searchFilter ); @@ -4433,20 +4355,19 @@ export class IrisGrid extends Component { unmountOnExit >
- {partitionTable && ( + {model.partitionKeysTable && ( model.displayString(value, type, stringName)} columns={model.partitionColumns} - partitions={model.partition as readonly (string | null)[]} + partitions={model.partition} onChange={this.handlePartitionChange} - onFetchAll={this.handlePartitionFetchAll} - onDone={this.handlePartitionDone} + onMerge={this.handlePartitionMerge} /> )}
diff --git a/packages/iris-grid/src/IrisGridModel.ts b/packages/iris-grid/src/IrisGridModel.ts index 491fdb5d05..4599ad2485 100644 --- a/packages/iris-grid/src/IrisGridModel.ts +++ b/packages/iris-grid/src/IrisGridModel.ts @@ -213,6 +213,13 @@ abstract class IrisGridModel< */ abstract set partitionColumns(columns: readonly Column[]); + /** + * @returns A Table containing the keys of the columns that are partitioned on + */ + get partitionKeysTable(): Table | null { + return null; + } + /** * @returns The formatter used when formatting data */ diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.scss b/packages/iris-grid/src/IrisGridPartitionSelector.scss index 7534447219..01e6684870 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.scss +++ b/packages/iris-grid/src/IrisGridPartitionSelector.scss @@ -4,9 +4,9 @@ $partition-selector-input-height: 2.25em; .iris-grid-partition-selector { display: flex; flex-wrap: wrap; - background: transparent; + background: $gray-850; vertical-align: middle; - .status-message { + .column-name { text-overflow: ellipsis; display: flex; flex-direction: row; @@ -40,9 +40,4 @@ $partition-selector-input-height: 2.25em; margin: 0.35em 0.3em; padding: 0.25em 0.5em; } - .btn-close { - padding: 0 0.5em; - margin: 0.25em 0.25em 0.25em 0; - min-width: 0; - } } diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.tsx b/packages/iris-grid/src/IrisGridPartitionSelector.tsx index 805b0265b6..219bc028d0 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.tsx +++ b/packages/iris-grid/src/IrisGridPartitionSelector.tsx @@ -1,11 +1,22 @@ import React, { Component } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { DropdownMenu, Tooltip } from '@deephaven/components'; -import { vsTriangleDown, vsClose } from '@deephaven/icons'; +import { Button, DropdownMenu, Tooltip } from '@deephaven/components'; +import { + vsTriangleDown, + vsChevronRight, + vsMerge, + vsKey, +} from '@deephaven/icons'; import Log from '@deephaven/log'; import debounce from 'lodash.debounce'; -import type { Column, dh as DhType, Table } from '@deephaven/jsapi-types'; +import type { + Column, + dh as DhType, + Table, + TableData, +} from '@deephaven/jsapi-types'; import { TableUtils } from '@deephaven/jsapi-utils'; +import deepEqual from 'deep-equal'; import PartitionSelectorSearch from './PartitionSelectorSearch'; import './IrisGridPartitionSelector.scss'; import IrisGridUtils from './IrisGridUtils'; @@ -18,13 +29,13 @@ interface IrisGridPartitionSelectorProps { getFormattedString: (value: T, type: string, name: string) => string; table: Table; columns: readonly Column[]; - partitions: readonly (string | null)[]; - onFetchAll: () => void; - onDone: (event?: React.MouseEvent) => void; - onChange: (partitions: (string | null)[]) => void; + partitions: readonly unknown[]; + onMerge: () => void; + onChange: (partitions: readonly unknown[]) => void; } interface IrisGridPartitionSelectorState { - partitions: readonly (string | null)[]; + isShowingKeys: boolean; + partitions: readonly unknown[]; partitionTables: Table[] | null; } class IrisGridPartitionSelector extends Component< @@ -33,7 +44,7 @@ class IrisGridPartitionSelector extends Component< > { static defaultProps = { onChange: (): void => undefined, - onFetchAll: (): void => undefined, + onMerge: (): void => undefined, onDone: (): void => undefined, partitions: [], }; @@ -41,8 +52,7 @@ class IrisGridPartitionSelector extends Component< constructor(props: IrisGridPartitionSelectorProps) { super(props); - this.handleCloseClick = this.handleCloseClick.bind(this); - this.handleIgnoreClick = this.handleIgnoreClick.bind(this); + this.handleMergeClick = this.handleMergeClick.bind(this); this.handlePartitionChange = this.handlePartitionChange.bind(this); this.handlePartitionSelect = this.handlePartitionSelect.bind(this); this.handlePartitionListResized = @@ -56,6 +66,7 @@ class IrisGridPartitionSelector extends Component< this.selectorSearch = columns.map(() => null); this.state = { + isShowingKeys: false, partitions, partitionTables: null, }; @@ -68,7 +79,32 @@ class IrisGridPartitionSelector extends Component< const partitionTables = await Promise.all( columns.map(async (_, i) => table.selectDistinct(columns.slice(0, i + 1))) ); - this.updatePartitionFilters(partitions, partitionTables); + this.updatePartitions(0, partitions, partitionTables).then( + ([newPartitions, newPartitionTables]) => { + this.setState({ + partitions: newPartitions, + partitionTables: newPartitionTables, + }); + } + ); + } + + componentDidUpdate(prevProps: IrisGridPartitionSelectorProps): void { + const { partitions: prevPartitions } = prevProps; + const { partitions: newPartitions } = this.props; + const { partitionTables: prevTables } = this.state; + + if (!deepEqual(prevPartitions, newPartitions)) { + log.log(prevPartitions, newPartitions); + this.updatePartitions(0, newPartitions, prevTables).then( + ([partitions, partitionTables]) => { + this.setState({ + partitions, + partitionTables, + }); + } + ); + } } componentWillUnmount(): void { @@ -83,16 +119,10 @@ class IrisGridPartitionSelector extends Component< selectorSearch: (PartitionSelectorSearch | null)[]; - handleCloseClick(): void { - log.debug2('handleCloseClick'); - - this.sendDone(); - } - - handleIgnoreClick(): void { - log.debug2('handleIgnoreClick'); + handleMergeClick(): void { + log.debug2('handleMergeClick'); - this.sendFetchAll(); + this.sendMerge(); } handlePartitionChange( @@ -102,41 +132,46 @@ class IrisGridPartitionSelector extends Component< log.debug2('handlePartitionChange'); const { columns } = this.props; - const { partitions, partitionTables } = this.state; + const { partitions: prevPartitions, partitionTables: prevTables } = + this.state; const { value: partition } = event.target; - const newPartitions = [...partitions]; + const newPartitions = [...prevPartitions]; newPartitions[index] = TableUtils.isCharType(columns[index].type) && partition.length > 0 ? partition.charCodeAt(0).toString() : partition; - if (partitionTables) { - this.updatePartitionFilters(newPartitions, partitionTables); - } - this.setState({ - partitions: newPartitions, - }); + this.updatePartitions(index, newPartitions, prevTables).then( + ([partitions, partitionTables]) => { + this.setState({ + partitions, + partitionTables, + }); - this.debounceUpdate(); + this.debounceUpdate(); + } + ); } handlePartitionSelect(index: number, partition: string): void { - const { partitions, partitionTables } = this.state; + const { partitions: prevPartitions, partitionTables: prevTables } = + this.state; const selectedMenu = this.searchMenu[index]; if (selectedMenu) { selectedMenu.closeMenu(); } - const newPartitions = [...partitions]; + const newPartitions = [...prevPartitions]; newPartitions[index] = partition; - if (partitionTables) { - this.updatePartitionFilters(newPartitions, partitionTables); - } - this.setState({ partitions: newPartitions }, () => { - this.sendUpdate(); - }); + this.updatePartitions(index, newPartitions, prevTables).then( + ([partitions, partitionTables]) => { + this.setState({ partitions, partitionTables }, () => { + this.sendUpdate(); + }); + } + ); } handlePartitionListResized(index: number): void { @@ -163,28 +198,21 @@ class IrisGridPartitionSelector extends Component< this.sendUpdate(); }, PARTITION_CHANGE_DEBOUNCE_MS); - sendDone(): void { - this.debounceUpdate.flush(); - - const { onDone } = this.props; - onDone(); - } - sendUpdate(): void { log.debug2('sendUpdate'); const { onChange } = this.props; const { partitions } = this.state; - onChange([...partitions]); + onChange(partitions); } - sendFetchAll(): void { - log.debug2('sendFetchAll'); + sendMerge(): void { + log.debug2('sendMerge'); this.debounceUpdate.cancel(); - const { onFetchAll } = this.props; - onFetchAll(); + const { onMerge } = this.props; + onMerge(); } getDisplayValue(column: Column, index: number): string { @@ -194,25 +222,32 @@ class IrisGridPartitionSelector extends Component< return ''; } if (TableUtils.isCharType(column.type) && partition.toString().length > 0) { - return String.fromCharCode(parseInt(partition, 10)); + return String.fromCharCode(parseInt(partition.toString(), 10)); } return IrisGridUtils.convertValueToText(partition, column.type); } - async updatePartitionFilters( - partitions: readonly (string | null)[], - partitionTables: Table[] - ): Promise { - const { columns, getFormattedString } = this.props; + async updatePartitions( + index: number, + partitions: readonly unknown[], + partitionTables: Table[] | null + ): Promise<[unknown[], Table[] | null]> { + if (!Array.isArray(partitionTables)) { + return [[...partitions], partitionTables]; + } + const { columns, getFormattedString, table } = this.props; - const partitionFilters = []; - for (let i = 0; i < columns.length - 1; i += 1) { + const dataPromises = Array>(); + const partitionFilters = [...partitionTables[index].filter]; + + // Create partition filters + for (let i = index; i < columns.length; i += 1) { const partition = partitions[i]; const partitionColumn = columns[i]; - partitionTables[i]?.applyFilter(partitionFilters); + partitionTables[i].applyFilter([...partitionFilters]); if ( - partition !== null && + partition != null && !(TableUtils.isCharType(partitionColumn.type) && partition === '') ) { const partitionText = TableUtils.isCharType(partitionColumn.type) @@ -221,23 +256,54 @@ class IrisGridPartitionSelector extends Component< partitionColumn.type, partitionColumn.name ) - : partition; + : partition?.toString() ?? ''; const partitionFilter = this.tableUtils.makeQuickFilterFromComponent( partitionColumn, partitionText ); if (partitionFilter !== null) { + // Should never be null partitionFilters.push(partitionFilter); } } + const partitionFilterCopy = [...partitionFilters]; + dataPromises.push( + table.copy().then(async t => { + t.applyFilter(partitionFilterCopy); + t.setViewport(0, 0, columns as Column[]); + const data = await t.getViewportData(); + t.close(); + return data; + }) + ); } - partitionTables[partitionTables.length - 1]?.applyFilter(partitionFilters); - this.setState({ partitionTables }); + + return Promise.all(dataPromises).then(tableData => { + // Check if a partition is selected (no partitions for key table or merge table) + if (partitions[0] === undefined) { + return [[...partitions], partitionTables]; + } + // Check if partitions are valid + const validPartitions = partitions.slice(0, index + 1); + for (let i = 1; i < tableData.length; i += 1) { + const data = tableData[i]; + if (data.rows.length > 0 && partitions[index + i] !== undefined) { + validPartitions.push(partitions[index + i]); + } else { + return this.updatePartitions( + index + i, + columns.map(c => tableData[i - 1].rows[0].get(c)), + partitionTables + ); + } + } + return [validPartitions, partitionTables]; + }); } render(): JSX.Element { - const { columns, dh, getFormattedString, onDone } = this.props; - const { partitionTables } = this.state; + const { columns, dh, getFormattedString } = this.props; + const { isShowingKeys, partitionTables } = this.state; const partitionSelectorSearch = columns.map( (column, index) => @@ -259,8 +325,8 @@ class IrisGridPartitionSelector extends Component< ) ); const partitionSelectors = columns.map((column, index) => ( - <> -
+ +
{column.name}:
@@ -291,26 +357,32 @@ class IrisGridPartitionSelector extends Component<
+ {/* */}
- + )); return (
- {partitionSelectors} - - + merge + + {partitionSelectors}
); } diff --git a/packages/iris-grid/src/IrisGridPartitionedTableModel.tsx b/packages/iris-grid/src/IrisGridPartitionedTableModel.tsx index a3e877087a..d9925eab57 100644 --- a/packages/iris-grid/src/IrisGridPartitionedTableModel.tsx +++ b/packages/iris-grid/src/IrisGridPartitionedTableModel.tsx @@ -38,17 +38,27 @@ import { IrisGridThemeType } from './IrisGridTheme'; const log = Log.module('IrisGridPartitionedTableModel'); +export function isIrisGridPartitionedTableModel( + model: IrisGridModel +): model is IrisGridPartitionedTableModel { + return ( + (model as IrisGridPartitionedTableModel).partitionedTable !== undefined + ); +} + class IrisGridPartitionedTableModel extends IrisGridModel { private irisFormatter: Formatter; - private partitionedTable: PartitionedTable; + readonly partitionedTable: PartitionedTable; // Track every getTable and close them all at the end (test opening a closed getTable) // Test getTable(key) and hold a reference to it, call close on PartitionedTable, check if table is still open - private model: IrisGridTableModel | EmptyIrisGridModel; + model: IrisGridTableModel | EmptyIrisGridModel; private partitionKeys: unknown[]; + private keyTable: Table | null = null; + /** * @param dh JSAPI instance * @param table Partitioned table to be used in the model @@ -62,7 +72,8 @@ class IrisGridPartitionedTableModel extends IrisGridModel { super(dh); this.partitionedTable = partitionedTable; this.irisFormatter = formatter; - this.partitionKeys = this.partitionedTable.getKeys().values().next().value; + const initialKey = this.partitionedTable.getKeys().values().next().value; + this.partitionKeys = Array.isArray(initialKey) ? initialKey : [initialKey]; this.model = new EmptyIrisGridModel(dh); } @@ -214,7 +225,7 @@ class IrisGridPartitionedTableModel extends IrisGridModel { return this.model.isFilterRequired; } - static get isPartitionRequired(): boolean { + get isPartitionRequired(): boolean { return true; } @@ -326,8 +337,8 @@ class IrisGridPartitionedTableModel extends IrisGridModel { return this.model.textSnapshot(ranges, includeHeaders, formatValue); } - valuesTable(column: Column): Promise
{ - return this.model.valuesTable(column); + valuesTable(columns: Column | readonly Column[]): Promise
{ + return this.model.valuesTable(columns); } close(): void { @@ -396,12 +407,12 @@ class IrisGridPartitionedTableModel extends IrisGridModel { return this.model.dataBarOptionsForCell(column, row, theme); } - get partitionColumns(): readonly Column[] { + get partitionColumns(): Column[] { // TODO - throw new Error('Method not implemented.'); + return this.partitionedTable.keyColumns; } - set partitionColumns(columns: readonly Column[]) { + set partitionColumns(columns: Column[]) { // Do nothing, partition columns of PartitionedTable can't be modified } @@ -409,40 +420,25 @@ class IrisGridPartitionedTableModel extends IrisGridModel { return this.partitionKeys; } - set partition(value: unknown[]) { - log.debug2('set partition', value); - this.partitionKeys = value; - this.updatePartitions(); + set partition(partition: unknown[]) { + log.debug2('set partition', partition); + this.partitionKeys = partition; + } + + get partitionKeysTable(): Table | null { + return this.keyTable; } async initializePartitionModel(): Promise { + this.keyTable = this.partitionedTable.keyTable; + const sorts = this.partitionColumns.map(column => column.sort().desc()); + this.keyTable.applySort(sorts); + this.keyTable.setViewport(0, 0, this.partitionColumns); + const initTable = await this.partitionedTable.getTable(this.partitionKeys); this.model = new IrisGridTableModel(this.dh, initTable, this.irisFormatter); - log.log('initializeModel', this.columns); - this.dispatchEvent( - new EventShimCustomEvent(IrisGridModel.EVENT.COLUMNS_CHANGED, { - detail: this.model.columns, - }) - ); - this.dispatchEvent( - new EventShimCustomEvent(IrisGridModel.EVENT.TABLE_CHANGED, { - detail: this.model.table, - }) - ); return Promise.resolve(this.model); } - - private async updatePartitions(): Promise { - this.model.table = - this.partitionKeys.length > 0 - ? await this.partitionedTable.getTable(this.partitionKeys) - : await this.partitionedTable.getMergedTable(); - this.dispatchEvent( - new EventShimCustomEvent(IrisGridModel.EVENT.TABLE_CHANGED, { - detail: this.model.table, - }) - ); - } } export default IrisGridPartitionedTableModel; diff --git a/packages/iris-grid/src/IrisGridProxyModel.ts b/packages/iris-grid/src/IrisGridProxyModel.ts index 819e984542..db6287706b 100644 --- a/packages/iris-grid/src/IrisGridProxyModel.ts +++ b/packages/iris-grid/src/IrisGridProxyModel.ts @@ -30,7 +30,9 @@ import { MoveOperation, } from '@deephaven/grid'; import IrisGridTableModel from './IrisGridTableModel'; -import IrisGridPartitionedTableModel from './IrisGridPartitionedTableModel'; +import IrisGridPartitionedTableModel, { + isIrisGridPartitionedTableModel, +} from './IrisGridPartitionedTableModel'; import IrisGridTreeTableModel from './IrisGridTreeTableModel'; import IrisGridModel from './IrisGridModel'; import { @@ -127,13 +129,13 @@ class IrisGridProxyModel extends IrisGridModel { const oldModel = this.model; - if (oldModel !== this.originalModel) { + if (oldModel !== this.originalModel && oldModel !== this.model) { oldModel.close(); } this.model = model; - if (this.listenerCount > 0) { + if (this.listenerCount > 0 && oldModel !== this.model) { this.addListeners(model); } @@ -460,11 +462,11 @@ class IrisGridProxyModel extends IrisGridModel { } get partitionColumns(): readonly Column[] { - return this.model.partitionColumns; + return this.originalModel.partitionColumns; } set partitionColumns(columns: readonly Column[]) { - this.model.partitionColumns = [...columns]; + this.originalModel.partitionColumns = columns; } get description(): string { @@ -492,11 +494,33 @@ class IrisGridProxyModel extends IrisGridModel { } get partition(): readonly unknown[] { - return this.model.partition; + return this.originalModel.partition; } set partition(partition: readonly unknown[]) { - this.model.partition = partition; + log.debug('set partition', partition); + this.originalModel.partition = partition; + if (isIrisGridPartitionedTableModel(this.originalModel)) { + if (partition.length === 0) { + this.setNextModel( + this.originalModel.partitionedTable + .getMergedTable() + .then(table => makeModel(this.dh, table, this.formatter)) + ); + return; + } + // Singular keys are not in arrays + const tablePromise = this.originalModel.partitionedTable.getTable( + partition.length > 1 ? partition : partition[0] + ); + this.setNextModel( + tablePromise.then(table => makeModel(this.dh, table, this.formatter)) + ); + } + } + + get partitionKeysTable(): Table | null { + return this.originalModel.partitionKeysTable; } get formatter(): Formatter { @@ -621,11 +645,11 @@ class IrisGridProxyModel extends IrisGridModel { } get isFilterRequired(): boolean { - return this.model.isFilterRequired; + return this.originalModel.isFilterRequired; } get isPartitionRequired(): boolean { - return this.model.isPartitionRequired; + return this.originalModel.isPartitionRequired; } get isEditable(): boolean { diff --git a/packages/iris-grid/src/IrisGridTableModel.ts b/packages/iris-grid/src/IrisGridTableModel.ts index 53096994dd..6f1cf5b423 100644 --- a/packages/iris-grid/src/IrisGridTableModel.ts +++ b/packages/iris-grid/src/IrisGridTableModel.ts @@ -7,13 +7,14 @@ import type { ColumnStatistics, CustomColumn, dh as DhType, + FilterCondition, InputTable, LayoutHints, Table, ValueTypeUnion, } from '@deephaven/jsapi-types'; import Log from '@deephaven/log'; -import { Formatter } from '@deephaven/jsapi-utils'; +import { Formatter, TableUtils } from '@deephaven/jsapi-utils'; import { EventShimCustomEvent, PromiseUtils, @@ -40,6 +41,10 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { private _partitionColumns: Column[] = []; + private partitionFilters: FilterCondition[] = []; + + private _partitionTable: Table | null = null; + /** * @param dh JSAPI instance * @param table Iris data table to be used in the model @@ -196,14 +201,61 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { set partitionColumns(partitionColumns: Column[]) { this._partitionColumns = partitionColumns; + if (this._partitionColumns.length !== this._partition.length) { + this.initializePartition(); + } } get partition(): unknown[] { return this._partition; } - set partition(partition: unknown[]) { - this._partition = partition; + set partition(partitions: unknown[]) { + log.log(partitions); + const partitionFilters = []; + + for (let i = 0; i < this.partitionColumns.length; i += 1) { + const partition = partitions[i]; + const partitionColumn = this.partitionColumns[i]; + + if ( + partition != null && + !(TableUtils.isCharType(partitionColumn.type) && partition === '') + ) { + const partitionText = TableUtils.isCharType(partitionColumn.type) + ? this.displayString( + partition, + partitionColumn.type, + partitionColumn.name + ) + : partition.toString(); + const partitionFilter = this.tableUtils.makeQuickFilterFromComponent( + partitionColumn, + partitionText + ); + if (partitionFilter !== null) { + partitionFilters.push(partitionFilter); + } + } + } + + const prevFilters = super.filter.slice( + 0, + super.filter.length - this.partitionFilters.length + ); + this._partition = partitions; + this.partitionFilters = partitionFilters; + this.filter = prevFilters; + } + + get partitionKeysTable(): Table | null { + return this._partitionTable; + } + + set filter(filter: FilterCondition[]) { + this.closeSubscription(); + this.table.applyFilter([...filter, ...this.partitionFilters]); + this.applyViewport(); } set totalsConfig(totalsConfig: UITotalsTableConfig | null) { @@ -258,7 +310,7 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { } get isPartitionRequired(): boolean { - return this.table.isUncoalesced; + return this.table.isUncoalesced && this.isValuesTableAvailable; } isFilterable(columnIndex: ModelIndex): boolean { @@ -410,6 +462,27 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { get isSeekRowAvailable(): boolean { return this.table.seekRow != null; } + + private async initializePartition(): Promise { + const table = await this.valuesTable(this.partitionColumns); + + const columns = this.table.columns.slice(0, this.partitionColumns.length); + const sorts = columns.map(column => column.sort().desc()); + this.table.applySort(sorts); + this.table.setViewport(0, 0, columns); + + const data = await this.table.getViewportData(); + if (data.rows.length > 0) { + const row = data.rows[0]; + const values = columns.map(column => row.get(column)); + + this.partition = values; + } else { + log.info('Table does not have any data'); + this.partition = []; + } + this._partitionTable = table; + } } export default IrisGridTableModel; diff --git a/packages/jsapi-types/src/dh.types.ts b/packages/jsapi-types/src/dh.types.ts index e2a8396bbe..89bcd97dd1 100644 --- a/packages/jsapi-types/src/dh.types.ts +++ b/packages/jsapi-types/src/dh.types.ts @@ -962,8 +962,11 @@ export interface TableTemplate extends Evented { export interface PartitionedTable extends Evented, PartitionedTableStatic { readonly size: number; + readonly columns: Column[]; + readonly keyColumns: Column[]; + readonly keyTable: Table; - getTable: (key: object) => Promise
; + getTable: (key: unknown) => Promise
; getMergedTable: () => Promise
; getKeys: () => Set; From 7ead3b587f2bad3b28608f352b1777e672ec0695 Mon Sep 17 00:00:00 2001 From: georgecwan Date: Thu, 16 Nov 2023 12:27:59 -0800 Subject: [PATCH 04/33] Fix partially selected partitions --- .../src/panels/IrisGridPanel.tsx | 2 - packages/iris-grid/src/IrisGrid.tsx | 22 +++---- packages/iris-grid/src/IrisGridModel.ts | 4 ++ .../src/IrisGridPartitionSelector.tsx | 57 ++++++++++++++++--- .../src/IrisGridPartitionedTableModel.tsx | 1 - packages/iris-grid/src/IrisGridProxyModel.ts | 29 ++++++++++ 6 files changed, 88 insertions(+), 27 deletions(-) diff --git a/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx b/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx index 117be35cbc..3983ceca3f 100644 --- a/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx +++ b/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx @@ -1236,7 +1236,6 @@ export class IrisGridPanel extends PureComponent< model, movedColumns, movedRows, - partitions, partitionColumns, quickFilters, reverseType, @@ -1318,7 +1317,6 @@ export class IrisGridPanel extends PureComponent< isStuckToRight={isStuckToRight} movedColumns={movedColumns} movedRows={movedRows} - partitions={partitions} partitionColumns={partitionColumns} quickFilters={quickFilters} reverseType={reverseType} diff --git a/packages/iris-grid/src/IrisGrid.tsx b/packages/iris-grid/src/IrisGrid.tsx index 9e7f4bc8f4..5a80ceb01d 100644 --- a/packages/iris-grid/src/IrisGrid.tsx +++ b/packages/iris-grid/src/IrisGrid.tsx @@ -284,7 +284,6 @@ export interface IrisGridProps { onDataSelected: (index: ModelIndex, map: Record) => void; onStateChange: (irisGridState: IrisGridState, gridState: GridState) => void; onAdvancedSettingsChange: AdvancedSettingsMenuCallback; - partitions: (string | null)[]; partitionColumns: Column[]; sorts: readonly Sort[]; reverseType: ReverseType; @@ -458,7 +457,6 @@ export class IrisGrid extends Component { onError: (): void => undefined, onStateChange: (): void => undefined, onAdvancedSettingsChange: (): void => undefined, - partitions: [], quickFilters: EMPTY_MAP, selectDistinctColumns: EMPTY_ARRAY, sorts: EMPTY_ARRAY, @@ -573,6 +571,7 @@ export class IrisGrid extends Component { this.handleDownloadCompleted = this.handleDownloadCompleted.bind(this); this.handlePartitionChange = this.handlePartitionChange.bind(this); this.handlePartitionMerge = this.handlePartitionMerge.bind(this); + this.handlePartitionKeyTable = this.handlePartitionKeyTable.bind(this); this.handleColumnVisibilityChanged = this.handleColumnVisibilityChanged.bind(this); this.handleColumnVisibilityReset = @@ -1883,19 +1882,6 @@ export class IrisGrid extends Component { this.initFormatter(); } - async loadPartitionsTable(): Promise { - const { model } = this.props; - log.log('loadPartitionsTable'); - - try { - this.setState({ isSelectingPartition: true }, () => { - this.initState(); - }); - } catch (error) { - this.handleTableLoadError(error); - } - } - copyCell( columnIndex: GridRangeIndex, rowIndex: GridRangeIndex, @@ -2307,6 +2293,11 @@ export class IrisGrid extends Component { model.partition = []; } + handlePartitionKeyTable(): void { + const { model } = this.props; + model.openPartitionKeysTable(); + } + handleTableLoadError(error: unknown): void { if (PromiseUtils.isCanceled(error)) { return; @@ -4368,6 +4359,7 @@ export class IrisGrid extends Component { partitions={model.partition} onChange={this.handlePartitionChange} onMerge={this.handlePartitionMerge} + onKeyTable={this.handlePartitionKeyTable} /> )} diff --git a/packages/iris-grid/src/IrisGridModel.ts b/packages/iris-grid/src/IrisGridModel.ts index 4599ad2485..4064334a3f 100644 --- a/packages/iris-grid/src/IrisGridModel.ts +++ b/packages/iris-grid/src/IrisGridModel.ts @@ -220,6 +220,10 @@ abstract class IrisGridModel< return null; } + openPartitionKeysTable(): void { + throw new Error('Method not implemented.'); + } + /** * @returns The formatter used when formatting data */ diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.tsx b/packages/iris-grid/src/IrisGridPartitionSelector.tsx index 219bc028d0..c57a9bbc5b 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.tsx +++ b/packages/iris-grid/src/IrisGridPartitionSelector.tsx @@ -32,6 +32,7 @@ interface IrisGridPartitionSelectorProps { partitions: readonly unknown[]; onMerge: () => void; onChange: (partitions: readonly unknown[]) => void; + onKeyTable: () => void; } interface IrisGridPartitionSelectorState { isShowingKeys: boolean; @@ -45,13 +46,14 @@ class IrisGridPartitionSelector extends Component< static defaultProps = { onChange: (): void => undefined, onMerge: (): void => undefined, - onDone: (): void => undefined, + onKeyTable: (): void => undefined, partitions: [], }; constructor(props: IrisGridPartitionSelectorProps) { super(props); + this.handleKeyTableClick = this.handleKeyTableClick.bind(this); this.handleMergeClick = this.handleMergeClick.bind(this); this.handlePartitionChange = this.handlePartitionChange.bind(this); this.handlePartitionSelect = this.handlePartitionSelect.bind(this); @@ -119,9 +121,17 @@ class IrisGridPartitionSelector extends Component< selectorSearch: (PartitionSelectorSearch | null)[]; + handleKeyTableClick(): void { + log.debug2('handleKeyTableClick'); + + this.setState({ isShowingKeys: true }); + this.sendKeyTable(); + } + handleMergeClick(): void { log.debug2('handleMergeClick'); + this.setState({ isShowingKeys: false }); this.sendMerge(); } @@ -167,9 +177,12 @@ class IrisGridPartitionSelector extends Component< this.updatePartitions(index, newPartitions, prevTables).then( ([partitions, partitionTables]) => { - this.setState({ partitions, partitionTables }, () => { - this.sendUpdate(); - }); + this.setState( + { isShowingKeys: false, partitions, partitionTables }, + () => { + this.sendUpdate(); + } + ); } ); } @@ -215,6 +228,15 @@ class IrisGridPartitionSelector extends Component< onMerge(); } + sendKeyTable(): void { + log.debug2('sendOpenKeys'); + + this.debounceUpdate.cancel(); + + const { onKeyTable } = this.props; + onKeyTable(); + } + getDisplayValue(column: Column, index: number): string { const { partitions } = this.state; const partition = partitions[index]; @@ -280,11 +302,27 @@ class IrisGridPartitionSelector extends Component< return Promise.all(dataPromises).then(tableData => { // Check if a partition is selected (no partitions for key table or merge table) - if (partitions[0] === undefined) { + if (partitions[index] === undefined) { return [[...partitions], partitionTables]; } - // Check if partitions are valid + // Check if columns before index are defined const validPartitions = partitions.slice(0, index + 1); + if (validPartitions.includes(undefined)) { + for ( + let emptyIndex = 0; + emptyIndex < validPartitions.length; + emptyIndex += 1 + ) { + if (validPartitions[emptyIndex] === undefined) { + return this.updatePartitions( + emptyIndex, + columns.map(c => tableData[0].rows[0].get(c)), + partitionTables + ); + } + } + } + // Check if columns after index are defined for (let i = 1; i < tableData.length; i += 1) { const data = tableData[i]; if (data.rows.length > 0 && partitions[index + i] !== undefined) { @@ -303,7 +341,7 @@ class IrisGridPartitionSelector extends Component< render(): JSX.Element { const { columns, dh, getFormattedString } = this.props; - const { isShowingKeys, partitionTables } = this.state; + const { isShowingKeys, partitions, partitionTables } = this.state; const partitionSelectorSearch = columns.map( (column, index) => @@ -366,10 +404,10 @@ class IrisGridPartitionSelector extends Component< @@ -379,6 +417,7 @@ class IrisGridPartitionSelector extends Component< onClick={this.handleMergeClick} kind="inline" icon={} + disabled={!isShowingKeys && partitions.length === 0} > merge diff --git a/packages/iris-grid/src/IrisGridPartitionedTableModel.tsx b/packages/iris-grid/src/IrisGridPartitionedTableModel.tsx index d9925eab57..88d3d5d06b 100644 --- a/packages/iris-grid/src/IrisGridPartitionedTableModel.tsx +++ b/packages/iris-grid/src/IrisGridPartitionedTableModel.tsx @@ -22,7 +22,6 @@ import type { ValueTypeUnion, } from '@deephaven/jsapi-types'; import { Formatter } from '@deephaven/jsapi-utils'; -import { EventShimCustomEvent } from '@deephaven/utils'; import Log from '@deephaven/log'; import { ColumnName, diff --git a/packages/iris-grid/src/IrisGridProxyModel.ts b/packages/iris-grid/src/IrisGridProxyModel.ts index db6287706b..f1444a7f41 100644 --- a/packages/iris-grid/src/IrisGridProxyModel.ts +++ b/packages/iris-grid/src/IrisGridProxyModel.ts @@ -516,6 +516,11 @@ class IrisGridProxyModel extends IrisGridModel { this.setNextModel( tablePromise.then(table => makeModel(this.dh, table, this.formatter)) ); + } else if ( + isIrisGridTableModelTemplate(this.originalModel) && + this.model !== this.originalModel + ) { + this.setNextModel(Promise.resolve(this.originalModel)); } } @@ -523,6 +528,30 @@ class IrisGridProxyModel extends IrisGridModel { return this.originalModel.partitionKeysTable; } + openPartitionKeysTable(): void { + log.debug('opening keysTable'); + if (!this.originalModel.partitionKeysTable) { + return; + } + if (isIrisGridPartitionedTableModel(this.originalModel)) { + this.setNextModel( + this.originalModel.partitionKeysTable + .selectDistinct(this.originalModel.partitionColumns) + .then(table => makeModel(this.dh, table, this.formatter)) + ); + } else { + this.setNextModel( + Promise.resolve( + makeModel( + this.dh, + this.originalModel.partitionKeysTable, + this.formatter + ) + ) + ); + } + } + get formatter(): Formatter { return this.model.formatter; } From 35120eb5fe0608247082a26e27363f06db57f85c Mon Sep 17 00:00:00 2001 From: georgecwan Date: Wed, 22 Nov 2023 08:13:56 -0800 Subject: [PATCH 05/33] Fix minor bugs and button formatting --- .../src/IrisGridPartitionSelector.scss | 39 +++++++++++---- .../src/IrisGridPartitionSelector.tsx | 47 +++++++++++-------- .../src/IrisGridPartitionedTableModel.tsx | 43 +++++++++-------- packages/iris-grid/src/IrisGridProxyModel.ts | 38 +++++++++------ packages/iris-grid/src/IrisGridTableModel.ts | 18 ++++--- .../iris-grid/src/PartitionSelectorSearch.tsx | 6 ++- 6 files changed, 120 insertions(+), 71 deletions(-) diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.scss b/packages/iris-grid/src/IrisGridPartitionSelector.scss index 01e6684870..8d8941ebff 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.scss +++ b/packages/iris-grid/src/IrisGridPartitionSelector.scss @@ -1,11 +1,19 @@ @import '@deephaven/components/scss/custom.scss'; -$partition-selector-color: $white; +$partition-selector-color: $gray-400; $partition-selector-input-height: 2.25em; .iris-grid-partition-selector { display: flex; flex-wrap: wrap; background: $gray-850; vertical-align: middle; + .table-name { + text-overflow: ellipsis; + display: flex; + flex-direction: row; + align-items: center; + padding: 0.5em; + white-space: nowrap; + } .column-name { text-overflow: ellipsis; display: flex; @@ -21,23 +29,36 @@ $partition-selector-input-height: 2.25em; width: auto; align-items: center; .form-control { - background-color: transparent; - border: 2px solid $partition-selector-color; + background-color: $gray-700; + border: 1px solid $partition-selector-color; border-right: none; height: $partition-selector-input-height; } .btn { - background-color: transparent; + color: $white; + padding: 0.25em 0.5em; + background-color: $gray-700; + border: 1px solid $partition-selector-color; margin: 0; + height: $partition-selector-input-height; } } .iris-grid-partition-selector-spacer { flex-grow: 1; } - .btn-outline-primary { - color: $white; - border-color: $white; - margin: 0.35em 0.3em; - padding: 0.25em 0.5em; + .partition-button-group { + border: 2px $gray-900; + border-style: none solid none solid; + margin: 0.25em 0 0.25em 0; + padding: 0 0.5em 0 0.5em; + .btn { + color: $white; + background-color: $gray-900; + border-color: $gray-400; + border-radius: 3px; + font-size: 0.75rem; + margin: 0.35em 0.3em; + padding: 0.25em 0.5em; + } } } diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.tsx b/packages/iris-grid/src/IrisGridPartitionSelector.tsx index c57a9bbc5b..dc790f1d2c 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.tsx +++ b/packages/iris-grid/src/IrisGridPartitionSelector.tsx @@ -401,26 +401,33 @@ class IrisGridPartitionSelector extends Component< )); return (
- - +
+ Partitioned Table +
+
+ + +
{partitionSelectors}
); diff --git a/packages/iris-grid/src/IrisGridPartitionedTableModel.tsx b/packages/iris-grid/src/IrisGridPartitionedTableModel.tsx index 88d3d5d06b..b8c3eb4795 100644 --- a/packages/iris-grid/src/IrisGridPartitionedTableModel.tsx +++ b/packages/iris-grid/src/IrisGridPartitionedTableModel.tsx @@ -52,7 +52,7 @@ class IrisGridPartitionedTableModel extends IrisGridModel { // Track every getTable and close them all at the end (test opening a closed getTable) // Test getTable(key) and hold a reference to it, call close on PartitionedTable, check if table is still open - model: IrisGridTableModel | EmptyIrisGridModel; + model: IrisGridModel; private partitionKeys: unknown[]; @@ -71,9 +71,17 @@ class IrisGridPartitionedTableModel extends IrisGridModel { super(dh); this.partitionedTable = partitionedTable; this.irisFormatter = formatter; - const initialKey = this.partitionedTable.getKeys().values().next().value; - this.partitionKeys = Array.isArray(initialKey) ? initialKey : [initialKey]; + // Get the most recently added key + let lastKey; + this.partitionedTable.getKeys().forEach(key => { + lastKey = key; + }); + this.partitionKeys = Array.isArray(lastKey) ? lastKey : [lastKey]; this.model = new EmptyIrisGridModel(dh); + + this.keyTable = this.partitionedTable.keyTable; + const sorts = this.partitionColumns.map(column => column.sort().desc()); + this.keyTable.applySort(sorts); } get rowCount(): number { @@ -128,11 +136,11 @@ class IrisGridPartitionedTableModel extends IrisGridModel { return this.model.valueForCell(column, row); } - get filter(): FilterCondition[] { + get filter(): readonly FilterCondition[] { return this.model.filter; } - set filter(value: FilterCondition[]) { + set filter(value: readonly FilterCondition[]) { this.model.filter = value; } @@ -152,27 +160,27 @@ class IrisGridPartitionedTableModel extends IrisGridModel { return this.model.displayString(value, columnType, columnName); } - get sort(): Sort[] { + get sort(): readonly Sort[] { return this.model.sort; } - set sort(value: Sort[]) { + set sort(value: readonly Sort[]) { this.model.sort = value; } - get customColumns(): ColumnName[] { + get customColumns(): readonly ColumnName[] { return this.model.customColumns; } - set customColumns(customColumns: ColumnName[]) { + set customColumns(customColumns: readonly ColumnName[]) { this.model.customColumns = customColumns; } - get formatColumns(): CustomColumn[] { + get formatColumns(): readonly CustomColumn[] { return this.model.formatColumns; } - set formatColumns(formatColumns: CustomColumn[]) { + set formatColumns(formatColumns: readonly CustomColumn[]) { this.model.formatColumns = formatColumns; } @@ -280,11 +288,11 @@ class IrisGridPartitionedTableModel extends IrisGridModel { return this.model.isTotalsAvailable; } - get selectDistinctColumns(): ColumnName[] { + get selectDistinctColumns(): readonly ColumnName[] { return this.model.selectDistinctColumns; } - set selectDistinctColumns(names: ColumnName[]) { + set selectDistinctColumns(names: readonly ColumnName[]) { this.model.selectDistinctColumns = names; } @@ -379,11 +387,11 @@ class IrisGridPartitionedTableModel extends IrisGridModel { return this.model.isSeekRowAvailable; } - get columnHeaderGroups(): ColumnHeaderGroup[] { + get columnHeaderGroups(): readonly ColumnHeaderGroup[] { return this.model.columnHeaderGroups; } - set columnHeaderGroups(groups: ColumnHeaderGroup[]) { + set columnHeaderGroups(groups: readonly ColumnHeaderGroup[]) { this.model.columnHeaderGroups = groups; } @@ -429,11 +437,6 @@ class IrisGridPartitionedTableModel extends IrisGridModel { } async initializePartitionModel(): Promise { - this.keyTable = this.partitionedTable.keyTable; - const sorts = this.partitionColumns.map(column => column.sort().desc()); - this.keyTable.applySort(sorts); - this.keyTable.setViewport(0, 0, this.partitionColumns); - const initTable = await this.partitionedTable.getTable(this.partitionKeys); this.model = new IrisGridTableModel(this.dh, initTable, this.irisFormatter); return Promise.resolve(this.model); diff --git a/packages/iris-grid/src/IrisGridProxyModel.ts b/packages/iris-grid/src/IrisGridProxyModel.ts index f1444a7f41..2325125620 100644 --- a/packages/iris-grid/src/IrisGridProxyModel.ts +++ b/packages/iris-grid/src/IrisGridProxyModel.ts @@ -129,13 +129,13 @@ class IrisGridProxyModel extends IrisGridModel { const oldModel = this.model; - if (oldModel !== this.originalModel && oldModel !== this.model) { + if (oldModel !== this.originalModel) { oldModel.close(); } this.model = model; - if (this.listenerCount > 0 && oldModel !== this.model) { + if (this.listenerCount > 0) { this.addListeners(model); } @@ -503,9 +503,12 @@ class IrisGridProxyModel extends IrisGridModel { if (isIrisGridPartitionedTableModel(this.originalModel)) { if (partition.length === 0) { this.setNextModel( - this.originalModel.partitionedTable - .getMergedTable() - .then(table => makeModel(this.dh, table, this.formatter)) + this.originalModel.partitionedTable.getMergedTable().then(table => { + const newModel = makeModel(this.dh, table, this.formatter); + (this.originalModel as IrisGridPartitionedTableModel).model = + newModel; + return newModel; + }) ); return; } @@ -514,7 +517,12 @@ class IrisGridProxyModel extends IrisGridModel { partition.length > 1 ? partition : partition[0] ); this.setNextModel( - tablePromise.then(table => makeModel(this.dh, table, this.formatter)) + tablePromise.then(table => { + const newModel = makeModel(this.dh, table, this.formatter); + (this.originalModel as IrisGridPartitionedTableModel).model = + newModel; + return newModel; + }) ); } else if ( isIrisGridTableModelTemplate(this.originalModel) && @@ -530,6 +538,7 @@ class IrisGridProxyModel extends IrisGridModel { openPartitionKeysTable(): void { log.debug('opening keysTable'); + this.originalModel.partition = []; if (!this.originalModel.partitionKeysTable) { return; } @@ -537,17 +546,18 @@ class IrisGridProxyModel extends IrisGridModel { this.setNextModel( this.originalModel.partitionKeysTable .selectDistinct(this.originalModel.partitionColumns) - .then(table => makeModel(this.dh, table, this.formatter)) + .then(table => { + const newModel = makeModel(this.dh, table, this.formatter); + (this.originalModel as IrisGridPartitionedTableModel).model = + newModel; + return newModel; + }) ); } else { this.setNextModel( - Promise.resolve( - makeModel( - this.dh, - this.originalModel.partitionKeysTable, - this.formatter - ) - ) + this.originalModel.partitionKeysTable + .copy() + .then(table => makeModel(this.dh, table, this.formatter)) ); } } diff --git a/packages/iris-grid/src/IrisGridTableModel.ts b/packages/iris-grid/src/IrisGridTableModel.ts index 6f1cf5b423..32dfdd16a6 100644 --- a/packages/iris-grid/src/IrisGridTableModel.ts +++ b/packages/iris-grid/src/IrisGridTableModel.ts @@ -211,7 +211,7 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { } set partition(partitions: unknown[]) { - log.log(partitions); + log.log('setting partition', partitions); const partitionFilters = []; for (let i = 0; i < this.partitionColumns.length; i += 1) { @@ -464,14 +464,18 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { } private async initializePartition(): Promise { - const table = await this.valuesTable(this.partitionColumns); + log.debug('Initializing partition'); + const partitionTable = await this.valuesTable(this.partitionColumns); - const columns = this.table.columns.slice(0, this.partitionColumns.length); + const columns = partitionTable.columns.slice( + 0, + this.partitionColumns.length + ); const sorts = columns.map(column => column.sort().desc()); - this.table.applySort(sorts); - this.table.setViewport(0, 0, columns); + partitionTable.applySort(sorts); + partitionTable.setViewport(0, 0, columns); - const data = await this.table.getViewportData(); + const data = await partitionTable.getViewportData(); if (data.rows.length > 0) { const row = data.rows[0]; const values = columns.map(column => row.get(column)); @@ -481,7 +485,7 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { log.info('Table does not have any data'); this.partition = []; } - this._partitionTable = table; + this._partitionTable = partitionTable; } } diff --git a/packages/iris-grid/src/PartitionSelectorSearch.tsx b/packages/iris-grid/src/PartitionSelectorSearch.tsx index e168a72dea..0b4b9ce693 100644 --- a/packages/iris-grid/src/PartitionSelectorSearch.tsx +++ b/packages/iris-grid/src/PartitionSelectorSearch.tsx @@ -3,7 +3,11 @@ import PropTypes from 'prop-types'; import debounce from 'lodash.debounce'; import { TableUtils } from '@deephaven/jsapi-utils'; import type { Column, dh as DhType, Table } from '@deephaven/jsapi-types'; -import { ItemList, LoadingSpinner } from '@deephaven/components'; +import { + ItemList, + LoadingSpinner, + SearchableCombobox, +} from '@deephaven/components'; import Log from '@deephaven/log'; import { CanceledPromiseError } from '@deephaven/utils'; import './PartitionSelectorSearch.scss'; From 64cfaf4c82f3e065509c9d4b9026ff00395a2175 Mon Sep 17 00:00:00 2001 From: georgecwan Date: Wed, 29 Nov 2023 12:48:30 -0800 Subject: [PATCH 06/33] Add new partition selector dropdowns --- .../src/IrisGridPartitionSelector.scss | 30 +- .../src/IrisGridPartitionSelector.tsx | 353 +++++++---------- .../src/PartitionSelectorSearch.scss | 25 -- .../src/PartitionSelectorSearch.test.tsx | 78 ---- .../iris-grid/src/PartitionSelectorSearch.tsx | 370 ------------------ 5 files changed, 145 insertions(+), 711 deletions(-) delete mode 100644 packages/iris-grid/src/PartitionSelectorSearch.scss delete mode 100644 packages/iris-grid/src/PartitionSelectorSearch.test.tsx delete mode 100644 packages/iris-grid/src/PartitionSelectorSearch.tsx diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.scss b/packages/iris-grid/src/IrisGridPartitionSelector.scss index 8d8941ebff..31bb178e33 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.scss +++ b/packages/iris-grid/src/IrisGridPartitionSelector.scss @@ -1,11 +1,11 @@ @import '@deephaven/components/scss/custom.scss'; $partition-selector-color: $gray-400; -$partition-selector-input-height: 2.25em; .iris-grid-partition-selector { display: flex; flex-wrap: wrap; background: $gray-850; vertical-align: middle; + padding: 0.2em 0 0.2em 0; .table-name { text-overflow: ellipsis; display: flex; @@ -22,42 +22,24 @@ $partition-selector-input-height: 2.25em; padding: 0.5em; white-space: nowrap; } - div { - flex-grow: 0; - } - .input-group { + .column-selector { + display: flex; width: auto; align-items: center; - .form-control { - background-color: $gray-700; - border: 1px solid $partition-selector-color; - border-right: none; - height: $partition-selector-input-height; - } - .btn { - color: $white; - padding: 0.25em 0.5em; - background-color: $gray-700; - border: 1px solid $partition-selector-color; - margin: 0; - height: $partition-selector-input-height; - } } .iris-grid-partition-selector-spacer { flex-grow: 1; } .partition-button-group { - border: 2px $gray-900; + border: 1px $gray-900; border-style: none solid none solid; - margin: 0.25em 0 0.25em 0; - padding: 0 0.5em 0 0.5em; + padding: 0 0.3em 0 0.3em; .btn { color: $white; background-color: $gray-900; border-color: $gray-400; border-radius: 3px; - font-size: 0.75rem; - margin: 0.35em 0.3em; + margin: 0.3em 0.3em; padding: 0.25em 0.5em; } } diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.tsx b/packages/iris-grid/src/IrisGridPartitionSelector.tsx index dc790f1d2c..ee78b0f550 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.tsx +++ b/packages/iris-grid/src/IrisGridPartitionSelector.tsx @@ -1,12 +1,7 @@ import React, { Component } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, DropdownMenu, Tooltip } from '@deephaven/components'; -import { - vsTriangleDown, - vsChevronRight, - vsMerge, - vsKey, -} from '@deephaven/icons'; +import { Button, Option, Select } from '@deephaven/components'; +import { vsChevronRight, vsMerge, vsKey } from '@deephaven/icons'; import Log from '@deephaven/log'; import debounce from 'lodash.debounce'; import type { @@ -17,16 +12,21 @@ import type { } from '@deephaven/jsapi-types'; import { TableUtils } from '@deephaven/jsapi-utils'; import deepEqual from 'deep-equal'; -import PartitionSelectorSearch from './PartitionSelectorSearch'; import './IrisGridPartitionSelector.scss'; import IrisGridUtils from './IrisGridUtils'; const log = Log.module('IrisGridPartitionSelector'); const PARTITION_CHANGE_DEBOUNCE_MS = 250; -interface IrisGridPartitionSelectorProps { + +interface Item { + key: React.Key; + value: string; +} + +interface IrisGridPartitionSelectorProps { dh: DhType; - getFormattedString: (value: T, type: string, name: string) => string; + getFormattedString: (value: unknown, type: string, name: string) => string; table: Table; columns: readonly Column[]; partitions: readonly unknown[]; @@ -36,11 +36,12 @@ interface IrisGridPartitionSelectorProps { } interface IrisGridPartitionSelectorState { isShowingKeys: boolean; + selectorValue: readonly string[]; partitions: readonly unknown[]; - partitionTables: Table[] | null; + partitionColumnValues: readonly Item[][]; } -class IrisGridPartitionSelector extends Component< - IrisGridPartitionSelectorProps, +class IrisGridPartitionSelector extends Component< + IrisGridPartitionSelectorProps, IrisGridPartitionSelectorState > { static defaultProps = { @@ -50,27 +51,22 @@ class IrisGridPartitionSelector extends Component< partitions: [], }; - constructor(props: IrisGridPartitionSelectorProps) { + constructor(props: IrisGridPartitionSelectorProps) { super(props); this.handleKeyTableClick = this.handleKeyTableClick.bind(this); this.handleMergeClick = this.handleMergeClick.bind(this); - this.handlePartitionChange = this.handlePartitionChange.bind(this); this.handlePartitionSelect = this.handlePartitionSelect.bind(this); - this.handlePartitionListResized = - this.handlePartitionListResized.bind(this); - this.handleSearchOpened = this.handleSearchOpened.bind(this); - this.handleSearchClosed = this.handleSearchClosed.bind(this); const { dh, columns, partitions } = props; this.tableUtils = new TableUtils(dh); - this.searchMenu = columns.map(() => null); - this.selectorSearch = columns.map(() => null); + this.partitionTables = null; this.state = { isShowingKeys: false, + selectorValue: columns.map(() => ''), partitions, - partitionTables: null, + partitionColumnValues: columns.map(() => [] as Item[]), }; } @@ -78,48 +74,33 @@ class IrisGridPartitionSelector extends Component< const { columns, table } = this.props; const { partitions } = this.state; - const partitionTables = await Promise.all( + this.partitionTables = await Promise.all( columns.map(async (_, i) => table.selectDistinct(columns.slice(0, i + 1))) ); - this.updatePartitions(0, partitions, partitionTables).then( - ([newPartitions, newPartitionTables]) => { - this.setState({ - partitions: newPartitions, - partitionTables: newPartitionTables, - }); - } - ); + this.updatePartitions(0, partitions); } - componentDidUpdate(prevProps: IrisGridPartitionSelectorProps): void { + componentDidUpdate(prevProps: IrisGridPartitionSelectorProps): void { const { partitions: prevPartitions } = prevProps; const { partitions: newPartitions } = this.props; - const { partitionTables: prevTables } = this.state; if (!deepEqual(prevPartitions, newPartitions)) { - log.log(prevPartitions, newPartitions); - this.updatePartitions(0, newPartitions, prevTables).then( - ([partitions, partitionTables]) => { - this.setState({ - partitions, - partitionTables, - }); - } - ); + this.updatePartitions(0, newPartitions); } } componentWillUnmount(): void { - const { partitionTables } = this.state; - partitionTables?.forEach(table => table.close()); + this.partitionTables?.forEach(table => table.close()); this.debounceUpdate.cancel(); } tableUtils: TableUtils; - searchMenu: (DropdownMenu | null)[]; + // searchMenu: (DropdownMenu | null)[]; + + // selectorSearch: (PartitionSelectorSearch | null)[]; - selectorSearch: (PartitionSelectorSearch | null)[]; + partitionTables: Table[] | null; handleKeyTableClick(): void { log.debug2('handleKeyTableClick'); @@ -135,78 +116,27 @@ class IrisGridPartitionSelector extends Component< this.sendMerge(); } - handlePartitionChange( - index: number, - event: React.ChangeEvent - ): void { - log.debug2('handlePartitionChange'); - + handlePartitionSelect(index: number, partition: unknown): void { const { columns } = this.props; - const { partitions: prevPartitions, partitionTables: prevTables } = - this.state; - const { value: partition } = event.target; + const { partitions: prevPartitions } = this.state; - const newPartitions = [...prevPartitions]; - newPartitions[index] = - TableUtils.isCharType(columns[index].type) && partition.length > 0 - ? partition.charCodeAt(0).toString() - : partition; - - this.updatePartitions(index, newPartitions, prevTables).then( - ([partitions, partitionTables]) => { - this.setState({ - partitions, - partitionTables, - }); - - this.debounceUpdate(); - } - ); - } - - handlePartitionSelect(index: number, partition: string): void { - const { partitions: prevPartitions, partitionTables: prevTables } = - this.state; - const selectedMenu = this.searchMenu[index]; - if (selectedMenu) { - selectedMenu.closeMenu(); + if (prevPartitions[index] === partition) { + return; } + log.debug('handlePartitionSelect', index, partition); const newPartitions = [...prevPartitions]; - newPartitions[index] = partition; - - this.updatePartitions(index, newPartitions, prevTables).then( - ([partitions, partitionTables]) => { - this.setState( - { isShowingKeys: false, partitions, partitionTables }, - () => { - this.sendUpdate(); - } - ); - } + newPartitions[index] = TableUtils.isNumberType(columns[index].type) + ? Number(partition) + : partition; + + this.updatePartitions(index, newPartitions).then(() => + this.setState({ isShowingKeys: false }, () => { + this.sendUpdate(); + }) ); } - handlePartitionListResized(index: number): void { - const selectedMenu = this.searchMenu[index]; - if (selectedMenu) { - selectedMenu.scheduleUpdate(); - } - } - - handleSearchClosed(): void { - // Reset the table filter so it's ready next time user opens search - const { table } = this.props; - table.applyFilter([]); - } - - handleSearchOpened(index: number): void { - const selectedSearch = this.selectorSearch[index]; - if (selectedSearch) { - selectedSearch.focus(); - } - } - debounceUpdate = debounce((): void => { this.sendUpdate(); }, PARTITION_CHANGE_DEBOUNCE_MS); @@ -237,44 +167,46 @@ class IrisGridPartitionSelector extends Component< onKeyTable(); } - getDisplayValue(column: Column, index: number): string { + getDisplayValue(index: number, partition?: unknown): string { + const { columns } = this.props; const { partitions } = this.state; - const partition = partitions[index]; - if (partition == null) { + + const value = partition === undefined ? partitions[index] : partition; + if (value == null) { return ''; } - if (TableUtils.isCharType(column.type) && partition.toString().length > 0) { - return String.fromCharCode(parseInt(partition.toString(), 10)); + const column = columns[index]; + if (TableUtils.isCharType(column.type) && value.toString().length > 0) { + return String.fromCharCode(parseInt(value.toString(), 10)); } - return IrisGridUtils.convertValueToText(partition, column.type); + return IrisGridUtils.convertValueToText(value, column.type); } async updatePartitions( index: number, - partitions: readonly unknown[], - partitionTables: Table[] | null - ): Promise<[unknown[], Table[] | null]> { - if (!Array.isArray(partitionTables)) { - return [[...partitions], partitionTables]; + partitions: readonly unknown[] + ): Promise { + if (!Array.isArray(this.partitionTables)) { + return; } const { columns, getFormattedString, table } = this.props; const dataPromises = Array>(); - const partitionFilters = [...partitionTables[index].filter]; + const partitionFilters = [...this.partitionTables[index].filter]; - // Create partition filters + // Update partition filters for (let i = index; i < columns.length; i += 1) { const partition = partitions[i]; const partitionColumn = columns[i]; - partitionTables[i].applyFilter([...partitionFilters]); + this.partitionTables[i].applyFilter([...partitionFilters]); if ( partition != null && !(TableUtils.isCharType(partitionColumn.type) && partition === '') ) { const partitionText = TableUtils.isCharType(partitionColumn.type) ? getFormattedString( - partition as T, + partition, partitionColumn.type, partitionColumn.name ) @@ -300,104 +232,97 @@ class IrisGridPartitionSelector extends Component< ); } - return Promise.all(dataPromises).then(tableData => { - // Check if a partition is selected (no partitions for key table or merge table) - if (partitions[index] === undefined) { - return [[...partitions], partitionTables]; - } - // Check if columns before index are defined - const validPartitions = partitions.slice(0, index + 1); - if (validPartitions.includes(undefined)) { - for ( - let emptyIndex = 0; - emptyIndex < validPartitions.length; - emptyIndex += 1 - ) { - if (validPartitions[emptyIndex] === undefined) { - return this.updatePartitions( - emptyIndex, - columns.map(c => tableData[0].rows[0].get(c)), - partitionTables - ); - } - } - } - // Check if columns after index are defined - for (let i = 1; i < tableData.length; i += 1) { - const data = tableData[i]; - if (data.rows.length > 0 && partitions[index + i] !== undefined) { - validPartitions.push(partitions[index + i]); - } else { + // Check if a partition is selected (no partitions for key table or merge table) + if (partitions[index] === undefined) { + this.setState({ selectorValue: columns.map(() => '') }); + return; + } + + // Update Partition Values + const tableData = await Promise.all(dataPromises); + const validPartitions = partitions.slice(0, index + 1); + // Check if columns before index are defined + if (validPartitions.includes(undefined)) { + for ( + let emptyIndex = 0; + emptyIndex < validPartitions.length; + emptyIndex += 1 + ) { + if (validPartitions[emptyIndex] === undefined) { return this.updatePartitions( - index + i, - columns.map(c => tableData[i - 1].rows[0].get(c)), - partitionTables + emptyIndex, + columns.map(c => tableData[0].rows[0].get(c)) ); } } - return [validPartitions, partitionTables]; + } + // Check if columns after index are defined + for (let i = 1; i < tableData.length; i += 1) { + const data = tableData[i]; + if (data.rows.length > 0 && partitions[index + i] !== undefined) { + validPartitions.push(partitions[index + i]); + } else { + return this.updatePartitions( + index + i, + columns.map(c => tableData[i - 1].rows[0].get(c)) + ); + } + } + // Valid partitions found, update dropdown values + const newColumnValuesPromise = this.partitionTables?.map( + async (partitionTable, colIndex) => { + partitionTable.setViewport(0, partitionTable.size); + const data = await partitionTable.getViewportData(); + return data.rows.reduce((columnValues, row) => { + const column = columns[colIndex]; + const key = row.get(column); + columnValues.push({ + key, + value: getFormattedString(key, column.type, column.name), + }); + return columnValues; + }, [] as Item[]); + } + ); + const newColumnValues = await Promise.all(newColumnValuesPromise); + this.setState({ + partitions: validPartitions, + partitionColumnValues: newColumnValues, + selectorValue: validPartitions.map((_, i) => this.getDisplayValue(i)), }); } render(): JSX.Element { - const { columns, dh, getFormattedString } = this.props; - const { isShowingKeys, partitions, partitionTables } = this.state; - - const partitionSelectorSearch = columns.map( - (column, index) => - partitionTables && ( - - this.handlePartitionSelect(index, partition) - } - onListResized={() => this.handlePartitionListResized(index)} - ref={selectorSearch => { - this.selectorSearch[index] = selectorSearch; - }} - /> - ) - ); + const { columns } = this.props; + const { isShowingKeys, selectorValue, partitions, partitionColumnValues } = + this.state; + const partitionSelectors = columns.map((column, index) => ( - +
- {column.name}: -
-
- { - this.handlePartitionChange(index, e); - }} - className="form-control input-partition" - /> -
- -
+ {column.name}
- {/* */} -
- + + {columns.length - 1 === index || ( + <> +   + + + )} +
)); return (
@@ -407,7 +332,7 @@ class IrisGridPartitionSelector extends Component<
; columns: readonly Column[]; partitions: readonly unknown[]; onMerge: () => void; @@ -58,20 +61,23 @@ class IrisGridPartitionSelector extends Component< const { dh, columns, partitions } = props; this.tableUtils = new TableUtils(dh); + this.table = null; this.partitionTables = null; this.state = { isShowingKeys: false, selectorValue: columns.map(() => ''), partitions, - partitionColumnValues: columns.map(() => ({})), + partitionColumnValues: columns.map(() => ({ order: [], data: {} })), }; } async componentDidMount(): Promise { - const { columns, table } = this.props; + const { columns, tablePromise } = this.props; const { partitions } = this.state; + const table = await tablePromise; + this.table = table; this.partitionTables = await Promise.all( columns.map(async (_, i) => table.selectDistinct(columns.slice(0, i + 1))) ); @@ -88,15 +94,14 @@ class IrisGridPartitionSelector extends Component< } componentWillUnmount(): void { + this.table?.close(); this.partitionTables?.forEach(table => table.close()); this.debounceUpdate.cancel(); } tableUtils: TableUtils; - // searchMenu: (DropdownMenu | null)[]; - - // selectorSearch: (PartitionSelectorSearch | null)[]; + table: Table | null; partitionTables: Table[] | null; @@ -123,7 +128,7 @@ class IrisGridPartitionSelector extends Component< log.debug('handlePartitionSelect', index, partition); const newPartitions = [...prevPartitions]; - newPartitions[index] = partitionColumnValues[index][partition]; + newPartitions[index] = partitionColumnValues[index].data[partition]; this.updatePartitions(index, newPartitions).then(() => this.setState({ isShowingKeys: false }, () => { @@ -167,19 +172,13 @@ class IrisGridPartitionSelector extends Component< const { partitions } = this.state; const value = partition === undefined ? partitions[index] : partition; - log.log('displayvalue', value); - if (value == null) { + if (value == null || value === '') { return ''; } const column = columns[index]; if (TableUtils.isCharType(column.type) && value.toString().length > 0) { return String.fromCharCode(parseInt(value.toString(), 10)); } - log.log( - 'displayvalue', - value, - getFormattedString(value, column.type, column.name) - ); return getFormattedString(value, column.type, column.name); } @@ -191,11 +190,11 @@ class IrisGridPartitionSelector extends Component< if (!Array.isArray(this.partitionTables)) { return; } - const { columns, getFormattedString, table } = this.props; + const { columns, getFormattedString, tablePromise } = this.props; + const table = await tablePromise; const dataPromises = Array>(); const partitionFilters = [...this.partitionTables[index].filter]; - log.log('partitionFilter orig', this.partitionTables[index].filter); // Update partition filters for (let i = index; i < columns.length; i += 1) { @@ -226,7 +225,6 @@ class IrisGridPartitionSelector extends Component< const partitionFilterCopy = [...partitionFilters]; dataPromises.push( table.copy().then(async t => { - log.log('promisefilter', partitionFilterCopy); t.applyFilter(partitionFilterCopy); t.setViewport(0, 0, columns as Column[]); const data = await t.getViewportData(); @@ -270,26 +268,26 @@ class IrisGridPartitionSelector extends Component< async (partitionTable, colIndex) => { partitionTable.setViewport(0, partitionTable.size); const data = await partitionTable.getViewportData(); - return data.rows.reduce((columnValues, row) => { - const column = columns[colIndex]; - const value = row.get(column); - const displayValue = getFormattedString( - value, - column.type, - column.name - ); - const nextValues = { ...columnValues }; - nextValues[displayValue] = value; - return nextValues; - }, {} as Items); + return data.rows.reduce( + (columnValues, row) => { + const column = columns[colIndex]; + const value = row.get(column); + const displayValue = getFormattedString( + value, + column.type, + column.name + ); + + return { + order: [...columnValues.order, displayValue], + data: { ...columnValues.data, [displayValue]: value }, + }; + }, + { order: [], data: {} } as Items + ); } ); const newColumnValues = await Promise.all(newColumnValuesPromise); - log.log( - validPartitions, - newColumnValues, - columns.map((_, i) => this.getDisplayValue(i, validPartitions[i] ?? '')) - ); this.setState({ partitions: validPartitions, partitionColumnValues: newColumnValues, @@ -314,10 +312,12 @@ class IrisGridPartitionSelector extends Component< value={selectorValue[index]} onChange={value => this.handlePartitionSelect(index, value)} > - - {Object.keys(partitionColumnValues[index]).map(value => ( + {partitions.length > 0 || ( + + )} + {partitionColumnValues[index].order.map(value => ( diff --git a/packages/iris-grid/src/IrisGridPartitionedTableModel.ts b/packages/iris-grid/src/IrisGridPartitionedTableModel.ts index b8c3eb4795..318e64a1fb 100644 --- a/packages/iris-grid/src/IrisGridPartitionedTableModel.ts +++ b/packages/iris-grid/src/IrisGridPartitionedTableModel.ts @@ -56,8 +56,6 @@ class IrisGridPartitionedTableModel extends IrisGridModel { private partitionKeys: unknown[]; - private keyTable: Table | null = null; - /** * @param dh JSAPI instance * @param table Partitioned table to be used in the model @@ -78,10 +76,6 @@ class IrisGridPartitionedTableModel extends IrisGridModel { }); this.partitionKeys = Array.isArray(lastKey) ? lastKey : [lastKey]; this.model = new EmptyIrisGridModel(dh); - - this.keyTable = this.partitionedTable.keyTable; - const sorts = this.partitionColumns.map(column => column.sort().desc()); - this.keyTable.applySort(sorts); } get rowCount(): number { @@ -349,10 +343,7 @@ class IrisGridPartitionedTableModel extends IrisGridModel { } close(): void { - // TODO this.partitionedTable.close(); - // close model table - // close keytables, subscription table } isRowMovable(): boolean { @@ -432,13 +423,18 @@ class IrisGridPartitionedTableModel extends IrisGridModel { this.partitionKeys = partition; } - get partitionKeysTable(): Table | null { - return this.keyTable; + get partitionKeysTable(): Promise
{ + const sorts = this.partitionColumns.map(column => column.sort().desc()); + return this.partitionedTable.getKeyTable().then(table => { + table.applySort(sorts); + return table; + }); } async initializePartitionModel(): Promise { const initTable = await this.partitionedTable.getTable(this.partitionKeys); - this.model = new IrisGridTableModel(this.dh, initTable, this.irisFormatter); + const tableCopy = await initTable.copy(); + this.model = new IrisGridTableModel(this.dh, tableCopy, this.irisFormatter); return Promise.resolve(this.model); } } diff --git a/packages/iris-grid/src/IrisGridProxyModel.ts b/packages/iris-grid/src/IrisGridProxyModel.ts index 3f4eaea09d..3f2aa02375 100644 --- a/packages/iris-grid/src/IrisGridProxyModel.ts +++ b/packages/iris-grid/src/IrisGridProxyModel.ts @@ -513,9 +513,9 @@ class IrisGridProxyModel extends IrisGridModel { return; } // Singular keys are not in arrays - const tablePromise = this.originalModel.partitionedTable.getTable( - partition.length > 1 ? partition : partition[0] - ); + const tablePromise = this.originalModel.partitionedTable + .getTable(partition.length > 1 ? partition : partition[0]) + .then(table => table.copy()); this.setNextModel( tablePromise.then(table => { const newModel = makeModel(this.dh, table, this.formatter); @@ -532,7 +532,7 @@ class IrisGridProxyModel extends IrisGridModel { } } - get partitionKeysTable(): Table | null { + get partitionKeysTable(): Promise
| null { return this.originalModel.partitionKeysTable; } @@ -544,20 +544,19 @@ class IrisGridProxyModel extends IrisGridModel { } if (isIrisGridPartitionedTableModel(this.originalModel)) { this.setNextModel( - this.originalModel.partitionKeysTable - .selectDistinct(this.originalModel.partitionColumns) - .then(table => { - const newModel = makeModel(this.dh, table, this.formatter); - (this.originalModel as IrisGridPartitionedTableModel).model = - newModel; - return newModel; - }) + this.originalModel.partitionKeysTable.then(table => { + const newModel = makeModel(this.dh, table, this.formatter); + (this.originalModel as IrisGridPartitionedTableModel).model = + newModel; + return newModel; + }) ); - } else { + } else if (this.model === this.originalModel) { this.setNextModel( - this.originalModel.partitionKeysTable - .copy() - .then(table => makeModel(this.dh, table, this.formatter)) + this.originalModel.partitionKeysTable.then(table => { + log.log('openPartitionKeysTable', table); + return makeModel(this.dh, table, this.formatter); + }) ); } } diff --git a/packages/iris-grid/src/IrisGridTableModel.ts b/packages/iris-grid/src/IrisGridTableModel.ts index 77b7ca70c5..7d39952df7 100644 --- a/packages/iris-grid/src/IrisGridTableModel.ts +++ b/packages/iris-grid/src/IrisGridTableModel.ts @@ -62,6 +62,11 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { this.formatColumnList = []; } + close(): void { + super.close(); + this._partitionTable?.close(); + } + get isExportAvailable(): boolean { return this.table.freeze != null; } @@ -248,8 +253,8 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { this.filter = prevFilters; } - get partitionKeysTable(): Table | null { - return this._partitionTable; + get partitionKeysTable(): Promise
| null { + return this._partitionTable?.copy() ?? null; } set filter(filter: FilterCondition[]) { diff --git a/packages/jsapi-types/src/dh.types.ts b/packages/jsapi-types/src/dh.types.ts index 130be5e831..40d0ff1c9a 100644 --- a/packages/jsapi-types/src/dh.types.ts +++ b/packages/jsapi-types/src/dh.types.ts @@ -957,11 +957,11 @@ export interface PartitionedTable extends Evented, PartitionedTableStatic { readonly size: number; readonly columns: Column[]; readonly keyColumns: Column[]; - readonly keyTable: Table; getTable: (key: unknown) => Promise
; getMergedTable: () => Promise
; getKeys: () => Set; + getKeyTable: () => Promise
; close: () => void; } From 208dd61377c0db6a05db0f8c871151817b8748e8 Mon Sep 17 00:00:00 2001 From: georgecwan Date: Thu, 7 Dec 2023 09:52:55 -0800 Subject: [PATCH 12/33] Remove partitionColumns setter --- packages/iris-grid/src/EmptyIrisGridModel.ts | 141 +++++-- packages/iris-grid/src/IrisGrid.tsx | 5 +- packages/iris-grid/src/IrisGridModel.ts | 6 - .../src/IrisGridPartitionSelector.tsx | 8 +- .../src/IrisGridPartitionedTableModel.ts | 368 +----------------- packages/iris-grid/src/IrisGridProxyModel.ts | 47 +-- packages/iris-grid/src/IrisGridTableModel.ts | 25 +- 7 files changed, 148 insertions(+), 452 deletions(-) diff --git a/packages/iris-grid/src/EmptyIrisGridModel.ts b/packages/iris-grid/src/EmptyIrisGridModel.ts index 9decc3cdc8..e119158421 100644 --- a/packages/iris-grid/src/EmptyIrisGridModel.ts +++ b/packages/iris-grid/src/EmptyIrisGridModel.ts @@ -27,44 +27,13 @@ import { } from './CommonTypes'; class EmptyIrisGridModel extends IrisGridModel { - constructor(dh: DhType) { + constructor(dh: DhType, formatter = new Formatter(dh)) { super(dh); - this.dh = dh; - this.formatter = new Formatter(dh); + this.modelFormatter = formatter; } - dh: DhType; - - table: Table | null = null; - - columns: Column[] = []; - - filter: FilterCondition[] = []; - - partition: unknown[] = []; - - partitionColumns: Column[] = []; - - formatter: Formatter; - - sort: Sort[] = []; - - customColumns: ColumnName[] = []; - - formatColumns: CustomColumn[] = []; - - rollupConfig: RollupConfig | null = null; - - totalsConfig: UITotalsTableConfig | null = null; - - selectDistinctColumns: ColumnName[] = []; - - pendingDataMap: PendingDataMap = new Map(); - - pendingRowCount = 0; - - columnHeaderGroups: ColumnHeaderGroup[] = []; + modelFormatter: Formatter; get rowCount(): number { return 0; @@ -82,7 +51,11 @@ class EmptyIrisGridModel extends IrisGridModel { return undefined; } - getColumnIndexByName(name: string): number | undefined { + get columns(): readonly Column[] { + return []; + } + + getColumnIndexByName(name: string): ModelIndex | undefined { return undefined; } @@ -110,6 +83,34 @@ class EmptyIrisGridModel extends IrisGridModel { return undefined; } + get filter(): readonly FilterCondition[] { + return []; + } + + set filter(filter: FilterCondition[]) { + // No-op + } + + get partition(): readonly unknown[] { + return []; + } + + set partition(partition: unknown[]) { + // No-op + } + + get partitionColumns(): readonly Column[] { + return []; + } + + get formatter(): Formatter { + return this.modelFormatter; + } + + set formatter(formatter: Formatter) { + this.modelFormatter = formatter; + } + displayString( value: unknown, columnType: string, @@ -118,10 +119,46 @@ class EmptyIrisGridModel extends IrisGridModel { return ''; } + get sort(): Sort[] { + return []; + } + + set sort(sort: Sort[]) { + // No-op + } + + get customColumns(): readonly ColumnName[] { + return []; + } + + set customColumns(customColumns: readonly ColumnName[]) { + // No-op + } + + get formatColumns(): readonly CustomColumn[] { + return []; + } + updateFrozenColumns(columns: readonly ColumnName[]): void { // Do nothing } + get rollupConfig(): RollupConfig | null { + return null; + } + + set rollupConfig(rollupConfig: RollupConfig | null) { + // No-op + } + + get totalsConfig(): UITotalsTableConfig | null { + return null; + } + + set totalsConfig(totalsConfig: UITotalsTableConfig | null) { + // No-op + } + export(): Promise
{ throw new Error('Method not implemented.'); } @@ -130,6 +167,30 @@ class EmptyIrisGridModel extends IrisGridModel { throw new Error('Method not implemented.'); } + get selectDistinctColumns(): readonly ColumnName[] { + return []; + } + + set selectDistinctColumns(selectDistinctColumns: readonly ColumnName[]) { + // No-op + } + + get pendingDataMap(): PendingDataMap { + return new Map(); + } + + set pendingDataMap(map: PendingDataMap) { + // No-op + } + + get pendingRowCount(): number { + return 0; + } + + set pendingRowCount(count: number) { + // No-op + } + get pendingDataErrors(): PendingDataErrorMap { return new Map(); } @@ -143,7 +204,7 @@ class EmptyIrisGridModel extends IrisGridModel { bottom: VisibleIndex, columns?: Column[] ): void { - // Do nothing + // No-op } snapshot(ranges: readonly GridRange[]): Promise { @@ -178,6 +239,14 @@ class EmptyIrisGridModel extends IrisGridModel { return Promise.resolve(0); } + get columnHeaderGroups(): readonly ColumnHeaderGroup[] { + return []; + } + + set columnHeaderGroups(groups: readonly ColumnHeaderGroup[]) { + // No-op + } + get columnHeaderGroupMap(): ReadonlyMap { return new Map(); } diff --git a/packages/iris-grid/src/IrisGrid.tsx b/packages/iris-grid/src/IrisGrid.tsx index afb2673bcb..b344780657 100644 --- a/packages/iris-grid/src/IrisGrid.tsx +++ b/packages/iris-grid/src/IrisGrid.tsx @@ -826,11 +826,8 @@ export class IrisGrid extends Component { } componentDidMount(): void { - const { partitionColumns, model } = this.props; + const { model } = this.props; try { - model.partitionColumns = partitionColumns.length - ? partitionColumns - : model.columns.filter(c => c.isPartitionColumn); if (model.isPartitionRequired && model.partitionColumns.length) { this.setState({ isSelectingPartition: true }, () => { this.initState(); diff --git a/packages/iris-grid/src/IrisGridModel.ts b/packages/iris-grid/src/IrisGridModel.ts index 85dfde12d8..8b112783e6 100644 --- a/packages/iris-grid/src/IrisGridModel.ts +++ b/packages/iris-grid/src/IrisGridModel.ts @@ -207,12 +207,6 @@ abstract class IrisGridModel< */ abstract get partitionColumns(): readonly Column[]; - /** - * Retrieve the columns this model is partitioned on - * @returns All columns to partition on - */ - abstract set partitionColumns(columns: readonly Column[]); - /** * @returns A Table containing the keys of the columns that are partitioned on */ diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.tsx b/packages/iris-grid/src/IrisGridPartitionSelector.tsx index 30997c1908..d7845515d3 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.tsx +++ b/packages/iris-grid/src/IrisGridPartitionSelector.tsx @@ -338,19 +338,17 @@ class IrisGridPartitionSelector extends Component<
{ - return this.model.export(); - } - - get isColumnStatisticsAvailable(): boolean { - return this.model.isColumnStatisticsAvailable; - } - - get description(): string { - return this.model.description; - } - - columnStatistics(column: Column): Promise { - return this.model.columnStatistics(column); - } - - get isCustomColumnsAvailable(): boolean { - return this.model.isCustomColumnsAvailable; - } - - get isFormatColumnsAvailable(): boolean { - return this.model.isFormatColumnsAvailable; - } - - get isExportAvailable(): boolean { - return this.model.isExportAvailable; - } - - get isValuesTableAvailable(): boolean { - return this.model.isValuesTableAvailable; - } - - get isChartBuilderAvailable(): boolean { - return this.model.isChartBuilderAvailable; - } - - get isRollupAvailable(): boolean { - return this.model.isRollupAvailable; - } - - get isSelectDistinctAvailable(): boolean { - return this.model.isSelectDistinctAvailable; - } - - get isTotalsAvailable(): boolean { - return this.model.isTotalsAvailable; - } - - get selectDistinctColumns(): readonly ColumnName[] { - return this.model.selectDistinctColumns; - } - - set selectDistinctColumns(names: readonly ColumnName[]) { - this.model.selectDistinctColumns = names; - } - - get pendingDataMap(): PendingDataMap { - return this.model.pendingDataMap; - } - - set pendingDataMap(map: PendingDataMap) { - this.model.pendingDataMap = map; - } - - get pendingRowCount(): number { - return this.model.pendingRowCount; - } - - set pendingRowCount(count: number) { - this.model.pendingRowCount = count; - } - - get pendingDataErrors(): PendingDataErrorMap { - return this.model.pendingDataErrors; - } - - commitPending(): Promise { - return this.model.commitPending(); - } - - isFilterable(columnIndex: ModelIndex): boolean { - return this.model.isFilterable(columnIndex); - } - - setViewport( - top: VisibleIndex, - bottom: VisibleIndex, - columns?: Column[] - ): void { - this.model.setViewport(top, bottom, columns); - } - - snapshot(ranges: readonly GridRange[]): Promise { - return this.model.snapshot(ranges); - } - - textSnapshot( - ranges: readonly GridRange[], - includeHeaders?: boolean, - formatValue?: (value: unknown, column: Column, row?: Row) => string - ): Promise { - return this.model.textSnapshot(ranges, includeHeaders, formatValue); - } - - valuesTable(columns: Column | readonly Column[]): Promise
{ - return this.model.valuesTable(columns); + return ''; } close(): void { this.partitionedTable.close(); } - isRowMovable(): boolean { - return this.model.isRowMovable(); - } - - delete(ranges: readonly GridRange[]): Promise { - return this.model.delete(ranges); - } - - seekRow( - startRow: number, - column: Column, - valueType: ValueTypeUnion, - value: unknown, - insensitive?: boolean, - contains?: boolean, - isBackwards?: boolean - ): Promise { - return this.model.seekRow( - startRow, - column, - valueType, - value, - insensitive, - contains, - isBackwards - ); - } - - get isSeekRowAvailable(): boolean { - return this.model.isSeekRowAvailable; - } - - get columnHeaderGroups(): readonly ColumnHeaderGroup[] { - return this.model.columnHeaderGroups; - } - - set columnHeaderGroups(groups: readonly ColumnHeaderGroup[]) { - this.model.columnHeaderGroups = groups; - } - - get columnHeaderGroupMap(): ReadonlyMap { - return this.model.columnHeaderGroupMap; - } - - getColumnHeaderParentGroup( - modelIndex: ModelIndex, - depth: number - ): ColumnHeaderGroup | undefined { - return this.model.getColumnHeaderParentGroup(modelIndex, depth); - } - - dataBarOptionsForCell( - column: number, - row: number, - theme: IrisGridThemeType - ): DataBarOptions { - return this.model.dataBarOptionsForCell(column, row, theme); - } - - get partitionColumns(): Column[] { - // TODO + get partitionColumns(): readonly Column[] { return this.partitionedTable.keyColumns; } - set partitionColumns(columns: Column[]) { - // Do nothing, partition columns of PartitionedTable can't be modified - } - get partition(): unknown[] { return this.partitionKeys; } @@ -434,8 +91,9 @@ class IrisGridPartitionedTableModel extends IrisGridModel { async initializePartitionModel(): Promise { const initTable = await this.partitionedTable.getTable(this.partitionKeys); const tableCopy = await initTable.copy(); - this.model = new IrisGridTableModel(this.dh, tableCopy, this.irisFormatter); - return Promise.resolve(this.model); + return Promise.resolve( + new IrisGridTableModel(this.dh, tableCopy, this.formatter) + ); } } diff --git a/packages/iris-grid/src/IrisGridProxyModel.ts b/packages/iris-grid/src/IrisGridProxyModel.ts index 3f2aa02375..7b62b4dac3 100644 --- a/packages/iris-grid/src/IrisGridProxyModel.ts +++ b/packages/iris-grid/src/IrisGridProxyModel.ts @@ -465,10 +465,6 @@ class IrisGridProxyModel extends IrisGridModel { return this.originalModel.partitionColumns; } - set partitionColumns(columns: readonly Column[]) { - this.originalModel.partitionColumns = columns; - } - get description(): string { return this.model.description; } @@ -498,31 +494,25 @@ class IrisGridProxyModel extends IrisGridModel { } set partition(partition: readonly unknown[]) { + if (!this.originalModel.isPartitionRequired) { + throw new Error('Partitions are not available'); + } log.debug('set partition', partition); this.originalModel.partition = partition; if (isIrisGridPartitionedTableModel(this.originalModel)) { if (partition.length === 0) { this.setNextModel( - this.originalModel.partitionedTable.getMergedTable().then(table => { - const newModel = makeModel(this.dh, table, this.formatter); - (this.originalModel as IrisGridPartitionedTableModel).model = - newModel; - return newModel; - }) + this.originalModel.partitionedTable + .getMergedTable() + .then(table => makeModel(this.dh, table, this.formatter)) ); return; } - // Singular keys are not in arrays const tablePromise = this.originalModel.partitionedTable - .getTable(partition.length > 1 ? partition : partition[0]) + .getTable(partition) .then(table => table.copy()); this.setNextModel( - tablePromise.then(table => { - const newModel = makeModel(this.dh, table, this.formatter); - (this.originalModel as IrisGridPartitionedTableModel).model = - newModel; - return newModel; - }) + tablePromise.then(table => makeModel(this.dh, table, this.formatter)) ); } else if ( isIrisGridTableModelTemplate(this.originalModel) && @@ -542,21 +532,14 @@ class IrisGridProxyModel extends IrisGridModel { if (this.originalModel.partitionKeysTable === null) { return; } - if (isIrisGridPartitionedTableModel(this.originalModel)) { - this.setNextModel( - this.originalModel.partitionKeysTable.then(table => { - const newModel = makeModel(this.dh, table, this.formatter); - (this.originalModel as IrisGridPartitionedTableModel).model = - newModel; - return newModel; - }) - ); - } else if (this.model === this.originalModel) { + if ( + isIrisGridPartitionedTableModel(this.originalModel) || + this.model === this.originalModel + ) { this.setNextModel( - this.originalModel.partitionKeysTable.then(table => { - log.log('openPartitionKeysTable', table); - return makeModel(this.dh, table, this.formatter); - }) + this.originalModel.partitionKeysTable.then(table => + makeModel(this.dh, table, this.formatter) + ) ); } } diff --git a/packages/iris-grid/src/IrisGridTableModel.ts b/packages/iris-grid/src/IrisGridTableModel.ts index 7d39952df7..ab5a21d11e 100644 --- a/packages/iris-grid/src/IrisGridTableModel.ts +++ b/packages/iris-grid/src/IrisGridTableModel.ts @@ -37,6 +37,8 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { formatColumnList: CustomColumn[]; + private initialUncoalesced: boolean; + private _partition: unknown[] = []; private _partitionColumns: Column[] = []; @@ -60,6 +62,8 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { super(dh, table, formatter, inputTable); this.customColumnList = []; this.formatColumnList = []; + this.initialUncoalesced = this.table.isUncoalesced; + this.initializePartitionModel(); } close(): void { @@ -200,17 +204,10 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { ); } - get partitionColumns(): Column[] { + get partitionColumns(): readonly Column[] { return this._partitionColumns; } - set partitionColumns(partitionColumns: Column[]) { - this._partitionColumns = partitionColumns; - if (this._partitionColumns.length !== this._partition.length) { - this.initializePartition(); - } - } - get partition(): unknown[] { return this._partition; } @@ -311,11 +308,11 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { } get isFilterRequired(): boolean { - return this.table.isUncoalesced; + return this.initialUncoalesced; } get isPartitionRequired(): boolean { - return this.table.isUncoalesced && this.isValuesTableAvailable; + return this.initialUncoalesced && this.isValuesTableAvailable; } isFilterable(columnIndex: ModelIndex): boolean { @@ -468,13 +465,13 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { return this.table.seekRow != null; } - private async initializePartition(): Promise { - log.debug('Initializing partition'); - const partitionTable = await this.valuesTable(this.partitionColumns); + async initializePartitionModel(): Promise { + this._partitionColumns = this.columns.filter(c => c.isPartitionColumn); + const partitionTable = await this.valuesTable(this._partitionColumns); const columns = partitionTable.columns.slice( 0, - this.partitionColumns.length + this._partitionColumns.length ); const sorts = columns.map(column => column.sort().desc()); partitionTable.applySort(sorts); From 3131270a345ae3672cad8b1d496e0a4481197c96 Mon Sep 17 00:00:00 2001 From: georgecwan Date: Thu, 7 Dec 2023 10:24:25 -0800 Subject: [PATCH 13/33] Check partition in IrisGridTableModel constructor --- packages/iris-grid/src/EmptyIrisGridModel.ts | 4 ++-- packages/iris-grid/src/IrisGridTableModel.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/iris-grid/src/EmptyIrisGridModel.ts b/packages/iris-grid/src/EmptyIrisGridModel.ts index e119158421..0d2b1b74fc 100644 --- a/packages/iris-grid/src/EmptyIrisGridModel.ts +++ b/packages/iris-grid/src/EmptyIrisGridModel.ts @@ -87,7 +87,7 @@ class EmptyIrisGridModel extends IrisGridModel { return []; } - set filter(filter: FilterCondition[]) { + set filter(filter: readonly FilterCondition[]) { // No-op } @@ -95,7 +95,7 @@ class EmptyIrisGridModel extends IrisGridModel { return []; } - set partition(partition: unknown[]) { + set partition(partition: readonly unknown[]) { // No-op } diff --git a/packages/iris-grid/src/IrisGridTableModel.ts b/packages/iris-grid/src/IrisGridTableModel.ts index ab5a21d11e..c1fd84e025 100644 --- a/packages/iris-grid/src/IrisGridTableModel.ts +++ b/packages/iris-grid/src/IrisGridTableModel.ts @@ -63,7 +63,9 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { this.customColumnList = []; this.formatColumnList = []; this.initialUncoalesced = this.table.isUncoalesced; - this.initializePartitionModel(); + if (this.table.isUncoalesced && this.isValuesTableAvailable) { + this.initializePartitionModel(); + } } close(): void { @@ -466,7 +468,9 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { } async initializePartitionModel(): Promise { - this._partitionColumns = this.columns.filter(c => c.isPartitionColumn); + this._partitionColumns = this.table.columns.filter( + c => c.isPartitionColumn + ); const partitionTable = await this.valuesTable(this._partitionColumns); const columns = partitionTable.columns.slice( From 105dc3a9e5016e245819f1cde7e5b3cc9e7ad6d1 Mon Sep 17 00:00:00 2001 From: georgecwan Date: Mon, 11 Dec 2023 10:12:47 -0800 Subject: [PATCH 14/33] Fix styling --- .../src/IrisGridPartitionSelector.scss | 43 ++++++------------- .../src/IrisGridPartitionSelector.tsx | 43 ++++++++++--------- 2 files changed, 35 insertions(+), 51 deletions(-) diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.scss b/packages/iris-grid/src/IrisGridPartitionSelector.scss index 4854ba8fc5..1af044a9f5 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.scss +++ b/packages/iris-grid/src/IrisGridPartitionSelector.scss @@ -1,43 +1,26 @@ @import '@deephaven/components/scss/custom.scss'; -$partition-selector-color: $gray-400; .iris-grid-partition-selector { display: flex; flex-wrap: wrap; background: $gray-850; - vertical-align: middle; - padding: 0.2em 0 0.2em 0; - .table-name { - text-overflow: ellipsis; - display: flex; - flex-direction: row; - align-items: center; - padding: 0.5em; - white-space: nowrap; - } - .column-name { - text-overflow: ellipsis; - display: flex; - flex-direction: row; - align-items: center; - padding: 0.5em; - white-space: nowrap; - } + align-items: center; + padding: $spacer-2; + gap: $spacer-2; + .column-selector { display: flex; - width: auto; align-items: center; - } - .iris-grid-partition-selector-spacer { - flex-grow: 1; + gap: $spacer-2; + + .custom-select { + min-width: 4rem; + } } .partition-button-group { + display: flex; border: 1px $gray-900; - border-style: none solid none solid; - padding: 0 0.3em 0 0.3em; - .btn { - color: $white; - margin: 0.3em 0.3em; - padding: 0.25em 0.5em; - } + border-style: none solid; + padding: 0 $spacer-2; + gap: $spacer-2; } } diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.tsx b/packages/iris-grid/src/IrisGridPartitionSelector.tsx index d7845515d3..90b66da15b 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.tsx +++ b/packages/iris-grid/src/IrisGridPartitionSelector.tsx @@ -190,17 +190,21 @@ class IrisGridPartitionSelector extends Component< if (!Array.isArray(this.partitionTables)) { return; } - const { columns, getFormattedString, tablePromise } = this.props; - const table = await tablePromise; + const { columns, getFormattedString } = this.props; const dataPromises = Array>(); const partitionFilters = [...this.partitionTables[index].filter]; + log.log(columns.map(column => column.name)); + log.log(this.table); + // Update partition filters for (let i = index; i < columns.length; i += 1) { const partition = partitions[i]; const partitionColumn = columns[i]; + log.log('loop', partition, partitionColumn.name, partitionFilters); + this.partitionTables[i].applyFilter([...partitionFilters]); if ( partition != null && @@ -223,15 +227,19 @@ class IrisGridPartitionSelector extends Component< } } const partitionFilterCopy = [...partitionFilters]; - dataPromises.push( - table.copy().then(async t => { - t.applyFilter(partitionFilterCopy); - t.setViewport(0, 0, columns as Column[]); - const data = await t.getViewportData(); - t.close(); - return data as TableData; - }) - ); + if (this.table) { + dataPromises.push( + this.table.copy().then(async t => { + t.applyFilter(partitionFilterCopy); + t.setViewport(0, 0, columns as Column[]); + log.log('viewport set'); + const data = await t.getViewportData(); + log.log('promise completed', data.rows.length); + t.close(); + return data as TableData; + }) + ); + } } // Update Partition Values const tableData = await Promise.all(dataPromises); @@ -304,9 +312,7 @@ class IrisGridPartitionSelector extends Component< const partitionSelectors = columns.map((column, index) => (
-
- {column.name} -
+
{column.name}
| null { - return null; - } - - openPartitionKeysTable(): void { - throw new Error('Method not implemented.'); - } - /** * @returns The formatter used when formatting data */ @@ -341,14 +314,6 @@ abstract class IrisGridModel< return false; } - /** - * Replaces isPartitionRequired() - * @returns True if this model requires a partition to be set - */ - get isPartitionRequired(): boolean { - return false; - } - get isReversible(): boolean { return true; } diff --git a/packages/iris-grid/src/IrisGridPartitionedTableModel.ts b/packages/iris-grid/src/IrisGridPartitionedTableModel.ts index 1f5e6ac98a..5c5337a238 100644 --- a/packages/iris-grid/src/IrisGridPartitionedTableModel.ts +++ b/packages/iris-grid/src/IrisGridPartitionedTableModel.ts @@ -10,6 +10,7 @@ import { ColumnName } from './CommonTypes'; import IrisGridModel from './IrisGridModel'; import IrisGridTableModel from './IrisGridTableModel'; import EmptyIrisGridModel from './EmptyIrisGridModel'; +import { PartitionConfig, PartitionedGridModel } from './PartitionedGridModel'; export function isIrisGridPartitionedTableModel( model: IrisGridModel @@ -19,10 +20,13 @@ export function isIrisGridPartitionedTableModel( ); } -class IrisGridPartitionedTableModel extends EmptyIrisGridModel { +class IrisGridPartitionedTableModel + extends EmptyIrisGridModel + implements PartitionedGridModel +{ readonly partitionedTable: PartitionedTable; - private partitionKeys: unknown[]; + private config: PartitionConfig; /** * @param dh JSAPI instance @@ -41,7 +45,10 @@ class IrisGridPartitionedTableModel extends EmptyIrisGridModel { this.partitionedTable.getKeys().forEach(key => { lastKey = key; }); - this.partitionKeys = Array.isArray(lastKey) ? lastKey : [lastKey]; + this.config = { + partitions: Array.isArray(lastKey) ? lastKey : [lastKey], + mode: 'partition', + }; } get isPartitionRequired(): boolean { @@ -68,15 +75,15 @@ class IrisGridPartitionedTableModel extends EmptyIrisGridModel { return this.partitionedTable.keyColumns; } - get partition(): unknown[] { - return this.partitionKeys; + get partitionConfig(): PartitionConfig { + return this.config; } - set partition(partition: unknown[]) { - this.partitionKeys = partition; + set partitionConfig(rollupConfig: PartitionConfig) { + this.config = rollupConfig; } - get partitionKeysTable(): Promise
{ + partitionKeysTable(): Promise
{ const sorts = this.partitionColumns.map(column => column.sort().desc()); return this.partitionedTable.getKeyTable().then(table => { table.applySort(sorts); @@ -85,7 +92,9 @@ class IrisGridPartitionedTableModel extends EmptyIrisGridModel { } async initializePartitionModel(): Promise { - const initTable = await this.partitionedTable.getTable(this.partitionKeys); + const initTable = await this.partitionedTable.getTable( + this.config.partitions + ); const tableCopy = await initTable.copy(); return Promise.resolve( new IrisGridTableModel(this.dh, tableCopy, this.formatter) diff --git a/packages/iris-grid/src/IrisGridProxyModel.ts b/packages/iris-grid/src/IrisGridProxyModel.ts index 7b62b4dac3..a0bc316377 100644 --- a/packages/iris-grid/src/IrisGridProxyModel.ts +++ b/packages/iris-grid/src/IrisGridProxyModel.ts @@ -44,6 +44,7 @@ import { } from './CommonTypes'; import { isIrisGridTableModelTemplate } from './IrisGridTableModelTemplate'; import type ColumnHeaderGroup from './ColumnHeaderGroup'; +import { PartitionConfig, PartitionedGridModel } from './PartitionedGridModel'; const log = Log.module('IrisGridProxyModel'); @@ -66,7 +67,7 @@ function makeModel( * Model which proxies calls to other IrisGridModels. * This allows for operations that generate new tables, like rollups. */ -class IrisGridProxyModel extends IrisGridModel { +class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { /** * @param dh JSAPI instance * @param table Iris data table to be used in the model diff --git a/packages/iris-grid/src/IrisGridTableModel.ts b/packages/iris-grid/src/IrisGridTableModel.ts index c1fd84e025..4411b91e57 100644 --- a/packages/iris-grid/src/IrisGridTableModel.ts +++ b/packages/iris-grid/src/IrisGridTableModel.ts @@ -23,6 +23,7 @@ import { import IrisGridModel from './IrisGridModel'; import { ColumnName, UIRow, UITotalsTableConfig } from './CommonTypes'; import IrisGridTableModelTemplate from './IrisGridTableModelTemplate'; +import { PartitionConfig, PartitionedGridModel } from './PartitionedGridModel'; const log = Log.module('IrisGridTableModel'); @@ -30,7 +31,10 @@ const log = Log.module('IrisGridTableModel'); * Model for a grid showing an iris data table */ -class IrisGridTableModel extends IrisGridTableModelTemplate { +class IrisGridTableModel + extends IrisGridTableModelTemplate + implements PartitionedGridModel +{ userFrozenColumns?: ColumnName[]; customColumnList: string[]; @@ -39,9 +43,9 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { private initialUncoalesced: boolean; - private _partition: unknown[] = []; + private _partitionConfig: unknown[] = []; - private _partitionColumns: Column[] = []; + private _partitionColumns: Column[]; private partitionFilters: FilterCondition[] = []; @@ -63,6 +67,9 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { this.customColumnList = []; this.formatColumnList = []; this.initialUncoalesced = this.table.isUncoalesced; + this._partitionColumns = this.table.columns.filter( + c => c.isPartitionColumn + ); if (this.table.isUncoalesced && this.isValuesTableAvailable) { this.initializePartitionModel(); } @@ -210,12 +217,12 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { return this._partitionColumns; } - get partition(): unknown[] { - return this._partition; + get partitionConfig(): PartitionConfig { + return this._partitionConfig; } - set partition(partitions: unknown[]) { - log.log('setting partition', partitions); + set partitionConfig(partitionConfig: PartitionConfig) { + log.log('setting partition', partitionConfig); const partitionFilters = []; for (let i = 0; i < this.partitionColumns.length; i += 1) { @@ -252,8 +259,11 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { this.filter = prevFilters; } - get partitionKeysTable(): Promise
| null { - return this._partitionTable?.copy() ?? null; + partitionKeysTable(): Promise
{ + if (this._partitionTable === null) { + throw new Error('Partition table not initialized'); + } + return this._partitionTable?.copy(); } set filter(filter: FilterCondition[]) { @@ -468,9 +478,6 @@ class IrisGridTableModel extends IrisGridTableModelTemplate { } async initializePartitionModel(): Promise { - this._partitionColumns = this.table.columns.filter( - c => c.isPartitionColumn - ); const partitionTable = await this.valuesTable(this._partitionColumns); const columns = partitionTable.columns.slice( diff --git a/packages/iris-grid/src/IrisGridTableModelTemplate.ts b/packages/iris-grid/src/IrisGridTableModelTemplate.ts index afcba525b2..af4c6681be 100644 --- a/packages/iris-grid/src/IrisGridTableModelTemplate.ts +++ b/packages/iris-grid/src/IrisGridTableModelTemplate.ts @@ -141,22 +141,6 @@ class IrisGridTableModelTemplate< throw new Error('Method not implemented.'); } - get partitionColumns(): readonly Column[] { - throw new Error('Method not implemented.'); - } - - set partitionColumns(columns: readonly Column[]) { - throw new Error('Method not implemented.'); - } - - get partition(): readonly unknown[] { - throw new Error('Method not implemented.'); - } - - set partition(partition: readonly unknown[]) { - throw new Error('Method not implemented.'); - } - /** * Returns an array of the columns in the model * The order of model columns should never change once established diff --git a/packages/iris-grid/src/PartitionedGridModel.ts b/packages/iris-grid/src/PartitionedGridModel.ts new file mode 100644 index 0000000000..2c445b1ca8 --- /dev/null +++ b/packages/iris-grid/src/PartitionedGridModel.ts @@ -0,0 +1,34 @@ +import type { Column, Table } from '@deephaven/jsapi-types'; +import IrisGridModel from './IrisGridModel'; + +export function isPartitionedGridModel( + model: IrisGridModel +): model is PartitionedGridModel { + return (model as PartitionedGridModel)?.partitionConfig !== undefined; +} + +export interface PartitionConfig { + /** The partition values to set on the model */ + partitions: unknown[]; + + /** What data to display - the keys table, the merged table, or the selected partition */ + mode: 'keys' | 'merged' | 'partition'; +} + +export interface PartitionedGridModel extends IrisGridModel { + get partitionConfig(): PartitionConfig; + + set partitionConfig(partitionConfig: PartitionConfig); + + /** + * Retrieve the columns this model is partitioned on + * @returns All columns to partition on + */ + get partitionColumns(): readonly Column[]; + + /** Whether a partition is required to be set on this model */ + get isPartitionRequired(): boolean; + + /** Get a keys table for the partitions */ + partitionKeysTable: () => Promise
; +} From 98f9ba33dc5dd686c98711bfe25d640f0b9be185 Mon Sep 17 00:00:00 2001 From: georgecwan Date: Mon, 18 Dec 2023 13:11:47 -0800 Subject: [PATCH 18/33] Add PartitionedGridModel --- .../src/panels/IrisGridPanel.tsx | 45 +++--- packages/iris-grid/src/IrisGrid.tsx | 55 +++++-- .../iris-grid/src/IrisGridModelUpdater.tsx | 14 ++ .../src/IrisGridPartitionSelector.tsx | 145 ++++++++++++------ packages/iris-grid/src/IrisGridProxyModel.ts | 71 +++++---- packages/iris-grid/src/IrisGridTableModel.ts | 85 ++++++---- packages/iris-grid/src/IrisGridUtils.ts | 64 ++------ .../iris-grid/src/PartitionedGridModel.ts | 2 +- packages/iris-grid/src/index.ts | 1 + 9 files changed, 287 insertions(+), 195 deletions(-) diff --git a/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx b/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx index 1f6d935d00..9805027072 100644 --- a/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx +++ b/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx @@ -43,6 +43,9 @@ import { ColumnHeaderGroup, IrisGridContextMenuData, IrisGridTableModel, + PartitionConfig, + isPartitionedGridModel, + PartitionedGridModel, } from '@deephaven/iris-grid'; import { AdvancedFilterOptions, @@ -126,9 +129,7 @@ export interface PanelState { type LoadedPanelState = PanelState & { irisGridPanelState: PanelState['irisGridPanelState'] & { partitions?: (string | null)[]; - partitionColumns?: ColumnName[]; partition?: string | null; - partitionColumn?: ColumnName | null; }; }; @@ -190,7 +191,7 @@ interface IrisGridPanelState { movedRows: readonly MoveOperation[]; isSelectingPartition: boolean; partitions: (string | null)[]; - partitionColumns: Column[]; + partitionConfig?: PartitionConfig; rollupConfig?: UIRollupConfig; showSearchBar: boolean; searchValue: string; @@ -296,7 +297,6 @@ export class IrisGridPanel extends PureComponent< movedRows: [], isSelectingPartition: false, partitions: [], - partitionColumns: [], rollupConfig: undefined, showSearchBar: false, searchValue: '', @@ -466,13 +466,11 @@ export class IrisGridPanel extends PureComponent< model: IrisGridModel, isSelectingPartition: boolean, partitions: (string | null)[], - partitionColumns: Column[], advancedSettings: Map ) => IrisGridUtils.dehydrateIrisGridPanelState(model, { isSelectingPartition, partitions, - partitionColumns, advancedSettings, }) ); @@ -499,7 +497,8 @@ export class IrisGridPanel extends PureComponent< pendingDataMap: PendingDataMap, frozenColumns: readonly ColumnName[], conditionalFormats: readonly SidebarFormattingRule[], - columnHeaderGroups: readonly ColumnHeaderGroup[] + columnHeaderGroups: readonly ColumnHeaderGroup[], + partitionConfig: PartitionConfig | undefined ) => { assertNotNull(this.irisGridUtils); return this.irisGridUtils.dehydrateIrisGridState(model, { @@ -525,6 +524,7 @@ export class IrisGridPanel extends PureComponent< frozenColumns, conditionalFormats, columnHeaderGroups, + partitionConfig, }); } ); @@ -583,6 +583,7 @@ export class IrisGridPanel extends PureComponent< customColumns, selectDistinctColumns = [], rollupConfig, + partitionConfig, } = { ...irisGridState, ...irisGridStateOverrides }; if (customColumns.length > 0) { @@ -612,6 +613,13 @@ export class IrisGridPanel extends PureComponent< m.selectDistinctColumns = selectDistinctColumns; }); } + + if (partitionConfig != null && isPartitionedGridModel(model)) { + modelQueue.push(m => { + // eslint-disable-next-line no-param-reassign + (m as PartitionedGridModel).partitionConfig = partitionConfig; + }); + } } this.setState({ model, modelQueue }); @@ -1035,12 +1043,8 @@ export class IrisGridPanel extends PureComponent< }[] ); } - const { - isSelectingPartition, - partitions, - partitionColumns, - advancedSettings, - } = IrisGridUtils.hydrateIrisGridPanelState(model, irisGridPanelState); + const { isSelectingPartition, partitions, advancedSettings } = + IrisGridUtils.hydrateIrisGridPanelState(model, irisGridPanelState); assertNotNull(this.irisGridUtils); const { advancedFilters, @@ -1063,6 +1067,7 @@ export class IrisGridPanel extends PureComponent< frozenColumns, conditionalFormats, columnHeaderGroups, + partitionConfig, } = this.irisGridUtils.hydrateIrisGridState(model, { ...irisGridState, ...irisGridStateOverrides, @@ -1084,7 +1089,6 @@ export class IrisGridPanel extends PureComponent< movedColumns, movedRows, partitions, - partitionColumns, quickFilters, reverseType, rollupConfig, @@ -1102,6 +1106,7 @@ export class IrisGridPanel extends PureComponent< isStuckToBottom, isStuckToRight, columnHeaderGroups, + partitionConfig, }); } catch (error) { log.error('loadPanelState failed to load panelState', panelState, error); @@ -1117,7 +1122,6 @@ export class IrisGridPanel extends PureComponent< panelState: oldPanelState, isSelectingPartition, partitions, - partitionColumns, advancedSettings, } = this.state; const { @@ -1140,6 +1144,7 @@ export class IrisGridPanel extends PureComponent< frozenColumns, conditionalFormats, columnHeaderGroups, + partitionConfig, } = irisGridState; assertNotNull(model); assertNotNull(metrics); @@ -1153,7 +1158,6 @@ export class IrisGridPanel extends PureComponent< model, isSelectingPartition, partitions, - partitionColumns, advancedSettings ), this.getDehydratedIrisGridState( @@ -1177,7 +1181,8 @@ export class IrisGridPanel extends PureComponent< pendingDataMap, frozenColumns, conditionalFormats, - columnHeaderGroups + columnHeaderGroups, + partitionConfig ), this.getDehydratedGridState( model, @@ -1240,7 +1245,8 @@ export class IrisGridPanel extends PureComponent< model, movedColumns, movedRows, - partitionColumns, + partitions, + partitionConfig, quickFilters, reverseType, rollupConfig, @@ -1320,7 +1326,8 @@ export class IrisGridPanel extends PureComponent< isStuckToRight={isStuckToRight} movedColumns={movedColumns} movedRows={movedRows} - partitionColumns={partitionColumns} + partitions={partitions} + partitionConfig={partitionConfig} quickFilters={quickFilters} reverseType={reverseType} rollupConfig={rollupConfig} diff --git a/packages/iris-grid/src/IrisGrid.tsx b/packages/iris-grid/src/IrisGrid.tsx index 308e42a97d..44d0e8689f 100644 --- a/packages/iris-grid/src/IrisGrid.tsx +++ b/packages/iris-grid/src/IrisGrid.tsx @@ -151,7 +151,10 @@ import { import IrisGridUtils from './IrisGridUtils'; import CrossColumnSearch from './CrossColumnSearch'; import IrisGridModel from './IrisGridModel'; -import { isPartitionedGridModel } from './PartitionedGridModel'; +import { + isPartitionedGridModel, + PartitionConfig, +} from './PartitionedGridModel'; import IrisGridPartitionSelector from './IrisGridPartitionSelector'; import SelectDistinctBuilder from './sidebar/SelectDistinctBuilder'; import AdvancedSettingsType from './sidebar/AdvancedSettingsType'; @@ -286,7 +289,8 @@ export interface IrisGridProps { onDataSelected: (index: ModelIndex, map: Record) => void; onStateChange: (irisGridState: IrisGridState, gridState: GridState) => void; onAdvancedSettingsChange: AdvancedSettingsMenuCallback; - partitionColumns: Column[]; + partitions: (string | null)[]; + partitionConfig?: PartitionConfig; sorts: readonly Sort[]; reverseType: ReverseType; quickFilters: ReadonlyQuickFilterMap | null; @@ -347,6 +351,8 @@ export interface IrisGridState { keyHandlers: readonly KeyHandler[]; mouseHandlers: readonly GridMouseHandler[]; + partitionConfig?: PartitionConfig; + // setAdvancedFilter and setQuickFilter mutate the arguments // so we want to always use map copies from the state instead of props quickFilters: ReadonlyQuickFilterMap; @@ -461,7 +467,8 @@ export class IrisGrid extends Component { onError: (): void => undefined, onStateChange: (): void => undefined, onAdvancedSettingsChange: (): void => undefined, - partitionColumns: EMPTY_ARRAY, + partitions: undefined, + partitionConfig: undefined, quickFilters: EMPTY_MAP, selectDistinctColumns: EMPTY_ARRAY, sorts: EMPTY_ARRAY, @@ -667,6 +674,7 @@ export class IrisGrid extends Component { customColumnFormatMap, isFilterBarShown, isSelectingPartition, + partitionConfig, model, movedColumns: movedColumnsProp, movedRows: movedRowsProp, @@ -745,6 +753,8 @@ export class IrisGrid extends Component { keyHandlers, mouseHandlers, + partitionConfig, + // setAdvancedFilter and setQuickFilter mutate the arguments // so we want to always use map copies from the state instead of props quickFilters: quickFilters ? new Map(quickFilters) : new Map(), @@ -830,9 +840,15 @@ export class IrisGrid extends Component { } componentDidMount(): void { - const { model } = this.props; + const { model, partitions, partitionConfig } = this.props; try { - if (model.isPartitionRequired && model.partitionColumns.length) { + if (isPartitionedGridModel(model) && model.isPartitionRequired) { + log.log('Partition mounting', partitions, partitionConfig); + if (partitionConfig) { + model.partitionConfig = partitionConfig; + } else if (partitions.length) { + model.partitionConfig = { partitions, mode: 'partition' }; + } this.setState({ isSelectingPartition: true }, () => { this.initState(); }); @@ -2314,19 +2330,26 @@ export class IrisGrid extends Component { this.isAnimating = false; } - handlePartitionChange(partitions: readonly unknown[]): void { - const { model } = this.props; - model.partition = partitions; + handlePartitionChange(partitionConfig: PartitionConfig): void { + this.setState({ partitionConfig }); } handlePartitionMerge(): void { - const { model } = this.props; - model.partition = []; + this.setState(prevState => ({ + partitionConfig: { + partitions: prevState.partitionConfig?.partitions ?? [], + mode: 'merged', + }, + })); } handlePartitionKeyTable(): void { - const { model } = this.props; - model.openPartitionKeysTable(); + this.setState(prevState => ({ + partitionConfig: { + partitions: prevState.partitionConfig?.partitions ?? [], + mode: 'keys', + }, + })); } handleTableLoadError(error: unknown): void { @@ -3935,6 +3958,7 @@ export class IrisGrid extends Component { gotoValueSelectedColumnName, gotoValue, gotoValueSelectedFilter, + partitionConfig, } = this.state; if (!isReady) { return null; @@ -4402,17 +4426,17 @@ export class IrisGrid extends Component { unmountOnExit >
- {model.partitionKeysTable && ( + {isPartitionedGridModel(model) && model.isPartitionRequired && ( model.displayString(value, type, columnName)} columns={model.partitionColumns} - partitions={model.partition} + partitionConfig={partitionConfig ?? model.partitionConfig} onChange={this.handlePartitionChange} onMerge={this.handlePartitionMerge} onKeyTable={this.handlePartitionKeyTable} @@ -4517,6 +4541,7 @@ export class IrisGrid extends Component { pendingDataMap={pendingDataMap} frozenColumns={frozenColumns} columnHeaderGroups={columnHeaderGroups} + partitionConfig={partitionConfig} /> )} {!isMenuShown && ( diff --git a/packages/iris-grid/src/IrisGridModelUpdater.tsx b/packages/iris-grid/src/IrisGridModelUpdater.tsx index 96b36473f8..8c02d6be6f 100644 --- a/packages/iris-grid/src/IrisGridModelUpdater.tsx +++ b/packages/iris-grid/src/IrisGridModelUpdater.tsx @@ -15,6 +15,10 @@ import IrisGridUtils from './IrisGridUtils'; import { ColumnName, UITotalsTableConfig, PendingDataMap } from './CommonTypes'; import IrisGridModel from './IrisGridModel'; import type ColumnHeaderGroup from './ColumnHeaderGroup'; +import { + PartitionConfig, + isPartitionedGridModel, +} from './PartitionedGridModel'; const COLUMN_BUFFER_PAGES = 1; @@ -41,6 +45,7 @@ interface IrisGridModelUpdaterProps { selectDistinctColumns?: readonly ColumnName[]; pendingRowCount?: number; pendingDataMap?: PendingDataMap; + partitionConfig?: PartitionConfig; } /** @@ -70,6 +75,7 @@ const IrisGridModelUpdater = React.memo( frozenColumns, formatColumns, columnHeaderGroups, + partitionConfig, }: IrisGridModelUpdaterProps) => { const columns = useMemo( () => @@ -186,6 +192,14 @@ const IrisGridModelUpdater = React.memo( }, [model, columnHeaderGroups] ); + useEffect( + function updatePartitionConfig() { + if (partitionConfig && isPartitionedGridModel(model)) { + model.partitionConfig = partitionConfig; + } + }, + [model, partitionConfig] + ); return null; } diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.tsx b/packages/iris-grid/src/IrisGridPartitionSelector.tsx index 0e2baf161f..aba9d9c1b7 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.tsx +++ b/packages/iris-grid/src/IrisGridPartitionSelector.tsx @@ -8,6 +8,7 @@ import type { Column, dh as DhType, Table } from '@deephaven/jsapi-types'; import { TableUtils } from '@deephaven/jsapi-utils'; import deepEqual from 'deep-equal'; import './IrisGridPartitionSelector.scss'; +import { PartitionConfig } from './PartitionedGridModel'; const log = Log.module('IrisGridPartitionSelector'); @@ -25,14 +26,13 @@ interface IrisGridPartitionSelectorProps { getFormattedString: (value: unknown, type: string, name: string) => string; tablePromise: Promise
; columns: readonly Column[]; - partitions: readonly unknown[]; + partitionConfig: PartitionConfig; onMerge: () => void; - onChange: (partitions: readonly unknown[]) => void; + onChange: (partitionConfig: PartitionConfig) => void; onKeyTable: () => void; } interface IrisGridPartitionSelectorState { - isShowingKeys: boolean; - partitions: readonly unknown[]; + partitionConfig: PartitionConfig; partitionColumnValues: readonly Items[]; selectorValue: readonly string[]; } @@ -54,37 +54,36 @@ class IrisGridPartitionSelector extends Component< this.handleMergeClick = this.handleMergeClick.bind(this); this.handlePartitionSelect = this.handlePartitionSelect.bind(this); - const { dh, columns, partitions } = props; + const { dh, columns, partitionConfig } = props; this.tableUtils = new TableUtils(dh); this.table = null; this.partitionTables = null; this.state = { - isShowingKeys: false, selectorValue: columns.map(() => ''), - partitions, + partitionConfig, partitionColumnValues: columns.map(() => ({ order: [], data: {} })), }; } async componentDidMount(): Promise { - const { columns, tablePromise } = this.props; - const { partitions } = this.state; + const { columns, partitionConfig, tablePromise } = this.props; const table = await tablePromise; this.table = table; this.partitionTables = await Promise.all( columns.map(async (_, i) => table.selectDistinct(columns.slice(0, i + 1))) ); - this.updatePartitions(partitions); + this.updateConfig(partitionConfig); } componentDidUpdate(prevProps: IrisGridPartitionSelectorProps): void { - const { partitions: prevPartitions } = prevProps; - const { partitions: newPartitions } = this.props; + const { partitionConfig: prevConfig } = prevProps; - if (!deepEqual(prevPartitions, newPartitions)) { - this.updatePartitions(newPartitions); + const { partitionConfig } = this.props; + + if (!deepEqual(prevConfig, partitionConfig)) { + this.updateConfig(partitionConfig); } } @@ -103,36 +102,38 @@ class IrisGridPartitionSelector extends Component< handleKeyTableClick(): void { log.debug2('handleKeyTableClick'); - this.setState({ isShowingKeys: true }); + this.setState(prevState => ({ + partitionConfig: { ...prevState.partitionConfig, mode: 'keys' }, + })); this.sendKeyTable(); } handleMergeClick(): void { log.debug2('handleMergeClick'); - this.setState({ isShowingKeys: false }); + this.setState(prevState => ({ + partitionConfig: { ...prevState.partitionConfig, mode: 'merged' }, + })); this.sendMerge(); } handlePartitionSelect(index: number, partition: string): void { const { columns } = this.props; - const { partitions: prevPartitions, partitionColumnValues } = this.state; + const { partitionConfig: prevConfig, partitionColumnValues } = this.state; - if (prevPartitions[index] === partition) { - return; - } - log.debug('handlePartitionSelect', index, partition, prevPartitions); + log.debug('handlePartitionSelect', index, partition, prevConfig); - const newPartitions = prevPartitions.length - ? [...prevPartitions] - : Array(columns.length).fill(null); + const newPartitions = + prevConfig.partitions.length === columns.length + ? [...prevConfig.partitions] + : Array(columns.length).fill(null); newPartitions[index] = partitionColumnValues[index].data[partition]; + const newConfig: PartitionConfig = { + partitions: newPartitions, + mode: 'partition', + }; - this.updatePartitions(newPartitions, index).then(() => - this.setState({ isShowingKeys: false }, () => { - this.sendUpdate(); - }) - ); + this.updateConfig(newConfig, index, true); } debounceUpdate = debounce((): void => { @@ -143,8 +144,8 @@ class IrisGridPartitionSelector extends Component< log.debug2('sendUpdate'); const { onChange } = this.props; - const { partitions } = this.state; - onChange(partitions); + const { partitionConfig } = this.state; + onChange(partitionConfig); } sendMerge(): void { @@ -165,11 +166,13 @@ class IrisGridPartitionSelector extends Component< onKeyTable(); } + /** Calls getFormattedString with a special character case */ getDisplayValue(index: number, partition?: unknown): string { const { columns, getFormattedString } = this.props; - const { partitions } = this.state; + const { partitionConfig } = this.state; - const value = partition === undefined ? partitions[index] : partition; + const value = + partition === undefined ? partitionConfig.partitions[index] : partition; if (value == null || value === '') { return ''; } @@ -185,15 +188,21 @@ class IrisGridPartitionSelector extends Component< * @param index The index of the partition that was changed * @param partitions Array of partitions containing updated values */ - async updatePartitions( - partitions: readonly unknown[], - index = 0 + async updateConfig( + partitionConfig: PartitionConfig, + index = 0, + updateIrisGrid = false ): Promise { - log.debug('partitionSelector update', index, partitions); + if (partitionConfig.mode !== 'partition') { + this.clearDropdowns(partitionConfig); + return; + } if (!Array.isArray(this.partitionTables)) { return; } + log.debug('partitionSelector update', index, partitionConfig); const { getFormattedString } = this.props; + // Cannot use columns from props since different index number will cause filters to fail const { columns } = this.partitionTables[this.partitionTables.length - 1]; if (!this.table) { // this.table should be assigned in componentDidMount before updatePartitions is called @@ -201,11 +210,11 @@ class IrisGridPartitionSelector extends Component< } const partitionFilters = [...this.partitionTables[index].filter]; - const validPartitions = [...partitions]; + const validPartitions = [...partitionConfig.partitions]; let lastValidPartition = null; // Update partition filters - for (let i = index; i < partitions.length; i += 1) { + for (let i = index; i < partitionConfig.partitions.length; i += 1) { /* eslint-disable no-await-in-loop */ const partition = validPartitions[i]; const partitionColumn = columns[i]; @@ -272,18 +281,58 @@ class IrisGridPartitionSelector extends Component< } ); const newColumnValues = await Promise.all(newColumnValuesPromise); + this.setState( + { + partitionConfig: { partitions: validPartitions, mode: 'partition' }, + partitionColumnValues: newColumnValues, + selectorValue: columns.map((_, i) => + this.getDisplayValue(i, validPartitions[i] ?? '') + ), + }, + updateIrisGrid ? this.sendUpdate : undefined + ); + } + + async clearDropdowns(partitionConfig: PartitionConfig): Promise { + if (!Array.isArray(this.partitionTables)) { + return; + } + log.debug('partitionSelector clearDropdowns', partitionConfig); + const { columns, getFormattedString } = this.props; + this.partitionTables.forEach(table => table.applyFilter([])); + const newColumnValues = Array(columns.length).fill({ + order: [], + data: {}, + } as Items); + this.partitionTables[0].setViewport(0, this.partitionTables[0].size); + const data = await this.partitionTables[0].getViewportData(); + const column = this.partitionTables[0].columns[0]; + newColumnValues[0] = data.rows.reduce( + (columnValues, row) => { + const value = row.get(column); + const displayValue = getFormattedString( + value, + column.type, + column.name + ); + + return { + order: [...columnValues.order, displayValue], + data: { ...columnValues.data, [displayValue]: value }, + }; + }, + { order: [], data: {} } as Items + ); this.setState({ - partitions: validPartitions, + partitionConfig, partitionColumnValues: newColumnValues, - selectorValue: columns.map((_, i) => - this.getDisplayValue(i, validPartitions[i] ?? '') - ), + selectorValue: Array(columns.length).fill(''), }); } render(): JSX.Element { const { columns } = this.props; - const { isShowingKeys, selectorValue, partitions, partitionColumnValues } = + const { selectorValue, partitionConfig, partitionColumnValues } = this.state; const partitionSelectors = columns.map((column, index) => ( @@ -293,9 +342,9 @@ class IrisGridPartitionSelector extends Component< className="custom-select-sm" value={selectorValue[index]} onChange={value => this.handlePartitionSelect(index, value)} - disabled={index > 0 && partitions.length === 0} + disabled={index > 0 && partitionConfig.mode !== 'partition'} > - {partitions.length > 0 || ( + {partitionConfig.mode === 'partition' || ( @@ -324,7 +373,7 @@ class IrisGridPartitionSelector extends Component< kind="inline" tooltip="View keys as table" icon={vsKey} - active={isShowingKeys} + active={partitionConfig.mode === 'keys'} > Keys @@ -334,7 +383,7 @@ class IrisGridPartitionSelector extends Component< kind="inline" tooltip="View all partitions as one merged table" icon={} - active={!isShowingKeys && partitions.length === 0} + active={partitionConfig.mode === 'merged'} > Merge diff --git a/packages/iris-grid/src/IrisGridProxyModel.ts b/packages/iris-grid/src/IrisGridProxyModel.ts index a0bc316377..1d96cce3ca 100644 --- a/packages/iris-grid/src/IrisGridProxyModel.ts +++ b/packages/iris-grid/src/IrisGridProxyModel.ts @@ -44,7 +44,11 @@ import { } from './CommonTypes'; import { isIrisGridTableModelTemplate } from './IrisGridTableModelTemplate'; import type ColumnHeaderGroup from './ColumnHeaderGroup'; -import { PartitionConfig, PartitionedGridModel } from './PartitionedGridModel'; +import { + PartitionConfig, + PartitionedGridModel, + isPartitionedGridModel, +} from './PartitionedGridModel'; const log = Log.module('IrisGridProxyModel'); @@ -463,6 +467,9 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { } get partitionColumns(): readonly Column[] { + if (!isPartitionedGridModel(this.originalModel)) { + throw new Error('Partitions are not available'); + } return this.originalModel.partitionColumns; } @@ -490,18 +497,33 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { this.model.filter = filter; } - get partition(): readonly unknown[] { - return this.originalModel.partition; + get partitionConfig(): PartitionConfig { + if ( + !isPartitionedGridModel(this.originalModel) || + !this.originalModel.isPartitionRequired + ) { + throw new Error('Partitions are not available'); + } + return this.originalModel.partitionConfig; } - set partition(partition: readonly unknown[]) { - if (!this.originalModel.isPartitionRequired) { + set partitionConfig(partitionConfig: PartitionConfig) { + if ( + !isPartitionedGridModel(this.originalModel) || + !this.originalModel.isPartitionRequired + ) { throw new Error('Partitions are not available'); } - log.debug('set partition', partition); - this.originalModel.partition = partition; - if (isIrisGridPartitionedTableModel(this.originalModel)) { - if (partition.length === 0) { + log.debug('set partitionConfig', partitionConfig); + this.originalModel.partitionConfig = partitionConfig; + if (partitionConfig.mode === 'keys') { + this.setNextModel( + this.originalModel + .partitionKeysTable() + .then(table => makeModel(this.dh, table, this.formatter)) + ); + } else if (isIrisGridPartitionedTableModel(this.originalModel)) { + if (partitionConfig.mode === 'merged') { this.setNextModel( this.originalModel.partitionedTable .getMergedTable() @@ -510,7 +532,7 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { return; } const tablePromise = this.originalModel.partitionedTable - .getTable(partition) + .getTable(partitionConfig.partitions) .then(table => table.copy()); this.setNextModel( tablePromise.then(table => makeModel(this.dh, table, this.formatter)) @@ -523,26 +545,11 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { } } - get partitionKeysTable(): Promise
| null { - return this.originalModel.partitionKeysTable; - } - - openPartitionKeysTable(): void { - log.debug('opening keysTable'); - this.originalModel.partition = []; - if (this.originalModel.partitionKeysTable === null) { - return; - } - if ( - isIrisGridPartitionedTableModel(this.originalModel) || - this.model === this.originalModel - ) { - this.setNextModel( - this.originalModel.partitionKeysTable.then(table => - makeModel(this.dh, table, this.formatter) - ) - ); + partitionKeysTable(): Promise
{ + if (!isPartitionedGridModel(this.originalModel)) { + throw new Error('Partitions are not available'); } + return this.originalModel.partitionKeysTable(); } get formatter(): Formatter { @@ -671,7 +678,9 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { } get isPartitionRequired(): boolean { - return this.originalModel.isPartitionRequired; + return isPartitionedGridModel(this.originalModel) + ? this.originalModel.isPartitionRequired + : false; } get isEditable(): boolean { @@ -807,7 +816,7 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { initializePartitionModel(): void { const { model } = this; - if (model instanceof IrisGridPartitionedTableModel) { + if (isIrisGridPartitionedTableModel(model)) { this.setNextModel(model.initializePartitionModel()); } } diff --git a/packages/iris-grid/src/IrisGridTableModel.ts b/packages/iris-grid/src/IrisGridTableModel.ts index 4411b91e57..04c37382a6 100644 --- a/packages/iris-grid/src/IrisGridTableModel.ts +++ b/packages/iris-grid/src/IrisGridTableModel.ts @@ -43,13 +43,15 @@ class IrisGridTableModel private initialUncoalesced: boolean; - private _partitionConfig: unknown[] = []; + private _partitionConfig: PartitionConfig; private _partitionColumns: Column[]; private partitionFilters: FilterCondition[] = []; - private _partitionTable: Table | null = null; + private partitionTablePromise?: Promise
; + + private _partitionTable?: Table; /** * @param dh JSAPI instance @@ -70,8 +72,12 @@ class IrisGridTableModel this._partitionColumns = this.table.columns.filter( c => c.isPartitionColumn ); + this._partitionConfig = { + partitions: [], + mode: 'partition', + }; if (this.table.isUncoalesced && this.isValuesTableAvailable) { - this.initializePartitionModel(); + this.partitionTablePromise = this.initializePartitionModel(); } } @@ -223,29 +229,32 @@ class IrisGridTableModel set partitionConfig(partitionConfig: PartitionConfig) { log.log('setting partition', partitionConfig); + const { partitions, mode } = partitionConfig; const partitionFilters = []; - for (let i = 0; i < this.partitionColumns.length; i += 1) { - const partition = partitions[i]; - const partitionColumn = this.partitionColumns[i]; - - if ( - partition != null && - !(TableUtils.isCharType(partitionColumn.type) && partition === '') - ) { - const partitionText = TableUtils.isCharType(partitionColumn.type) - ? this.displayString( - partition, - partitionColumn.type, - partitionColumn.name - ) - : partition.toString(); - const partitionFilter = this.tableUtils.makeQuickFilterFromComponent( - partitionColumn, - partitionText - ); - if (partitionFilter !== null) { - partitionFilters.push(partitionFilter); + if (mode === 'partition') { + for (let i = 0; i < this.partitionColumns.length; i += 1) { + const partition = partitions[i]; + const partitionColumn = this.partitionColumns[i]; + + if ( + partition != null && + !(TableUtils.isCharType(partitionColumn.type) && partition === '') + ) { + const partitionText = TableUtils.isCharType(partitionColumn.type) + ? this.displayString( + partition, + partitionColumn.type, + partitionColumn.name + ) + : partition.toString(); + const partitionFilter = this.tableUtils.makeQuickFilterFromComponent( + partitionColumn, + partitionText + ); + if (partitionFilter !== null) { + partitionFilters.push(partitionFilter); + } } } } @@ -254,16 +263,19 @@ class IrisGridTableModel 0, super.filter.length - this.partitionFilters.length ); - this._partition = partitions; + this._partitionConfig = partitionConfig; this.partitionFilters = partitionFilters; this.filter = prevFilters; } partitionKeysTable(): Promise
{ - if (this._partitionTable === null) { - throw new Error('Partition table not initialized'); + if (this._partitionTable !== undefined) { + return this._partitionTable.copy(); } - return this._partitionTable?.copy(); + if (this.partitionTablePromise !== undefined) { + return this.partitionTablePromise.then(table => table.copy()); + } + throw new Error('Partition table not initialized'); } set filter(filter: FilterCondition[]) { @@ -477,7 +489,7 @@ class IrisGridTableModel return this.table.seekRow != null; } - async initializePartitionModel(): Promise { + private async initializePartitionModel(): Promise
{ const partitionTable = await this.valuesTable(this._partitionColumns); const columns = partitionTable.columns.slice( @@ -489,16 +501,21 @@ class IrisGridTableModel partitionTable.setViewport(0, 0, columns); const data = await partitionTable.getViewportData(); - if (data.rows.length > 0) { + if ( + data.rows.length > 0 && + this._partitionConfig.partitions.length === 0 && + this._partitionConfig.mode === 'partition' + ) { const row = data.rows[0]; const values = columns.map(column => row.get(column)); - this.partition = values; - } else { - log.info('Table does not have any data'); - this.partition = []; + this.partitionConfig = { + partitions: values, + mode: 'partition', + }; } this._partitionTable = partitionTable; + return partitionTable; } } diff --git a/packages/iris-grid/src/IrisGridUtils.ts b/packages/iris-grid/src/IrisGridUtils.ts index 276dff5b95..bbe4aa5389 100644 --- a/packages/iris-grid/src/IrisGridUtils.ts +++ b/packages/iris-grid/src/IrisGridUtils.ts @@ -49,6 +49,7 @@ import IrisGridModel from './IrisGridModel'; import type AdvancedSettingsType from './sidebar/AdvancedSettingsType'; import AdvancedSettings from './sidebar/AdvancedSettings'; import ColumnHeaderGroup from './ColumnHeaderGroup'; +import { PartitionConfig } from './PartitionedGridModel'; const log = Log.module('IrisGridUtils'); @@ -72,6 +73,7 @@ type HydratedIrisGridState = Pick< | 'frozenColumns' | 'conditionalFormats' | 'columnHeaderGroups' + | 'partitionConfig' > & { metrics: Pick; }; @@ -141,19 +143,18 @@ export interface DehydratedIrisGridState { pendingDataMap: DehydratedPendingDataMap; frozenColumns: readonly ColumnName[]; columnHeaderGroups?: readonly ColumnGroup[]; + partitionConfig?: PartitionConfig; } 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][]; } @@ -164,17 +165,13 @@ export type DehydratedIrisGridPanelState = export function isPanelStateV1( state: DehydratedIrisGridPanelState ): state is DehydratedIrisGridPanelStateV1 { - return ( - (state as DehydratedIrisGridPanelStateV1).partitionColumn !== undefined - ); + return (state as DehydratedIrisGridPanelStateV1).partition !== undefined; } export function isPanelStateV2( state: DehydratedIrisGridPanelState ): state is DehydratedIrisGridPanelStateV2 { - return Array.isArray( - (state as DehydratedIrisGridPanelStateV2).partitionColumns - ); + return Array.isArray((state as DehydratedIrisGridPanelStateV2).partitions); } /** @@ -310,24 +307,16 @@ class IrisGridUtils { // This needs to be changed after IrisGridPanel is done isSelectingPartition: boolean; partitions: (string | null)[]; - partitionColumns: Column[]; advancedSettings: Map; } ): DehydratedIrisGridPanelState { - const { - isSelectingPartition, - partitions, - partitionColumns, - advancedSettings, - } = irisGridPanelState; + const { isSelectingPartition, partitions, advancedSettings } = + irisGridPanelState; // Return value will be serialized, should not contain undefined return { isSelectingPartition, partitions, - partitionColumns: partitionColumns.map( - partitionColumn => partitionColumn.name - ), advancedSettings: [...advancedSettings], }; } @@ -344,32 +333,17 @@ class IrisGridUtils { ): { isSelectingPartition: boolean; partitions: (string | null)[]; - partitionColumns: Column[]; advancedSettings: Map; } { const { isSelectingPartition, advancedSettings } = irisGridPanelState; - const { partitionColumns, partitions } = isPanelStateV2(irisGridPanelState) - ? irisGridPanelState - : { - partitionColumns: - irisGridPanelState.partitionColumn != null - ? [irisGridPanelState.partitionColumn] - : [], - partitions: [irisGridPanelState.partition], - }; + const partitions = isPanelStateV2(irisGridPanelState) + ? irisGridPanelState.partitions + : [irisGridPanelState.partition]; - const { columns } = model; return { isSelectingPartition, 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, @@ -423,7 +397,6 @@ class IrisGridUtils { }, inputFilters: InputFilter[] = [] ): { - partitionColumns: ColumnName[]; partitions: unknown[]; advancedFilters: AF; inputFilters: InputFilter[]; @@ -431,22 +404,15 @@ class IrisGridUtils { sorts: S; } { const { irisGridPanelState, irisGridState } = panelState; - const { partitionColumns, partitions } = isPanelStateV2(irisGridPanelState) - ? irisGridPanelState - : { - partitionColumns: - irisGridPanelState.partitionColumn !== null - ? [irisGridPanelState.partitionColumn] - : [], - partitions: [irisGridPanelState.partition], - }; + const partitions = isPanelStateV2(irisGridPanelState) + ? irisGridPanelState.partitions + : [irisGridPanelState.partition]; const { advancedFilters, quickFilters, sorts } = irisGridState; return { advancedFilters, inputFilters, partitions, - partitionColumns, quickFilters, sorts, }; @@ -1204,6 +1170,7 @@ class IrisGridUtils { pendingDataMap = EMPTY_MAP, frozenColumns, columnHeaderGroups, + partitionConfig = undefined, } = irisGridState; assertNotNull(metrics); const { userColumnWidths, userRowHeights } = metrics; @@ -1241,6 +1208,7 @@ class IrisGridUtils { children: item.children, color: item.color, })), + partitionConfig, }; } @@ -1277,6 +1245,7 @@ class IrisGridUtils { pendingDataMap = [], frozenColumns, columnHeaderGroups, + partitionConfig, } = irisGridState; const { columns, formatter } = model; @@ -1335,6 +1304,7 @@ class IrisGridUtils { model, columnHeaderGroups ?? model.layoutHints?.columnGroups ?? [] ).groups, + partitionConfig, }; } diff --git a/packages/iris-grid/src/PartitionedGridModel.ts b/packages/iris-grid/src/PartitionedGridModel.ts index 2c445b1ca8..2f7652636b 100644 --- a/packages/iris-grid/src/PartitionedGridModel.ts +++ b/packages/iris-grid/src/PartitionedGridModel.ts @@ -4,7 +4,7 @@ import IrisGridModel from './IrisGridModel'; export function isPartitionedGridModel( model: IrisGridModel ): model is PartitionedGridModel { - return (model as PartitionedGridModel)?.partitionConfig !== undefined; + return (model as PartitionedGridModel)?.isPartitionRequired !== undefined; } export interface PartitionConfig { diff --git a/packages/iris-grid/src/index.ts b/packages/iris-grid/src/index.ts index 9761b57a3d..046dc7d501 100644 --- a/packages/iris-grid/src/index.ts +++ b/packages/iris-grid/src/index.ts @@ -6,6 +6,7 @@ export * from './sidebar'; export * from './AdvancedFilterCreator'; export * from './CommonTypes'; export { default as ColumnHeaderGroup } from './ColumnHeaderGroup'; +export * from './PartitionedGridModel'; export * from './IrisGrid'; export { default as SHORTCUTS } from './IrisGridShortcuts'; export { default as IrisGridModel } from './IrisGridModel'; From e86692c6e6a289a1f82cdc85af439643dd9c73b5 Mon Sep 17 00:00:00 2001 From: georgecwan Date: Mon, 18 Dec 2023 14:29:03 -0800 Subject: [PATCH 19/33] Update unit tests --- .../src/panels/IrisGridPanel.tsx | 10 --- packages/iris-grid/src/IrisGrid.tsx | 7 +-- .../src/IrisGridPartitionSelector.test.tsx | 5 +- packages/iris-grid/src/IrisGridProxyModel.ts | 5 +- packages/iris-grid/src/IrisGridTableModel.ts | 2 +- packages/iris-grid/src/IrisGridUtils.test.ts | 61 ------------------- 6 files changed, 8 insertions(+), 82 deletions(-) diff --git a/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx b/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx index 9805027072..0f15937755 100644 --- a/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx +++ b/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx @@ -44,8 +44,6 @@ import { IrisGridContextMenuData, IrisGridTableModel, PartitionConfig, - isPartitionedGridModel, - PartitionedGridModel, } from '@deephaven/iris-grid'; import { AdvancedFilterOptions, @@ -583,7 +581,6 @@ export class IrisGridPanel extends PureComponent< customColumns, selectDistinctColumns = [], rollupConfig, - partitionConfig, } = { ...irisGridState, ...irisGridStateOverrides }; if (customColumns.length > 0) { @@ -613,13 +610,6 @@ export class IrisGridPanel extends PureComponent< m.selectDistinctColumns = selectDistinctColumns; }); } - - if (partitionConfig != null && isPartitionedGridModel(model)) { - modelQueue.push(m => { - // eslint-disable-next-line no-param-reassign - (m as PartitionedGridModel).partitionConfig = partitionConfig; - }); - } } this.setState({ model, modelQueue }); diff --git a/packages/iris-grid/src/IrisGrid.tsx b/packages/iris-grid/src/IrisGrid.tsx index cd1537bda2..4d7c7a33b2 100644 --- a/packages/iris-grid/src/IrisGrid.tsx +++ b/packages/iris-grid/src/IrisGrid.tsx @@ -841,13 +841,10 @@ export class IrisGrid extends Component { } componentDidMount(): void { - const { model, partitions, partitionConfig } = this.props; + const { model, partitions } = this.props; try { if (isPartitionedGridModel(model) && model.isPartitionRequired) { - log.log('Partition mounting', partitions, partitionConfig); - if (partitionConfig) { - model.partitionConfig = partitionConfig; - } else if (partitions.length) { + if (partitions.length) { model.partitionConfig = { partitions, mode: 'partition' }; } this.setState({ isSelectingPartition: true }, () => { diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.test.tsx b/packages/iris-grid/src/IrisGridPartitionSelector.test.tsx index b9a46e92eb..5a008580d2 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.test.tsx +++ b/packages/iris-grid/src/IrisGridPartitionSelector.test.tsx @@ -3,6 +3,7 @@ import { render, fireEvent } from '@testing-library/react'; import dh from '@deephaven/jsapi-shim'; import IrisGridPartitionSelector from './IrisGridPartitionSelector'; import IrisGridTestUtils from './IrisGridTestUtils'; +import { PartitionConfig } from './PartitionedGridModel'; function makeIrisGridPartitionSelector( table = new IrisGridTestUtils(dh).makeTable(), @@ -10,7 +11,8 @@ function makeIrisGridPartitionSelector( onChange = jest.fn(), onMerge = jest.fn(), onKeyTable = jest.fn(), - getFormattedString = jest.fn(value => `${value}`) + getFormattedString = jest.fn(value => `${value}`), + partitionConfig = { partitions: [], mode: 'merged' } ) { const tablePromise = Promise.resolve(table); return render( @@ -22,6 +24,7 @@ function makeIrisGridPartitionSelector( onChange={onChange} onMerge={onMerge} onKeyTable={onKeyTable} + partitionConfig={partitionConfig as PartitionConfig} /> ); } diff --git a/packages/iris-grid/src/IrisGridProxyModel.ts b/packages/iris-grid/src/IrisGridProxyModel.ts index 8ff33f100c..68eb67beff 100644 --- a/packages/iris-grid/src/IrisGridProxyModel.ts +++ b/packages/iris-grid/src/IrisGridProxyModel.ts @@ -543,10 +543,7 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { this.setNextModel( tablePromise.then(table => makeModel(this.dh, table, this.formatter)) ); - } else if ( - isIrisGridTableModelTemplate(this.originalModel) && - this.model !== this.originalModel - ) { + } else if (isIrisGridTableModelTemplate(this.originalModel)) { this.setNextModel(Promise.resolve(this.originalModel)); } } diff --git a/packages/iris-grid/src/IrisGridTableModel.ts b/packages/iris-grid/src/IrisGridTableModel.ts index 427b5a2ab6..94968ce696 100644 --- a/packages/iris-grid/src/IrisGridTableModel.ts +++ b/packages/iris-grid/src/IrisGridTableModel.ts @@ -228,7 +228,7 @@ class IrisGridTableModel } set partitionConfig(partitionConfig: PartitionConfig) { - log.log('setting partition', partitionConfig); + log.debug('setting partition', partitionConfig); const { partitions, mode } = partitionConfig; const partitionFilters = []; diff --git a/packages/iris-grid/src/IrisGridUtils.test.ts b/packages/iris-grid/src/IrisGridUtils.test.ts index a9507c8952..d332f1c24e 100644 --- a/packages/iris-grid/src/IrisGridUtils.test.ts +++ b/packages/iris-grid/src/IrisGridUtils.test.ts @@ -646,7 +646,6 @@ describe('dehydration methods', () => { IrisGridUtils.dehydrateIrisGridPanelState(irisGridTestUtils.makeModel(), { isSelectingPartition: false, partitions: [], - partitionColumns: [], advancedSettings: new Map(), }), ], @@ -676,55 +675,11 @@ describe('hydration methods', () => { ); 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: [], }, ], @@ -733,7 +688,6 @@ describe('hydration methods', () => { { isSelectingPartition: true, partition: 'a', - partitionColumn: 'name_0', advancedSettings: [], }, ], @@ -742,7 +696,6 @@ describe('hydration methods', () => { { isSelectingPartition: false, partitions: [], - partitionColumns: [], advancedSettings: [], }, ], @@ -751,7 +704,6 @@ describe('hydration methods', () => { { isSelectingPartition: true, partitions: [null, null], - partitionColumns: ['name_0', 'name_1'], advancedSettings: [], }, ], @@ -760,7 +712,6 @@ describe('hydration methods', () => { { isSelectingPartition: true, partitions: ['a', 'b'], - partitionColumns: ['name_0', 'name_1'], advancedSettings: [], }, ], @@ -769,7 +720,6 @@ describe('hydration methods', () => { { isSelectingPartition: true, partitions: [null, 'b', null], - partitionColumns: ['name_0', 'name_1', 'name_2'], advancedSettings: [], }, ], @@ -778,7 +728,6 @@ describe('hydration methods', () => { { isSelectingPartition: true, partitions: ['a', null, 'b'], - partitionColumns: ['name_0', 'name_1', 'name_2'], advancedSettings: [], }, ], @@ -787,18 +736,8 @@ describe('hydration methods', () => { 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(); - }); } }); }); From 820eb2eb68d5b5eeda08f8608c108ec4564641b1 Mon Sep 17 00:00:00 2001 From: georgecwan Date: Thu, 21 Dec 2023 16:16:12 -0800 Subject: [PATCH 20/33] Use model in partition selector --- packages/iris-grid/src/IrisGrid.tsx | 48 ++---- .../src/IrisGridPartitionSelector.tsx | 147 +++++++++--------- .../src/IrisGridPartitionedTableModel.ts | 6 +- 3 files changed, 87 insertions(+), 114 deletions(-) diff --git a/packages/iris-grid/src/IrisGrid.tsx b/packages/iris-grid/src/IrisGrid.tsx index 4d7c7a33b2..755b1d1d6a 100644 --- a/packages/iris-grid/src/IrisGrid.tsx +++ b/packages/iris-grid/src/IrisGrid.tsx @@ -290,7 +290,9 @@ export interface IrisGridProps { onDataSelected: (index: ModelIndex, map: Record) => void; onStateChange: (irisGridState: IrisGridState, gridState: GridState) => void; onAdvancedSettingsChange: AdvancedSettingsMenuCallback; - partitions: (string | null)[]; + + /** @deprecated use `partitionConfig` instead */ + partitions?: (string | null)[]; partitionConfig?: PartitionConfig; sorts: readonly Sort[]; reverseType: ReverseType; @@ -583,8 +585,6 @@ export class IrisGrid extends Component { this.handleDownloadCanceled = this.handleDownloadCanceled.bind(this); this.handleDownloadCompleted = this.handleDownloadCompleted.bind(this); this.handlePartitionChange = this.handlePartitionChange.bind(this); - this.handlePartitionMerge = this.handlePartitionMerge.bind(this); - this.handlePartitionKeyTable = this.handlePartitionKeyTable.bind(this); this.handleColumnVisibilityChanged = this.handleColumnVisibilityChanged.bind(this); this.handleColumnVisibilityReset = @@ -675,6 +675,7 @@ export class IrisGrid extends Component { customColumnFormatMap, isFilterBarShown, isSelectingPartition, + partitions, partitionConfig, model, movedColumns: movedColumnsProp, @@ -754,7 +755,11 @@ export class IrisGrid extends Component { keyHandlers, mouseHandlers, - partitionConfig, + partitionConfig: + partitionConfig ?? + (partitions && partitions.length + ? { partitions, mode: 'partition' } + : undefined), // setAdvancedFilter and setQuickFilter mutate the arguments // so we want to always use map copies from the state instead of props @@ -841,12 +846,9 @@ export class IrisGrid extends Component { } componentDidMount(): void { - const { model, partitions } = this.props; + const { model } = this.props; try { if (isPartitionedGridModel(model) && model.isPartitionRequired) { - if (partitions.length) { - model.partitionConfig = { partitions, mode: 'partition' }; - } this.setState({ isSelectingPartition: true }, () => { this.initState(); }); @@ -2341,27 +2343,10 @@ export class IrisGrid extends Component { } handlePartitionChange(partitionConfig: PartitionConfig): void { + this.startLoading('Partitioning...'); this.setState({ partitionConfig }); } - handlePartitionMerge(): void { - this.setState(prevState => ({ - partitionConfig: { - partitions: prevState.partitionConfig?.partitions ?? [], - mode: 'merged', - }, - })); - } - - handlePartitionKeyTable(): void { - this.setState(prevState => ({ - partitionConfig: { - partitions: prevState.partitionConfig?.partitions ?? [], - mode: 'keys', - }, - })); - } - handleTableLoadError(error: unknown): void { if (PromiseUtils.isCanceled(error)) { return; @@ -4438,18 +4423,9 @@ export class IrisGrid extends Component {
{isPartitionedGridModel(model) && model.isPartitionRequired && ( model.displayString(value, type, columnName)} - columns={model.partitionColumns} + model={model} partitionConfig={partitionConfig ?? model.partitionConfig} onChange={this.handlePartitionChange} - onMerge={this.handlePartitionMerge} - onKeyTable={this.handlePartitionKeyTable} /> )}
diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.tsx b/packages/iris-grid/src/IrisGridPartitionSelector.tsx index aba9d9c1b7..8eb4d1703a 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.tsx +++ b/packages/iris-grid/src/IrisGridPartitionSelector.tsx @@ -3,17 +3,14 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, Option, Select } from '@deephaven/components'; import { vsChevronRight, vsMerge, vsKey } from '@deephaven/icons'; import Log from '@deephaven/log'; -import debounce from 'lodash.debounce'; -import type { Column, dh as DhType, Table } from '@deephaven/jsapi-types'; +import type { Table } from '@deephaven/jsapi-types'; import { TableUtils } from '@deephaven/jsapi-utils'; -import deepEqual from 'deep-equal'; +import { Pending, PromiseUtils } from '@deephaven/utils'; import './IrisGridPartitionSelector.scss'; -import { PartitionConfig } from './PartitionedGridModel'; +import { PartitionConfig, PartitionedGridModel } from './PartitionedGridModel'; const log = Log.module('IrisGridPartitionSelector'); -const PARTITION_CHANGE_DEBOUNCE_MS = 250; - interface Items { order: string[]; data: { @@ -22,14 +19,9 @@ interface Items { } interface IrisGridPartitionSelectorProps { - dh: DhType; - getFormattedString: (value: unknown, type: string, name: string) => string; - tablePromise: Promise
; - columns: readonly Column[]; + model: PartitionedGridModel; partitionConfig: PartitionConfig; - onMerge: () => void; onChange: (partitionConfig: PartitionConfig) => void; - onKeyTable: () => void; } interface IrisGridPartitionSelectorState { partitionConfig: PartitionConfig; @@ -42,8 +34,6 @@ class IrisGridPartitionSelector extends Component< > { static defaultProps = { onChange: (): void => undefined, - onMerge: (): void => undefined, - onKeyTable: (): void => undefined, partitions: [], }; @@ -54,26 +44,50 @@ class IrisGridPartitionSelector extends Component< this.handleMergeClick = this.handleMergeClick.bind(this); this.handlePartitionSelect = this.handlePartitionSelect.bind(this); - const { dh, columns, partitionConfig } = props; - this.tableUtils = new TableUtils(dh); + const { model, partitionConfig } = props; + this.tableUtils = new TableUtils(model.dh); this.table = null; this.partitionTables = null; + this.pending = new Pending(); this.state = { - selectorValue: columns.map(() => ''), + selectorValue: model.partitionColumns.map(() => ''), partitionConfig, - partitionColumnValues: columns.map(() => ({ order: [], data: {} })), + partitionColumnValues: model.partitionColumns.map(() => ({ + order: [], + data: {}, + })), }; } async componentDidMount(): Promise { - const { columns, partitionConfig, tablePromise } = this.props; + const { model, partitionConfig } = this.props; - const table = await tablePromise; - this.table = table; - this.partitionTables = await Promise.all( - columns.map(async (_, i) => table.selectDistinct(columns.slice(0, i + 1))) + const table = await this.pending.add( + model.partitionKeysTable().then(keyTable => { + const sorts = model.partitionColumns.map(column => + column.sort().desc() + ); + keyTable.applySort(sorts); + return keyTable; + }), + t => t.close() ); + try { + this.partitionTables = await Promise.all( + model.partitionColumns.map(async (_, i) => + this.pending.add( + table.selectDistinct(model.partitionColumns.slice(0, i + 1)), + t => t.close() + ) + ) + ); + } catch (e) { + if (!PromiseUtils.isCanceled(e)) { + log.error('Unable to get partition tables', e); + } + } + this.table = table; this.updateConfig(partitionConfig); } @@ -82,17 +96,19 @@ class IrisGridPartitionSelector extends Component< const { partitionConfig } = this.props; - if (!deepEqual(prevConfig, partitionConfig)) { + if (prevConfig !== partitionConfig) { this.updateConfig(partitionConfig); } } componentWillUnmount(): void { + this.pending.cancel(); this.table?.close(); this.partitionTables?.forEach(table => table.close()); - this.debounceUpdate.cancel(); } + pending: Pending; + tableUtils: TableUtils; table: Table | null; @@ -102,31 +118,35 @@ class IrisGridPartitionSelector extends Component< handleKeyTableClick(): void { log.debug2('handleKeyTableClick'); - this.setState(prevState => ({ - partitionConfig: { ...prevState.partitionConfig, mode: 'keys' }, - })); - this.sendKeyTable(); + this.setState( + prevState => ({ + partitionConfig: { ...prevState.partitionConfig, mode: 'keys' }, + }), + this.sendUpdate + ); } handleMergeClick(): void { log.debug2('handleMergeClick'); - this.setState(prevState => ({ - partitionConfig: { ...prevState.partitionConfig, mode: 'merged' }, - })); - this.sendMerge(); + this.setState( + prevState => ({ + partitionConfig: { ...prevState.partitionConfig, mode: 'merged' }, + }), + this.sendUpdate + ); } handlePartitionSelect(index: number, partition: string): void { - const { columns } = this.props; + const { model } = this.props; const { partitionConfig: prevConfig, partitionColumnValues } = this.state; log.debug('handlePartitionSelect', index, partition, prevConfig); const newPartitions = - prevConfig.partitions.length === columns.length + prevConfig.partitions.length === model.partitionColumns.length ? [...prevConfig.partitions] - : Array(columns.length).fill(null); + : Array(model.partitionColumns.length).fill(null); newPartitions[index] = partitionColumnValues[index].data[partition]; const newConfig: PartitionConfig = { partitions: newPartitions, @@ -136,10 +156,6 @@ class IrisGridPartitionSelector extends Component< this.updateConfig(newConfig, index, true); } - debounceUpdate = debounce((): void => { - this.sendUpdate(); - }, PARTITION_CHANGE_DEBOUNCE_MS); - sendUpdate(): void { log.debug2('sendUpdate'); @@ -148,27 +164,9 @@ class IrisGridPartitionSelector extends Component< onChange(partitionConfig); } - sendMerge(): void { - log.debug2('sendMerge'); - - this.debounceUpdate.cancel(); - - const { onMerge } = this.props; - onMerge(); - } - - sendKeyTable(): void { - log.debug2('sendOpenKeys'); - - this.debounceUpdate.cancel(); - - const { onKeyTable } = this.props; - onKeyTable(); - } - - /** Calls getFormattedString with a special character case */ + /** Calls model.displayString with a special character case */ getDisplayValue(index: number, partition?: unknown): string { - const { columns, getFormattedString } = this.props; + const { model } = this.props; const { partitionConfig } = this.state; const value = @@ -176,11 +174,11 @@ class IrisGridPartitionSelector extends Component< if (value == null || value === '') { return ''; } - const column = columns[index]; + const column = model.partitionColumns[index]; if (TableUtils.isCharType(column.type) && value.toString().length > 0) { return String.fromCharCode(parseInt(value.toString(), 10)); } - return getFormattedString(value, column.type, column.name); + return model.displayString(value, column.type, column.name); } /** @@ -201,7 +199,7 @@ class IrisGridPartitionSelector extends Component< return; } log.debug('partitionSelector update', index, partitionConfig); - const { getFormattedString } = this.props; + const { model } = this.props; // Cannot use columns from props since different index number will cause filters to fail const { columns } = this.partitionTables[this.partitionTables.length - 1]; if (!this.table) { @@ -215,13 +213,14 @@ class IrisGridPartitionSelector extends Component< // Update partition filters for (let i = index; i < partitionConfig.partitions.length; i += 1) { + // Await in loop necessary since each partition values list cascades from the previous iteration /* eslint-disable no-await-in-loop */ const partition = validPartitions[i]; const partitionColumn = columns[i]; this.partitionTables[i].applyFilter([...partitionFilters]); const partitionText = TableUtils.isCharType(partitionColumn.type) - ? getFormattedString( + ? model.displayString( partition, partitionColumn.type, partitionColumn.name @@ -242,7 +241,9 @@ class IrisGridPartitionSelector extends Component< ); } - const t = await this.table.copy(); + const t = await this.pending.add(this.table.copy(), tCopy => + tCopy.close() + ); t.applyFilter(partitionFilters); t.setViewport(0, 0, t.columns); const data = await t.getViewportData(); @@ -265,7 +266,7 @@ class IrisGridPartitionSelector extends Component< (columnValues, row) => { const column = columns[colIndex]; const value = row.get(column); - const displayValue = getFormattedString( + const displayValue = model.displayString( value, column.type, column.name @@ -298,9 +299,9 @@ class IrisGridPartitionSelector extends Component< return; } log.debug('partitionSelector clearDropdowns', partitionConfig); - const { columns, getFormattedString } = this.props; + const { model } = this.props; this.partitionTables.forEach(table => table.applyFilter([])); - const newColumnValues = Array(columns.length).fill({ + const newColumnValues = Array(model.partitionColumns.length).fill({ order: [], data: {}, } as Items); @@ -310,7 +311,7 @@ class IrisGridPartitionSelector extends Component< newColumnValues[0] = data.rows.reduce( (columnValues, row) => { const value = row.get(column); - const displayValue = getFormattedString( + const displayValue = model.displayString( value, column.type, column.name @@ -326,16 +327,16 @@ class IrisGridPartitionSelector extends Component< this.setState({ partitionConfig, partitionColumnValues: newColumnValues, - selectorValue: Array(columns.length).fill(''), + selectorValue: Array(model.partitionColumns.length).fill(''), }); } render(): JSX.Element { - const { columns } = this.props; + const { model } = this.props; const { selectorValue, partitionConfig, partitionColumnValues } = this.state; - const partitionSelectors = columns.map((column, index) => ( + const partitionSelectors = model.partitionColumns.map((column, index) => (
{column.name}
- {columns.length - 1 === index || ( + {model.partitionColumns.length - 1 === index || ( )}
diff --git a/packages/iris-grid/src/IrisGridPartitionedTableModel.ts b/packages/iris-grid/src/IrisGridPartitionedTableModel.ts index 5c5337a238..a9fee2877b 100644 --- a/packages/iris-grid/src/IrisGridPartitionedTableModel.ts +++ b/packages/iris-grid/src/IrisGridPartitionedTableModel.ts @@ -84,11 +84,7 @@ class IrisGridPartitionedTableModel } partitionKeysTable(): Promise
{ - const sorts = this.partitionColumns.map(column => column.sort().desc()); - return this.partitionedTable.getKeyTable().then(table => { - table.applySort(sorts); - return table; - }); + return this.partitionedTable.getKeyTable(); } async initializePartitionModel(): Promise { From 5e82a319c35f83cc9d0d03f37e7d1a923a910316 Mon Sep 17 00:00:00 2001 From: georgecwan Date: Thu, 21 Dec 2023 17:04:25 -0800 Subject: [PATCH 21/33] Fix initial key selection --- .../src/IrisGridPartitionedTableModel.ts | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/iris-grid/src/IrisGridPartitionedTableModel.ts b/packages/iris-grid/src/IrisGridPartitionedTableModel.ts index a9fee2877b..60fc6e39b7 100644 --- a/packages/iris-grid/src/IrisGridPartitionedTableModel.ts +++ b/packages/iris-grid/src/IrisGridPartitionedTableModel.ts @@ -40,13 +40,8 @@ class IrisGridPartitionedTableModel ) { super(dh, formatter); this.partitionedTable = partitionedTable; - // Get the most recently added key - let lastKey; - this.partitionedTable.getKeys().forEach(key => { - lastKey = key; - }); this.config = { - partitions: Array.isArray(lastKey) ? lastKey : [lastKey], + partitions: [], mode: 'partition', }; } @@ -88,13 +83,23 @@ class IrisGridPartitionedTableModel } async initializePartitionModel(): Promise { + const keyTable = await this.partitionedTable.getKeyTable(); + const sorts = keyTable.columns.map(column => column.sort().desc()); + keyTable.applySort(sorts); + keyTable.setViewport(0, 0); + const data = await keyTable.getViewportData(); + const row = data.rows[0]; + const values = keyTable.columns.map(column => row.get(column)); + this.config = { + partitions: values, + mode: 'partition', + }; + const initTable = await this.partitionedTable.getTable( this.config.partitions ); const tableCopy = await initTable.copy(); - return Promise.resolve( - new IrisGridTableModel(this.dh, tableCopy, this.formatter) - ); + return new IrisGridTableModel(this.dh, tableCopy, this.formatter); } } From 394932d50554a7e5155e98e2603d8722ec460841 Mon Sep 17 00:00:00 2001 From: georgecwan Date: Fri, 22 Dec 2023 13:08:48 -0800 Subject: [PATCH 22/33] Fix unit tests --- packages/iris-grid/src/IrisGrid.tsx | 61 ++++++++++++++--- .../src/IrisGridPartitionSelector.test.tsx | 68 ++++++------------- .../src/IrisGridPartitionSelector.tsx | 18 +++-- .../src/IrisGridPartitionedTableModel.ts | 38 +++-------- packages/iris-grid/src/IrisGridProxyModel.ts | 55 +++++++-------- packages/iris-grid/src/IrisGridTableModel.ts | 36 +++------- .../iris-grid/src/PartitionedGridModel.ts | 6 ++ 7 files changed, 140 insertions(+), 142 deletions(-) diff --git a/packages/iris-grid/src/IrisGrid.tsx b/packages/iris-grid/src/IrisGrid.tsx index 755b1d1d6a..b6458e02bc 100644 --- a/packages/iris-grid/src/IrisGrid.tsx +++ b/packages/iris-grid/src/IrisGrid.tsx @@ -154,6 +154,7 @@ import IrisGridModel from './IrisGridModel'; import { isPartitionedGridModel, PartitionConfig, + PartitionedGridModel, } from './PartitionedGridModel'; import IrisGridPartitionSelector from './IrisGridPartitionSelector'; import SelectDistinctBuilder from './sidebar/SelectDistinctBuilder'; @@ -849,9 +850,7 @@ export class IrisGrid extends Component { const { model } = this.props; try { if (isPartitionedGridModel(model) && model.isPartitionRequired) { - this.setState({ isSelectingPartition: true }, () => { - this.initState(); - }); + this.loadPartitionsTable(model); } else { this.initState(); } @@ -1941,6 +1940,46 @@ export class IrisGrid extends Component { this.initFormatter(); } + async loadPartitionsTable(model: PartitionedGridModel): Promise { + const { partitionConfig } = this.state; + if (partitionConfig !== undefined) { + this.setState({ isSelectingPartition: true }, this.initState); + return; + } + + try { + const keyTable = await this.pending.add( + model.partitionKeysTable(), + resolved => resolved.close() + ); + + const sorts = keyTable.columns.map(column => column.sort().desc()); + keyTable.applySort(sorts); + keyTable.setViewport(0, 0); + + const data = await this.pending.add(keyTable.getViewportData()); + if (data.rows.length > 0) { + const row = data.rows[0]; + const values = keyTable.columns.map(column => row.get(column)); + const newPartition: PartitionConfig = { + partitions: values, + mode: 'partition', + }; + + this.setState( + { isSelectingPartition: true, partitionConfig: newPartition }, + this.initState + ); + } else { + log.info('Table does not have any data'); + this.setState({ isSelectingPartition: false }, this.initState); + } + keyTable.close(); + } catch (error) { + this.handleTableLoadError(error); + } + } + copyCell( columnIndex: GridRangeIndex, rowIndex: GridRangeIndex, @@ -4421,13 +4460,15 @@ export class IrisGrid extends Component { unmountOnExit >
- {isPartitionedGridModel(model) && model.isPartitionRequired && ( - - )} + {isPartitionedGridModel(model) && + model.isPartitionRequired && + partitionConfig && ( + + )}
+ Promise.resolve(irisGridTestUtils.makeTable()) + ), + partitionColumns: columns, + } as unknown as PartitionedGridModel; + return model; +} function makeIrisGridPartitionSelector( - table = new IrisGridTestUtils(dh).makeTable(), - columns = [new IrisGridTestUtils(dh).makeColumn()], + model = makeModel(), onChange = jest.fn(), - onMerge = jest.fn(), - onKeyTable = jest.fn(), - getFormattedString = jest.fn(value => `${value}`), partitionConfig = { partitions: [], mode: 'merged' } ) { - const tablePromise = Promise.resolve(table); return render( ); @@ -33,41 +38,12 @@ it('unmounts successfully without crashing', () => { makeIrisGridPartitionSelector(); }); -it('calls onKeyTable when key button is clicked', () => { - const onKeyTable = jest.fn(); - const component = makeIrisGridPartitionSelector( - undefined, - undefined, - undefined, - undefined, - onKeyTable - ); - - const keyButton = component.getAllByRole('button')[0]; - fireEvent.click(keyButton); - expect(onKeyTable).toHaveBeenCalled(); -}); - -it('calls onMerge when merge button is clicked', () => { - const onMerge = jest.fn(); - const component = makeIrisGridPartitionSelector( - undefined, - undefined, - undefined, - onMerge - ); - - const mergeButton = component.getAllByRole('button')[1]; - fireEvent.click(mergeButton); - expect(onMerge).toHaveBeenCalled(); -}); - it('should display multiple selectors to match columns', () => { const columns = [ - new IrisGridTestUtils(dh).makeColumn('a'), - new IrisGridTestUtils(dh).makeColumn('b'), + irisGridTestUtils.makeColumn('a'), + irisGridTestUtils.makeColumn('b'), ]; - const component = makeIrisGridPartitionSelector(undefined, columns); + const component = makeIrisGridPartitionSelector(makeModel(columns)); const selectors = component.getAllByRole('combobox'); expect(selectors).toHaveLength(2); diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.tsx b/packages/iris-grid/src/IrisGridPartitionSelector.tsx index 8eb4d1703a..5dc54d8901 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.tsx +++ b/packages/iris-grid/src/IrisGridPartitionSelector.tsx @@ -27,6 +27,7 @@ interface IrisGridPartitionSelectorState { partitionConfig: PartitionConfig; partitionColumnValues: readonly Items[]; selectorValue: readonly string[]; + isLoading: boolean; } class IrisGridPartitionSelector extends Component< IrisGridPartitionSelectorProps, @@ -57,6 +58,7 @@ class IrisGridPartitionSelector extends Component< order: [], data: {}, })), + isLoading: true, }; } @@ -153,6 +155,7 @@ class IrisGridPartitionSelector extends Component< mode: 'partition', }; + this.setState({ isLoading: true }); this.updateConfig(newConfig, index, true); } @@ -220,11 +223,7 @@ class IrisGridPartitionSelector extends Component< this.partitionTables[i].applyFilter([...partitionFilters]); const partitionText = TableUtils.isCharType(partitionColumn.type) - ? model.displayString( - partition, - partitionColumn.type, - partitionColumn.name - ) + ? this.getDisplayValue(i, partition) : partition?.toString() ?? ''; const partitionFilter = partition === null @@ -289,6 +288,7 @@ class IrisGridPartitionSelector extends Component< selectorValue: columns.map((_, i) => this.getDisplayValue(i, validPartitions[i] ?? '') ), + isLoading: false, }, updateIrisGrid ? this.sendUpdate : undefined ); @@ -333,7 +333,7 @@ class IrisGridPartitionSelector extends Component< render(): JSX.Element { const { model } = this.props; - const { selectorValue, partitionConfig, partitionColumnValues } = + const { selectorValue, partitionConfig, partitionColumnValues, isLoading } = this.state; const partitionSelectors = model.partitionColumns.map((column, index) => ( @@ -343,7 +343,9 @@ class IrisGridPartitionSelector extends Component< className="custom-select-sm" value={selectorValue[index]} onChange={value => this.handlePartitionSelect(index, value)} - disabled={index > 0 && partitionConfig.mode !== 'partition'} + disabled={ + (index > 0 && partitionConfig.mode !== 'partition') || isLoading + } > {partitionConfig.mode === 'partition' || (
{ + return this.partitionedTable.getMergedTable(); + } - const initTable = await this.partitionedTable.getTable( - this.config.partitions - ); - const tableCopy = await initTable.copy(); - return new IrisGridTableModel(this.dh, tableCopy, this.formatter); + partitionTable(partitionConfig: PartitionConfig): Promise
| null { + if (partitionConfig.mode !== 'partition') { + return null; + } + // TODO: Copy is necessary for now since getTable returns memoized tables https://github.com/deephaven/web-client-ui/pull/1663#discussion_r1434984641 + return this.partitionedTable + .getTable(partitionConfig.partitions) + .then(table => table.copy()); } } diff --git a/packages/iris-grid/src/IrisGridProxyModel.ts b/packages/iris-grid/src/IrisGridProxyModel.ts index 68eb67beff..d22801fb23 100644 --- a/packages/iris-grid/src/IrisGridProxyModel.ts +++ b/packages/iris-grid/src/IrisGridProxyModel.ts @@ -30,9 +30,7 @@ import { MoveOperation, } from '@deephaven/grid'; import IrisGridTableModel from './IrisGridTableModel'; -import IrisGridPartitionedTableModel, { - isIrisGridPartitionedTableModel, -} from './IrisGridPartitionedTableModel'; +import IrisGridPartitionedTableModel from './IrisGridPartitionedTableModel'; import IrisGridTreeTableModel from './IrisGridTreeTableModel'; import IrisGridModel from './IrisGridModel'; import { @@ -108,8 +106,6 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { this.modelPromise = null; this.rollup = null; this.selectDistinct = []; - - this.initializePartitionModel(); } close(): void { @@ -528,23 +524,21 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { .partitionKeysTable() .then(table => makeModel(this.dh, table, this.formatter)) ); - } else if (isIrisGridPartitionedTableModel(this.originalModel)) { - if (partitionConfig.mode === 'merged') { - this.setNextModel( - this.originalModel.partitionedTable - .getMergedTable() - .then(table => makeModel(this.dh, table, this.formatter)) - ); - return; - } - const tablePromise = this.originalModel.partitionedTable - .getTable(partitionConfig.partitions) - .then(table => table.copy()); + } else if (partitionConfig.mode === 'merged') { + this.setNextModel( + this.originalModel + .partitionMergedTable() + .then(table => makeModel(this.dh, table, this.formatter)) + ); + } else { + const partitionTable = this.originalModel.partitionTable(partitionConfig); this.setNextModel( - tablePromise.then(table => makeModel(this.dh, table, this.formatter)) + partitionTable === null + ? Promise.resolve(this.originalModel) + : partitionTable.then(table => + makeModel(this.dh, table, this.formatter) + ) ); - } else if (isIrisGridTableModelTemplate(this.originalModel)) { - this.setNextModel(Promise.resolve(this.originalModel)); } } @@ -555,6 +549,20 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { return this.originalModel.partitionKeysTable(); } + partitionMergedTable(): Promise
{ + if (!isPartitionedGridModel(this.originalModel)) { + throw new Error('Partitions are not available'); + } + return this.originalModel.partitionMergedTable(); + } + + partitionTable(partitionConfig: PartitionConfig): Promise
| null { + if (!isPartitionedGridModel(this.originalModel)) { + throw new Error('Partitions are not available'); + } + return this.originalModel.partitionTable(partitionConfig); + } + get formatter(): Formatter { return this.model.formatter; } @@ -816,13 +824,6 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { get isSeekRowAvailable(): boolean { return this.model.isSeekRowAvailable; } - - initializePartitionModel(): void { - const { model } = this; - if (isIrisGridPartitionedTableModel(model)) { - this.setNextModel(model.initializePartitionModel()); - } - } } export default IrisGridProxyModel; diff --git a/packages/iris-grid/src/IrisGridTableModel.ts b/packages/iris-grid/src/IrisGridTableModel.ts index 94968ce696..a63f534151 100644 --- a/packages/iris-grid/src/IrisGridTableModel.ts +++ b/packages/iris-grid/src/IrisGridTableModel.ts @@ -77,7 +77,7 @@ class IrisGridTableModel mode: 'partition', }; if (this.table.isUncoalesced && this.isValuesTableAvailable) { - this.partitionTablePromise = this.initializePartitionModel(); + this.partitionTablePromise = this.initializePartitionTable(); } } @@ -278,6 +278,15 @@ class IrisGridTableModel throw new Error('Partition table not initialized'); } + partitionMergedTable(): Promise
{ + return this.table.copy(); + } + + partitionTable(partitionConfig: PartitionConfig): Promise
| null { + this.partitionConfig = partitionConfig; + return null; + } + set filter(filter: FilterCondition[]) { this.closeSubscription(); this.table.applyFilter([...filter, ...this.partitionFilters]); @@ -500,31 +509,8 @@ class IrisGridTableModel return this.table.seekRow != null; } - private async initializePartitionModel(): Promise
{ + private async initializePartitionTable(): Promise
{ const partitionTable = await this.valuesTable(this._partitionColumns); - - const columns = partitionTable.columns.slice( - 0, - this._partitionColumns.length - ); - const sorts = columns.map(column => column.sort().desc()); - partitionTable.applySort(sorts); - partitionTable.setViewport(0, 0, columns); - - const data = await partitionTable.getViewportData(); - if ( - data.rows.length > 0 && - this._partitionConfig.partitions.length === 0 && - this._partitionConfig.mode === 'partition' - ) { - const row = data.rows[0]; - const values = columns.map(column => row.get(column)); - - this.partitionConfig = { - partitions: values, - mode: 'partition', - }; - } this._partitionTable = partitionTable; return partitionTable; } diff --git a/packages/iris-grid/src/PartitionedGridModel.ts b/packages/iris-grid/src/PartitionedGridModel.ts index 2f7652636b..6ab0303540 100644 --- a/packages/iris-grid/src/PartitionedGridModel.ts +++ b/packages/iris-grid/src/PartitionedGridModel.ts @@ -31,4 +31,10 @@ export interface PartitionedGridModel extends IrisGridModel { /** Get a keys table for the partitions */ partitionKeysTable: () => Promise
; + + /** Get a merged table containing all partitions */ + partitionMergedTable: () => Promise
; + + /** Get a table containing the selected partition */ + partitionTable: (partitionConfig: PartitionConfig) => Promise
| null; } From 1899334a41babd041448c1a072c769382c341d35 Mon Sep 17 00:00:00 2001 From: mikebender Date: Thu, 4 Jan 2024 15:58:26 -0500 Subject: [PATCH 23/33] WIP cleaning up George's partitioned table selector --- .../src/IrisGridPartitionSelector.tsx | 43 +++--- .../src/IrisGridPartitionedTableModel.ts | 28 +--- packages/iris-grid/src/IrisGridProxyModel.ts | 72 ++++----- packages/iris-grid/src/IrisGridTableModel.ts | 142 ++++++------------ .../iris-grid/src/IrisGridTreeTableModel.ts | 5 +- .../iris-grid/src/PartitionedGridModel.ts | 39 ++++- packages/jsapi-utils/src/TableUtils.ts | 5 +- 7 files changed, 146 insertions(+), 188 deletions(-) diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.tsx b/packages/iris-grid/src/IrisGridPartitionSelector.tsx index 5dc54d8901..6295b2b279 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.tsx +++ b/packages/iris-grid/src/IrisGridPartitionSelector.tsx @@ -24,7 +24,6 @@ interface IrisGridPartitionSelectorProps { onChange: (partitionConfig: PartitionConfig) => void; } interface IrisGridPartitionSelectorState { - partitionConfig: PartitionConfig; partitionColumnValues: readonly Items[]; selectorValue: readonly string[]; isLoading: boolean; @@ -45,7 +44,7 @@ class IrisGridPartitionSelector extends Component< this.handleMergeClick = this.handleMergeClick.bind(this); this.handlePartitionSelect = this.handlePartitionSelect.bind(this); - const { model, partitionConfig } = props; + const { model } = props; this.tableUtils = new TableUtils(model.dh); this.table = null; this.partitionTables = null; @@ -53,7 +52,6 @@ class IrisGridPartitionSelector extends Component< this.state = { selectorValue: model.partitionColumns.map(() => ''), - partitionConfig, partitionColumnValues: model.partitionColumns.map(() => ({ order: [], data: {}, @@ -120,28 +118,28 @@ class IrisGridPartitionSelector extends Component< handleKeyTableClick(): void { log.debug2('handleKeyTableClick'); - this.setState( - prevState => ({ - partitionConfig: { ...prevState.partitionConfig, mode: 'keys' }, - }), - this.sendUpdate - ); + const { partitionConfig } = this.props; + const newPartitionConfig = { ...partitionConfig }; + // Toggle between Keys and Partition mode + newPartitionConfig.mode = + partitionConfig.mode === 'keys' ? 'partition' : 'keys'; + this.sendUpdate(newPartitionConfig); } handleMergeClick(): void { log.debug2('handleMergeClick'); - this.setState( - prevState => ({ - partitionConfig: { ...prevState.partitionConfig, mode: 'merged' }, - }), - this.sendUpdate - ); + const { partitionConfig } = this.props; + const newPartitionConfig = { ...partitionConfig }; + // Toggle between Merged and Partition mode + newPartitionConfig.mode = + partitionConfig.mode === 'merged' ? 'partition' : 'merged'; + this.sendUpdate(newPartitionConfig); } handlePartitionSelect(index: number, partition: string): void { - const { model } = this.props; - const { partitionConfig: prevConfig, partitionColumnValues } = this.state; + const { model, partitionConfig } = this.props; + const { partitionColumnValues } = this.state; log.debug('handlePartitionSelect', index, partition, prevConfig); @@ -159,18 +157,16 @@ class IrisGridPartitionSelector extends Component< this.updateConfig(newConfig, index, true); } - sendUpdate(): void { - log.debug2('sendUpdate'); + sendUpdate(partitionConfig: PartitionConfig): void { + log.debug2('sendUpdate', partitionConfig); const { onChange } = this.props; - const { partitionConfig } = this.state; onChange(partitionConfig); } /** Calls model.displayString with a special character case */ getDisplayValue(index: number, partition?: unknown): string { - const { model } = this.props; - const { partitionConfig } = this.state; + const { model, partitionConfig } = this.props; const value = partition === undefined ? partitionConfig.partitions[index] : partition; @@ -191,8 +187,7 @@ class IrisGridPartitionSelector extends Component< */ async updateConfig( partitionConfig: PartitionConfig, - index = 0, - updateIrisGrid = false + index = 0 ): Promise { if (partitionConfig.mode !== 'partition') { this.clearDropdowns(partitionConfig); diff --git a/packages/iris-grid/src/IrisGridPartitionedTableModel.ts b/packages/iris-grid/src/IrisGridPartitionedTableModel.ts index 8ab14d8b62..0314744081 100644 --- a/packages/iris-grid/src/IrisGridPartitionedTableModel.ts +++ b/packages/iris-grid/src/IrisGridPartitionedTableModel.ts @@ -8,16 +8,17 @@ import type { import { Formatter } from '@deephaven/jsapi-utils'; import { ColumnName } from './CommonTypes'; import EmptyIrisGridModel from './EmptyIrisGridModel'; -import { PartitionConfig, PartitionedGridModel } from './PartitionedGridModel'; +import { + PartitionConfig, + PartitionedGridModelProvider, +} from './PartitionedGridModel'; class IrisGridPartitionedTableModel extends EmptyIrisGridModel - implements PartitionedGridModel + implements PartitionedGridModelProvider { readonly partitionedTable: PartitionedTable; - private config: PartitionConfig; - /** * @param dh JSAPI instance * @param table Partitioned table to be used in the model @@ -30,10 +31,6 @@ class IrisGridPartitionedTableModel ) { super(dh, formatter); this.partitionedTable = partitionedTable; - this.config = { - partitions: [], - mode: 'partition', - }; } get isPartitionRequired(): boolean { @@ -60,14 +57,6 @@ class IrisGridPartitionedTableModel return this.partitionedTable.keyColumns; } - get partitionConfig(): PartitionConfig { - return this.config; - } - - set partitionConfig(rollupConfig: PartitionConfig) { - this.config = rollupConfig; - } - partitionKeysTable(): Promise
{ return this.partitionedTable.getKeyTable(); } @@ -76,13 +65,10 @@ class IrisGridPartitionedTableModel return this.partitionedTable.getMergedTable(); } - partitionTable(partitionConfig: PartitionConfig): Promise
| null { - if (partitionConfig.mode !== 'partition') { - return null; - } + async partitionTable(partitions: unknown[]): Promise
{ // TODO: Copy is necessary for now since getTable returns memoized tables https://github.com/deephaven/web-client-ui/pull/1663#discussion_r1434984641 return this.partitionedTable - .getTable(partitionConfig.partitions) + .getTable(partitions) .then(table => table.copy()); } } diff --git a/packages/iris-grid/src/IrisGridProxyModel.ts b/packages/iris-grid/src/IrisGridProxyModel.ts index d22801fb23..5ce161f05c 100644 --- a/packages/iris-grid/src/IrisGridProxyModel.ts +++ b/packages/iris-grid/src/IrisGridProxyModel.ts @@ -45,7 +45,7 @@ import type ColumnHeaderGroup from './ColumnHeaderGroup'; import { PartitionConfig, PartitionedGridModel, - isPartitionedGridModel, + isPartitionedGridModelProvider, } from './PartitionedGridModel'; const log = Log.module('IrisGridProxyModel'); @@ -87,6 +87,8 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { rollup: RollupConfig | null; + partition: PartitionConfig | null; + selectDistinct: ColumnName[]; constructor( @@ -105,6 +107,7 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { this.model = model; this.modelPromise = null; this.rollup = null; + this.partition = null; this.selectDistinct = []; } @@ -463,7 +466,7 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { } get partitionColumns(): readonly Column[] { - if (!isPartitionedGridModel(this.originalModel)) { + if (!isPartitionedGridModelProvider(this.originalModel)) { throw new Error('Partitions are not available'); } return this.originalModel.partitionColumns; @@ -499,68 +502,65 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { this.model.filter = filter; } - get partitionConfig(): PartitionConfig { + get partitionConfig(): PartitionConfig | null { if ( - !isPartitionedGridModel(this.originalModel) || + !isPartitionedGridModelProvider(this.originalModel) || !this.originalModel.isPartitionRequired ) { throw new Error('Partitions are not available'); } - return this.originalModel.partitionConfig; + return this.partition; } - set partitionConfig(partitionConfig: PartitionConfig) { - if ( - !isPartitionedGridModel(this.originalModel) || - !this.originalModel.isPartitionRequired - ) { + set partitionConfig(partitionConfig: PartitionConfig | null) { + if (!this.isPartitionRequired) { throw new Error('Partitions are not available'); } log.debug('set partitionConfig', partitionConfig); - this.originalModel.partitionConfig = partitionConfig; - if (partitionConfig.mode === 'keys') { - this.setNextModel( - this.originalModel + this.partition = partitionConfig; + + let modelPromise = Promise.resolve(this.originalModel); + if ( + partitionConfig != null && + isPartitionedGridModelProvider(this.originalModel) + ) { + if (partitionConfig.mode === 'keys') { + modelPromise = this.originalModel .partitionKeysTable() - .then(table => makeModel(this.dh, table, this.formatter)) - ); - } else if (partitionConfig.mode === 'merged') { - this.setNextModel( - this.originalModel + .then(table => makeModel(this.dh, table, this.formatter)); + } else if (partitionConfig.mode === 'merged') { + modelPromise = this.originalModel .partitionMergedTable() - .then(table => makeModel(this.dh, table, this.formatter)) - ); - } else { - const partitionTable = this.originalModel.partitionTable(partitionConfig); - this.setNextModel( - partitionTable === null - ? Promise.resolve(this.originalModel) - : partitionTable.then(table => - makeModel(this.dh, table, this.formatter) - ) - ); + .then(table => makeModel(this.dh, table, this.formatter)); + } else { + modelPromise = this.originalModel + .partitionTable(partitionConfig.partitions) + .then(table => makeModel(this.dh, table, this.formatter)); + } } + + this.setNextModel(modelPromise); } partitionKeysTable(): Promise
{ - if (!isPartitionedGridModel(this.originalModel)) { + if (!isPartitionedGridModelProvider(this.originalModel)) { throw new Error('Partitions are not available'); } return this.originalModel.partitionKeysTable(); } partitionMergedTable(): Promise
{ - if (!isPartitionedGridModel(this.originalModel)) { + if (!isPartitionedGridModelProvider(this.originalModel)) { throw new Error('Partitions are not available'); } return this.originalModel.partitionMergedTable(); } - partitionTable(partitionConfig: PartitionConfig): Promise
| null { - if (!isPartitionedGridModel(this.originalModel)) { + partitionTable(partitions: unknown[]): Promise
{ + if (!isPartitionedGridModelProvider(this.originalModel)) { throw new Error('Partitions are not available'); } - return this.originalModel.partitionTable(partitionConfig); + return this.originalModel.partitionTable(partitions); } get formatter(): Formatter { @@ -689,7 +689,7 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { } get isPartitionRequired(): boolean { - return isPartitionedGridModel(this.originalModel) + return isPartitionedGridModelProvider(this.originalModel) ? this.originalModel.isPartitionRequired : false; } diff --git a/packages/iris-grid/src/IrisGridTableModel.ts b/packages/iris-grid/src/IrisGridTableModel.ts index a63f534151..9d8ebbcd2b 100644 --- a/packages/iris-grid/src/IrisGridTableModel.ts +++ b/packages/iris-grid/src/IrisGridTableModel.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-underscore-dangle */ /* eslint class-methods-use-this: "off" */ import memoize from 'memoize-one'; import { GridRange, ModelIndex } from '@deephaven/grid'; @@ -23,7 +22,7 @@ import { import IrisGridModel from './IrisGridModel'; import { ColumnName, UIRow, UITotalsTableConfig } from './CommonTypes'; import IrisGridTableModelTemplate from './IrisGridTableModelTemplate'; -import { PartitionConfig, PartitionedGridModel } from './PartitionedGridModel'; +import { PartitionedGridModelProvider } from './PartitionedGridModel'; const log = Log.module('IrisGridTableModel'); @@ -33,7 +32,7 @@ const log = Log.module('IrisGridTableModel'); class IrisGridTableModel extends IrisGridTableModelTemplate - implements PartitionedGridModel + implements PartitionedGridModelProvider { userFrozenColumns?: ColumnName[]; @@ -41,17 +40,7 @@ class IrisGridTableModel formatColumnList: CustomColumn[]; - private initialUncoalesced: boolean; - - private _partitionConfig: PartitionConfig; - - private _partitionColumns: Column[]; - - private partitionFilters: FilterCondition[] = []; - - private partitionTablePromise?: Promise
; - - private _partitionTable?: Table; + initialFilters: FilterCondition[] = []; /** * @param dh JSAPI instance @@ -68,22 +57,7 @@ class IrisGridTableModel super(dh, table, formatter, inputTable); this.customColumnList = []; this.formatColumnList = []; - this.initialUncoalesced = this.table.isUncoalesced; - this._partitionColumns = this.table.columns.filter( - c => c.isPartitionColumn - ); - this._partitionConfig = { - partitions: [], - mode: 'partition', - }; - if (this.table.isUncoalesced && this.isValuesTableAvailable) { - this.partitionTablePromise = this.initializePartitionTable(); - } - } - - close(): void { - super.close(); - this._partitionTable?.close(); + this.initialFilters = table.filter; } get isExportAvailable(): boolean { @@ -220,76 +194,56 @@ class IrisGridTableModel } get partitionColumns(): readonly Column[] { - return this._partitionColumns; + return this.getCachedPartitionColumns(this.columns); } - get partitionConfig(): PartitionConfig { - return this._partitionConfig; + partitionKeysTable(): Promise
{ + return this.valuesTable(this.partitionColumns); } - set partitionConfig(partitionConfig: PartitionConfig) { - log.debug('setting partition', partitionConfig); - const { partitions, mode } = partitionConfig; - const partitionFilters = []; + async partitionMergedTable(): Promise
{ + const t = await this.table.copy(); + t.applyFilter([]); + return t; + } - if (mode === 'partition') { - for (let i = 0; i < this.partitionColumns.length; i += 1) { - const partition = partitions[i]; - const partitionColumn = this.partitionColumns[i]; - - if ( - partition != null && - !(TableUtils.isCharType(partitionColumn.type) && partition === '') - ) { - const partitionText = TableUtils.isCharType(partitionColumn.type) - ? this.displayString( - partition, - partitionColumn.type, - partitionColumn.name - ) - : partition.toString(); - const partitionFilter = this.tableUtils.makeQuickFilterFromComponent( - partitionColumn, - partitionText - ); - if (partitionFilter !== null) { - partitionFilters.push(partitionFilter); - } + async partitionTable(partitions: unknown[]): Promise
{ + log.debug('getting partition table for partitions', partitions); + + const partitionFilters = []; + for (let i = 0; i < this.partitionColumns.length; i += 1) { + const partition = partitions[i]; + const partitionColumn = this.partitionColumns[i]; + + if ( + partition != null && + !(TableUtils.isCharType(partitionColumn.type) && partition === '') + ) { + const partitionText = TableUtils.isCharType(partitionColumn.type) + ? this.displayString( + partition, + partitionColumn.type, + partitionColumn.name + ) + : partition.toString(); + const partitionFilter = this.tableUtils.makeQuickFilterFromComponent( + partitionColumn, + partitionText + ); + if (partitionFilter !== null) { + partitionFilters.push(partitionFilter); } } } - const prevFilters = super.filter.slice( - 0, - super.filter.length - this.partitionFilters.length - ); - this._partitionConfig = partitionConfig; - this.partitionFilters = partitionFilters; - this.filter = prevFilters; - } - - partitionKeysTable(): Promise
{ - if (this._partitionTable !== undefined) { - return this._partitionTable.copy(); - } - if (this.partitionTablePromise !== undefined) { - return this.partitionTablePromise.then(table => table.copy()); - } - throw new Error('Partition table not initialized'); - } - - partitionMergedTable(): Promise
{ - return this.table.copy(); - } - - partitionTable(partitionConfig: PartitionConfig): Promise
| null { - this.partitionConfig = partitionConfig; - return null; + const t = await this.table.copy(); + t.applyFilter([...this.initialFilters, ...partitionFilters]); + return t; } set filter(filter: FilterCondition[]) { this.closeSubscription(); - this.table.applyFilter([...filter, ...this.partitionFilters]); + this.table.applyFilter([...this.initialFilters, ...filter]); this.applyViewport(); } @@ -341,11 +295,11 @@ class IrisGridTableModel } get isFilterRequired(): boolean { - return this.initialUncoalesced; + return this.table.isUncoalesced; } get isPartitionRequired(): boolean { - return this.initialUncoalesced && this.isValuesTableAvailable; + return this.table.isUncoalesced && this.isValuesTableAvailable; } isFilterable(columnIndex: ModelIndex): boolean { @@ -365,6 +319,10 @@ class IrisGridTableModel new Set(columns.map((_: Column, index: ModelIndex) => index)) ); + getCachedPartitionColumns = memoize((columns: readonly Column[]) => + columns.filter(column => column.isPartitionColumn) + ); + isColumnMovable(modelIndex: ModelIndex): boolean { const columnName = this.columns[modelIndex].name; if ( @@ -508,12 +466,6 @@ class IrisGridTableModel get isSeekRowAvailable(): boolean { return this.table.seekRow != null; } - - private async initializePartitionTable(): Promise
{ - const partitionTable = await this.valuesTable(this._partitionColumns); - this._partitionTable = partitionTable; - return partitionTable; - } } export default IrisGridTableModel; diff --git a/packages/iris-grid/src/IrisGridTreeTableModel.ts b/packages/iris-grid/src/IrisGridTreeTableModel.ts index cee4dd19af..65d2800804 100644 --- a/packages/iris-grid/src/IrisGridTreeTableModel.ts +++ b/packages/iris-grid/src/IrisGridTreeTableModel.ts @@ -138,10 +138,7 @@ class IrisGridTreeTableModel extends IrisGridTableModelTemplate< ): Promise { assertNotNull(this.viewport); assertNotNull(this.viewportData); - const { columns } = - this.viewport.columns === undefined - ? this - : (this.viewport as { columns: Column[] }); + const columns = this.viewport.columns ?? this.columns; const result = []; if (includeHeaders != null && includeHeaders) { diff --git a/packages/iris-grid/src/PartitionedGridModel.ts b/packages/iris-grid/src/PartitionedGridModel.ts index 6ab0303540..6f9f70ab5f 100644 --- a/packages/iris-grid/src/PartitionedGridModel.ts +++ b/packages/iris-grid/src/PartitionedGridModel.ts @@ -1,10 +1,25 @@ import type { Column, Table } from '@deephaven/jsapi-types'; import IrisGridModel from './IrisGridModel'; +export function isPartitionedGridModelProvider( + model: IrisGridModel +): model is PartitionedGridModelProvider { + return ( + (model as PartitionedGridModel)?.isPartitionRequired !== undefined && + (model as PartitionedGridModel)?.partitionColumns !== undefined && + (model as PartitionedGridModel)?.partitionKeysTable !== undefined && + (model as PartitionedGridModel)?.partitionMergedTable !== undefined && + (model as PartitionedGridModel)?.partitionTable !== undefined + ); +} + export function isPartitionedGridModel( model: IrisGridModel ): model is PartitionedGridModel { - return (model as PartitionedGridModel)?.isPartitionRequired !== undefined; + return ( + isPartitionedGridModelProvider(model) && + (model as PartitionedGridModel).partitionConfig !== undefined + ); } export interface PartitionConfig { @@ -15,11 +30,10 @@ export interface PartitionConfig { mode: 'keys' | 'merged' | 'partition'; } -export interface PartitionedGridModel extends IrisGridModel { - get partitionConfig(): PartitionConfig; - - set partitionConfig(partitionConfig: PartitionConfig); - +/** + * A grid model that provides key tables and partitions, cannot accept a `PartitionConfig` being set + */ +export interface PartitionedGridModelProvider extends IrisGridModel { /** * Retrieve the columns this model is partitioned on * @returns All columns to partition on @@ -36,5 +50,16 @@ export interface PartitionedGridModel extends IrisGridModel { partitionMergedTable: () => Promise
; /** Get a table containing the selected partition */ - partitionTable: (partitionConfig: PartitionConfig) => Promise
| null; + partitionTable: (partitions: unknown[]) => Promise
; +} + +/** + * A grid model that can be partitioned on a column + */ +export interface PartitionedGridModel extends PartitionedGridModelProvider { + /** Retrieve the currently set partition config */ + get partitionConfig(): PartitionConfig | null; + + /** Set the partition config */ + set partitionConfig(partitionConfig: PartitionConfig | null); } diff --git a/packages/jsapi-utils/src/TableUtils.ts b/packages/jsapi-utils/src/TableUtils.ts index 9ea05bdd5c..c1df46b0b8 100644 --- a/packages/jsapi-utils/src/TableUtils.ts +++ b/packages/jsapi-utils/src/TableUtils.ts @@ -749,7 +749,10 @@ export class TableUtils { static isPartitionedTable(table: unknown): table is PartitionedTable { return ( - table != null && (table as PartitionedTable).getMergedTable !== undefined + table != null && + (table as PartitionedTable).getMergedTable !== undefined && + (table as PartitionedTable).getKeyTable !== undefined && + (table as PartitionedTable).getKeys !== undefined ); } From c0bf727f5b498af71f8fd101e5d2d2d98b93d78a Mon Sep 17 00:00:00 2001 From: mikebender Date: Fri, 5 Jan 2024 10:53:03 -0500 Subject: [PATCH 24/33] Cleaning up some more - Added TableDropdown component to handle displaying a dropdown of values based on a table - Simplify the PartitionSelector a bit - Still some bugs - rehydration isn't working, table appears blank to start --- package-lock.json | 2 + packages/components/src/Option.tsx | 20 +- packages/iris-grid/package.json | 1 + .../src/IrisGridPartitionSelector.test.tsx | 13 +- .../src/IrisGridPartitionSelector.tsx | 382 ++++++++---------- .../src/IrisGridPartitionedTableModel.ts | 10 +- packages/iris-grid/tsconfig.json | 1 + .../jsapi-components/src/TableDropdown.tsx | 121 ++++++ packages/jsapi-components/src/index.ts | 1 + 9 files changed, 300 insertions(+), 251 deletions(-) create mode 100644 packages/jsapi-components/src/TableDropdown.tsx diff --git a/package-lock.json b/package-lock.json index 718a6e7aba..49e189a008 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28577,6 +28577,7 @@ "@deephaven/filters": "file:../filters", "@deephaven/grid": "file:../grid", "@deephaven/icons": "file:../icons", + "@deephaven/jsapi-components": "file:../jsapi-components", "@deephaven/jsapi-types": "file:../jsapi-types", "@deephaven/jsapi-utils": "file:../jsapi-utils", "@deephaven/log": "file:../log", @@ -30540,6 +30541,7 @@ "@deephaven/filters": "file:../filters", "@deephaven/grid": "file:../grid", "@deephaven/icons": "file:../icons", + "@deephaven/jsapi-components": "file:../jsapi-components", "@deephaven/jsapi-shim": "file:../jsapi-shim", "@deephaven/jsapi-types": "file:../jsapi-types", "@deephaven/jsapi-utils": "file:../jsapi-utils", diff --git a/packages/components/src/Option.tsx b/packages/components/src/Option.tsx index 34527b6452..0d85d7048f 100644 --- a/packages/components/src/Option.tsx +++ b/packages/components/src/Option.tsx @@ -1,23 +1,13 @@ -import React from 'react'; +import React, { OptionHTMLAttributes } from 'react'; -export type OptionProps = { +export type OptionProps = OptionHTMLAttributes & { children: React.ReactNode; - disabled?: boolean; - value: string; 'data-testid'?: string; }; -function Option({ - children, - disabled, - value, - 'data-testid': dataTestId, -}: OptionProps): JSX.Element { - return ( - - ); +function Option({ children, ...props }: OptionProps): JSX.Element { + // eslint-disable-next-line react/jsx-props-no-spreading + return ; } export default Option; diff --git a/packages/iris-grid/package.json b/packages/iris-grid/package.json index d5145a2471..91d78d88e5 100644 --- a/packages/iris-grid/package.json +++ b/packages/iris-grid/package.json @@ -36,6 +36,7 @@ "@deephaven/filters": "file:../filters", "@deephaven/grid": "file:../grid", "@deephaven/icons": "file:../icons", + "@deephaven/jsapi-components": "file:../jsapi-components", "@deephaven/jsapi-types": "file:../jsapi-types", "@deephaven/jsapi-utils": "file:../jsapi-utils", "@deephaven/log": "file:../log", diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.test.tsx b/packages/iris-grid/src/IrisGridPartitionSelector.test.tsx index 8948ff965d..0d26ca3579 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.test.tsx +++ b/packages/iris-grid/src/IrisGridPartitionSelector.test.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { render } from '@testing-library/react'; +import { ApiContext } from '@deephaven/jsapi-bootstrap'; import dh from '@deephaven/jsapi-shim'; import IrisGridPartitionSelector from './IrisGridPartitionSelector'; import IrisGridTestUtils from './IrisGridTestUtils'; @@ -26,11 +27,13 @@ function makeIrisGridPartitionSelector( partitionConfig = { partitions: [], mode: 'merged' } ) { return render( - + + + ); } diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.tsx b/packages/iris-grid/src/IrisGridPartitionSelector.tsx index 6295b2b279..5a079fa092 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.tsx +++ b/packages/iris-grid/src/IrisGridPartitionSelector.tsx @@ -1,42 +1,36 @@ import React, { Component } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, Option, Select } from '@deephaven/components'; +import { Button } from '@deephaven/components'; import { vsChevronRight, vsMerge, vsKey } from '@deephaven/icons'; import Log from '@deephaven/log'; -import type { Table } from '@deephaven/jsapi-types'; +import { TableDropdown } from '@deephaven/jsapi-components'; +import type { FilterCondition, Table } from '@deephaven/jsapi-types'; import { TableUtils } from '@deephaven/jsapi-utils'; -import { Pending, PromiseUtils } from '@deephaven/utils'; +import { assertNotNull, Pending, PromiseUtils } from '@deephaven/utils'; import './IrisGridPartitionSelector.scss'; import { PartitionConfig, PartitionedGridModel } from './PartitionedGridModel'; const log = Log.module('IrisGridPartitionSelector'); -interface Items { - order: string[]; - data: { - [key: string]: unknown; - }; -} - interface IrisGridPartitionSelectorProps { model: PartitionedGridModel; partitionConfig: PartitionConfig; onChange: (partitionConfig: PartitionConfig) => void; } interface IrisGridPartitionSelectorState { - partitionColumnValues: readonly Items[]; - selectorValue: readonly string[]; isLoading: boolean; + + keysTable: Table | null; + + partitionTables: Table[] | null; + + /** The filters to apply to each partition table */ + partitionFilters: FilterCondition[][] | null; } class IrisGridPartitionSelector extends Component< IrisGridPartitionSelectorProps, IrisGridPartitionSelectorState > { - static defaultProps = { - onChange: (): void => undefined, - partitions: [], - }; - constructor(props: IrisGridPartitionSelectorProps) { super(props); @@ -46,49 +40,51 @@ class IrisGridPartitionSelector extends Component< const { model } = props; this.tableUtils = new TableUtils(model.dh); - this.table = null; - this.partitionTables = null; this.pending = new Pending(); this.state = { - selectorValue: model.partitionColumns.map(() => ''), - partitionColumnValues: model.partitionColumns.map(() => ({ - order: [], - data: {}, - })), + // We start be loading the partition tables, so we should be in a loading state isLoading: true, + + keysTable: null, + partitionFilters: null, + partitionTables: null, }; } async componentDidMount(): Promise { - const { model, partitionConfig } = this.props; + const { model } = this.props; - const table = await this.pending.add( - model.partitionKeysTable().then(keyTable => { - const sorts = model.partitionColumns.map(column => - column.sort().desc() - ); - keyTable.applySort(sorts); - return keyTable; - }), - t => t.close() - ); try { - this.partitionTables = await Promise.all( + const keysTable = await this.pending.add( + model.partitionKeysTable().then(keyTable => { + const sorts = model.partitionColumns.map(column => + column.sort().desc() + ); + keyTable.applySort(sorts); + return keyTable; + }), + t => t.close() + ); + + const partitionTables = await Promise.all( model.partitionColumns.map(async (_, i) => this.pending.add( - table.selectDistinct(model.partitionColumns.slice(0, i + 1)), + keysTable.selectDistinct(model.partitionColumns.slice(0, i + 1)), t => t.close() ) ) ); + + this.setState({ isLoading: false, keysTable, partitionTables }, () => { + this.updatePartitionFilters(); + }); } catch (e) { if (!PromiseUtils.isCanceled(e)) { - log.error('Unable to get partition tables', e); + // Just re-throw the error if it's not a cancel + throw e; } } - this.table = table; - this.updateConfig(partitionConfig); } componentDidUpdate(prevProps: IrisGridPartitionSelectorProps): void { @@ -97,24 +93,22 @@ class IrisGridPartitionSelector extends Component< const { partitionConfig } = this.props; if (prevConfig !== partitionConfig) { - this.updateConfig(partitionConfig); + this.updatePartitionFilters(); } } componentWillUnmount(): void { this.pending.cancel(); - this.table?.close(); - this.partitionTables?.forEach(table => table.close()); + + const { keysTable, partitionTables } = this.state; + keysTable?.close(); + partitionTables?.forEach(table => table.close()); } pending: Pending; tableUtils: TableUtils; - table: Table | null; - - partitionTables: Table[] | null; - handleKeyTableClick(): void { log.debug2('handleKeyTableClick'); @@ -137,24 +131,70 @@ class IrisGridPartitionSelector extends Component< this.sendUpdate(newPartitionConfig); } - handlePartitionSelect(index: number, partition: string): void { - const { model, partitionConfig } = this.props; - const { partitionColumnValues } = this.state; - - log.debug('handlePartitionSelect', index, partition, prevConfig); - - const newPartitions = - prevConfig.partitions.length === model.partitionColumns.length - ? [...prevConfig.partitions] - : Array(model.partitionColumns.length).fill(null); - newPartitions[index] = partitionColumnValues[index].data[partition]; - const newConfig: PartitionConfig = { - partitions: newPartitions, - mode: 'partition', - }; + /** + * Handles when a partition dropdown selection is changed. Will send an update with the new partition config + * @param index Index of the partition column that was changed + * @param selectedValue Selected value of the partition column + */ + async handlePartitionSelect( + index: number, + selectedValue: unknown + ): Promise { + const { model, partitionConfig: prevConfig } = this.props; + + log.debug('handlePartitionSelect', index, selectedValue, prevConfig); + + const newPartitions = [...prevConfig.partitions]; + newPartitions[index] = selectedValue; + + // If it's the last partition changed, we know it's already a valid value, just emit it + if (index === model.partitionColumns.length - 1) { + this.sendUpdate({ partitions: newPartitions, mode: 'partition' }); + return; + } + + const { keysTable } = this.state; + // Otherwise, we need to get the value from a filtered key table + assertNotNull(keysTable); + try { + this.setState({ isLoading: true }); + const t = await this.pending.add(keysTable.copy(), tCopy => + tCopy.close() + ); + + // Apply our partition filters, and just get the first value + const partitionFilters = newPartitions + .slice(0, index + 1) + .map((partition, i) => { + const partitionColumn = model.partitionColumns[i]; + return partitionColumn + .filter() + .eq( + this.tableUtils.makeFilterRawValue( + partitionColumn.type, + partition + ) + ); + }); + t.applyFilter(partitionFilters); + t.setViewport(0, 0, t.columns); + const data = await this.pending.add(t.getViewportData()); + t.close(); - this.setState({ isLoading: true }); - this.updateConfig(newConfig, index, true); + const newConfig: PartitionConfig = { + partitions: model.partitionColumns.map(column => + data.rows[0].get(column) + ), + mode: 'partition', + }; + this.sendUpdate(newConfig); + } catch (e) { + if (!PromiseUtils.isCanceled(e)) { + log.error('Unable to get partition tables', e); + } + } finally { + this.setState({ isLoading: false }); + } } sendUpdate(partitionConfig: PartitionConfig): void { @@ -164,12 +204,14 @@ class IrisGridPartitionSelector extends Component< onChange(partitionConfig); } - /** Calls model.displayString with a special character case */ - getDisplayValue(index: number, partition?: unknown): string { - const { model, partitionConfig } = this.props; + /** + * Calls model.displayString with a special character case + * @param index The index of the partition column to get the display value for + * @param value The partition value to get the display value for + */ + getDisplayValue(index: number, value: unknown): string { + const { model } = this.props; - const value = - partition === undefined ? partitionConfig.partitions[index] : partition; if (value == null || value === '') { return ''; } @@ -181,181 +223,75 @@ class IrisGridPartitionSelector extends Component< } /** - * Resolve invalid partitions and get new values for partition dropdowns - * @param index The index of the partition that was changed - * @param partitions Array of partitions containing updated values + * Update the filters on the partition dropdown tables */ - async updateConfig( - partitionConfig: PartitionConfig, - index = 0 - ): Promise { - if (partitionConfig.mode !== 'partition') { - this.clearDropdowns(partitionConfig); - return; - } - if (!Array.isArray(this.partitionTables)) { + updatePartitionFilters(): void { + const { partitionTables } = this.state; + assertNotNull(partitionTables); + + const { model, partitionConfig } = this.props; + const { mode, partitions } = partitionConfig; + log.debug('updatePartitionFilters', partitionConfig); + if (mode !== 'partition') { + // We only need to update the filters if the mode is `partitions` + // In the other modes, we disable the dropdowns anyway return; } - log.debug('partitionSelector update', index, partitionConfig); - const { model } = this.props; - // Cannot use columns from props since different index number will cause filters to fail - const { columns } = this.partitionTables[this.partitionTables.length - 1]; - if (!this.table) { - // this.table should be assigned in componentDidMount before updatePartitions is called - throw new Error('Table not initialized'); - } - - const partitionFilters = [...this.partitionTables[index].filter]; - const validPartitions = [...partitionConfig.partitions]; - let lastValidPartition = null; - - // Update partition filters - for (let i = index; i < partitionConfig.partitions.length; i += 1) { - // Await in loop necessary since each partition values list cascades from the previous iteration - /* eslint-disable no-await-in-loop */ - const partition = validPartitions[i]; - const partitionColumn = columns[i]; - - this.partitionTables[i].applyFilter([...partitionFilters]); - const partitionText = TableUtils.isCharType(partitionColumn.type) - ? this.getDisplayValue(i, partition) - : partition?.toString() ?? ''; - const partitionFilter = - partition === null - ? partitionColumn.filter().isNull() - : this.tableUtils.makeQuickFilterFromComponent( - partitionColumn, - partitionText - ); - if (partitionFilter !== null) { - partitionFilters.push(partitionFilter); - } else { - throw new Error( - `Failed to build partition ${partition} for column ${partitionColumn.name}` - ); - } - const t = await this.pending.add(this.table.copy(), tCopy => - tCopy.close() + if (partitions.length !== partitionTables.length) { + throw new Error( + `Invalid partition config set. Expected ${partitionTables.length} partitions, but got ${partitions.length}` ); - t.applyFilter(partitionFilters); - t.setViewport(0, 0, t.columns); - const data = await t.getViewportData(); - t.close(); - // Check if columns after index are defined - if (data.rows.length > 0) { - [lastValidPartition] = data.rows; - } else { - validPartitions[i] = lastValidPartition?.get(partitionColumn); - partitionFilters.pop(); - i -= 1; - } } - // Valid partitions found, update dropdown values - const newColumnValuesPromise = this.partitionTables?.map( - async (partitionTable, colIndex) => { - partitionTable.setViewport(0, partitionTable.size); - const data = await partitionTable.getViewportData(); - return data.rows.reduce( - (columnValues, row) => { - const column = columns[colIndex]; - const value = row.get(column); - const displayValue = model.displayString( - value, - column.type, - column.name - ); - return { - order: [...columnValues.order, displayValue], - data: { ...columnValues.data, [displayValue]: value }, - }; - }, - { order: [], data: {} } as Items - ); + // The filters are applied in order, so we need to build up the filters for each partition + const partitionFilters: FilterCondition[][] = []; + for (let i = 0; i < partitions.length; i += 1) { + if (i === 0) { + // There's no reason to ever filter the first table + partitionFilters.push([]); + } else { + const previousFilter = partitionFilters[i - 1]; + const previousPartition = partitions[i - 1]; + const previousColumn = model.partitionColumns[i - 1]; + const partitionFilter = [ + ...previousFilter, + previousColumn + .filter() + .eq( + this.tableUtils.makeFilterRawValue( + previousColumn.type, + previousPartition + ) + ), + ]; + partitionFilters.push(partitionFilter); } - ); - const newColumnValues = await Promise.all(newColumnValuesPromise); - this.setState( - { - partitionConfig: { partitions: validPartitions, mode: 'partition' }, - partitionColumnValues: newColumnValues, - selectorValue: columns.map((_, i) => - this.getDisplayValue(i, validPartitions[i] ?? '') - ), - isLoading: false, - }, - updateIrisGrid ? this.sendUpdate : undefined - ); - } - - async clearDropdowns(partitionConfig: PartitionConfig): Promise { - if (!Array.isArray(this.partitionTables)) { - return; } - log.debug('partitionSelector clearDropdowns', partitionConfig); - const { model } = this.props; - this.partitionTables.forEach(table => table.applyFilter([])); - const newColumnValues = Array(model.partitionColumns.length).fill({ - order: [], - data: {}, - } as Items); - this.partitionTables[0].setViewport(0, this.partitionTables[0].size); - const data = await this.partitionTables[0].getViewportData(); - const column = this.partitionTables[0].columns[0]; - newColumnValues[0] = data.rows.reduce( - (columnValues, row) => { - const value = row.get(column); - const displayValue = model.displayString( - value, - column.type, - column.name - ); - - return { - order: [...columnValues.order, displayValue], - data: { ...columnValues.data, [displayValue]: value }, - }; - }, - { order: [], data: {} } as Items - ); - this.setState({ - partitionConfig, - partitionColumnValues: newColumnValues, - selectorValue: Array(model.partitionColumns.length).fill(''), - }); + this.setState({ partitionFilters }); } render(): JSX.Element { - const { model } = this.props; - const { selectorValue, partitionConfig, partitionColumnValues, isLoading } = - this.state; + const { model, partitionConfig } = this.props; + const { isLoading, partitionFilters, partitionTables } = this.state; + + const { mode, partitions } = partitionConfig; const partitionSelectors = model.partitionColumns.map((column, index) => (
{column.name}
- + formatValue={value => this.getDisplayValue(index, value)} + /> {model.partitionColumns.length - 1 === index || ( )} diff --git a/packages/iris-grid/src/IrisGridPartitionedTableModel.ts b/packages/iris-grid/src/IrisGridPartitionedTableModel.ts index 0314744081..21aa0e1d56 100644 --- a/packages/iris-grid/src/IrisGridPartitionedTableModel.ts +++ b/packages/iris-grid/src/IrisGridPartitionedTableModel.ts @@ -8,10 +8,7 @@ import type { import { Formatter } from '@deephaven/jsapi-utils'; import { ColumnName } from './CommonTypes'; import EmptyIrisGridModel from './EmptyIrisGridModel'; -import { - PartitionConfig, - PartitionedGridModelProvider, -} from './PartitionedGridModel'; +import { PartitionedGridModelProvider } from './PartitionedGridModel'; class IrisGridPartitionedTableModel extends EmptyIrisGridModel @@ -66,10 +63,7 @@ class IrisGridPartitionedTableModel } async partitionTable(partitions: unknown[]): Promise
{ - // TODO: Copy is necessary for now since getTable returns memoized tables https://github.com/deephaven/web-client-ui/pull/1663#discussion_r1434984641 - return this.partitionedTable - .getTable(partitions) - .then(table => table.copy()); + return this.partitionedTable.getTable(partitions); } } diff --git a/packages/iris-grid/tsconfig.json b/packages/iris-grid/tsconfig.json index 3cf2d5dfd7..d2e5247dcd 100644 --- a/packages/iris-grid/tsconfig.json +++ b/packages/iris-grid/tsconfig.json @@ -11,6 +11,7 @@ { "path": "../components" }, { "path": "../filters" }, { "path": "../grid" }, + { "path": "../jsapi-components" }, { "path": "../jsapi-shim" }, { "path": "../jsapi-types" }, { "path": "../jsapi-utils" }, diff --git a/packages/jsapi-components/src/TableDropdown.tsx b/packages/jsapi-components/src/TableDropdown.tsx new file mode 100644 index 0000000000..6c051463c4 --- /dev/null +++ b/packages/jsapi-components/src/TableDropdown.tsx @@ -0,0 +1,121 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Option, Select } from '@deephaven/components'; +import { useApi } from '@deephaven/jsapi-bootstrap'; +import { + Column, + FilterCondition, + Table, + ViewportData, +} from '@deephaven/jsapi-types'; + +function defaultFormatValue(value: unknown): string { + return `${value}`; +} + +export type TableDropdownProps = { + /** Table to use as the source of data. Does not own the table, does not close it on unmount. */ + table?: Table; + + /** Column to read data from the table */ + column: Column; + + /** Triggered when the dropdown selection has changed */ + onChange: (value: unknown) => void; + + /** Filter to apply on the table */ + filter?: FilterCondition[]; + + /** The currently selected value */ + selectedValue?: unknown; + + /** Whether the control is disabled */ + disabled?: boolean; + + /** Class to apply to the select element */ + className?: string; + + /** Optional function to format the value for display */ + formatValue?: (value: unknown) => string; +}; + +/** + * Dropdown that displays the values of a column in a table. + */ +export function TableDropdown({ + column, + table, + filter = [], + onChange, + selectedValue, + disabled, + className, + formatValue = defaultFormatValue, +}: TableDropdownProps): JSX.Element { + const dh = useApi(); + const [values, setValues] = useState([]); + + useEffect(() => { + if (table == null) { + setValues([]); + return undefined; + } + + // Need to set a viewport on the table and start listening to get the values to populate the dropdown + table.applyFilter(filter); + const subscription = table.setViewport(0, Number.MAX_SAFE_INTEGER - 5, [ + column, + ]); + + subscription.addEventListener( + dh.Table.EVENT_UPDATED, + (event: CustomEvent) => { + const { detail } = event; + const newValues = detail.rows.map(row => row.get(column)); + setValues(newValues); + } + ); + + return () => { + subscription.close(); + }; + }, [column, dh, filter, table]); + + // If the selected value is undefined, add a placeholder item + const allValues = useMemo(() => { + if (selectedValue === undefined) { + return [undefined, ...values]; + } + return values; + }, [selectedValue, values]); + + // Since values could be anything, not just strings, track the selected index based on the current data + const selectedIndex = useMemo( + () => allValues.findIndex(value => value === selectedValue), + [selectedValue, allValues] + ); + + const handleChange = useCallback( + newSelectedIndex => { + onChange(allValues[newSelectedIndex]); + }, + [onChange, allValues] + ); + + return ( + + ); +} + +export default TableDropdown; diff --git a/packages/jsapi-components/src/index.ts b/packages/jsapi-components/src/index.ts index 206a7e7639..d8b233c9ff 100644 --- a/packages/jsapi-components/src/index.ts +++ b/packages/jsapi-components/src/index.ts @@ -3,6 +3,7 @@ export * from './HookTestUtils'; export { default as TableInput } from './TableInput'; export * from './RefreshTokenBootstrap'; export * from './RefreshTokenUtils'; +export * from './TableDropdown'; export { default as useBroadcastChannel } from './useBroadcastChannel'; export { default as useBroadcastLoginListener } from './useBroadcastLoginListener'; export * from './useCheckIfExistsValue'; From 67265313a2f4f8d7cf5de14392ea23bc90d13732 Mon Sep 17 00:00:00 2001 From: mikebender Date: Fri, 5 Jan 2024 11:33:59 -0500 Subject: [PATCH 25/33] Don't throw for getting properties --- packages/iris-grid/src/IrisGridProxyModel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/iris-grid/src/IrisGridProxyModel.ts b/packages/iris-grid/src/IrisGridProxyModel.ts index 5ce161f05c..9f97ec2a5b 100644 --- a/packages/iris-grid/src/IrisGridProxyModel.ts +++ b/packages/iris-grid/src/IrisGridProxyModel.ts @@ -467,7 +467,7 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { get partitionColumns(): readonly Column[] { if (!isPartitionedGridModelProvider(this.originalModel)) { - throw new Error('Partitions are not available'); + return []; } return this.originalModel.partitionColumns; } @@ -507,7 +507,7 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { !isPartitionedGridModelProvider(this.originalModel) || !this.originalModel.isPartitionRequired ) { - throw new Error('Partitions are not available'); + return null; } return this.partition; } From 4e0ae6c8c39a450639bf582f8d5732371fec014f Mon Sep 17 00:00:00 2001 From: mikebender Date: Fri, 5 Jan 2024 12:38:16 -0500 Subject: [PATCH 26/33] Some more cleanup - Use EMPTY_ARRAY instead of returning a new array each time from the EmptyGridModel - Reduces re-renders - Save the current viewport in IrisGridProxyModel, and apply it to the new model if the columns haven't changed - Clean up hydration/dehydration - Needed to dehydrate the value correctly --- packages/iris-grid/src/EmptyIrisGridModel.ts | 37 ++++++++--------- .../src/IrisGridPartitionSelector.tsx | 14 ++++++- packages/iris-grid/src/IrisGridProxyModel.ts | 27 ++++++++++--- packages/iris-grid/src/IrisGridTableModel.ts | 26 +++--------- packages/iris-grid/src/IrisGridUtils.ts | 40 +++++++++++++++++-- .../jsapi-components/src/TableDropdown.tsx | 38 ++++++++++++++---- 6 files changed, 125 insertions(+), 57 deletions(-) diff --git a/packages/iris-grid/src/EmptyIrisGridModel.ts b/packages/iris-grid/src/EmptyIrisGridModel.ts index 0d2b1b74fc..cc86d2a246 100644 --- a/packages/iris-grid/src/EmptyIrisGridModel.ts +++ b/packages/iris-grid/src/EmptyIrisGridModel.ts @@ -18,6 +18,7 @@ import { Table, } from '@deephaven/jsapi-types'; import { ColumnName, Formatter } from '@deephaven/jsapi-utils'; +import { EMPTY_ARRAY, EMPTY_MAP } from '@deephaven/utils'; import IrisGridModel from './IrisGridModel'; import ColumnHeaderGroup from './ColumnHeaderGroup'; import { @@ -52,7 +53,7 @@ class EmptyIrisGridModel extends IrisGridModel { } get columns(): readonly Column[] { - return []; + return EMPTY_ARRAY; } getColumnIndexByName(name: string): ModelIndex | undefined { @@ -60,19 +61,19 @@ class EmptyIrisGridModel extends IrisGridModel { } get initialMovedColumns(): readonly MoveOperation[] { - return []; + return EMPTY_ARRAY; } get initialMovedRows(): readonly MoveOperation[] { - return []; + return EMPTY_ARRAY; } get initialColumnHeaderGroups(): readonly ColumnHeaderGroup[] { - return []; + return EMPTY_ARRAY; } get groupedColumns(): readonly Column[] { - return []; + return EMPTY_ARRAY; } formatForCell(column: ModelIndex, row: ModelIndex): Format | undefined { @@ -84,7 +85,7 @@ class EmptyIrisGridModel extends IrisGridModel { } get filter(): readonly FilterCondition[] { - return []; + return EMPTY_ARRAY; } set filter(filter: readonly FilterCondition[]) { @@ -92,7 +93,7 @@ class EmptyIrisGridModel extends IrisGridModel { } get partition(): readonly unknown[] { - return []; + return EMPTY_ARRAY; } set partition(partition: readonly unknown[]) { @@ -100,7 +101,7 @@ class EmptyIrisGridModel extends IrisGridModel { } get partitionColumns(): readonly Column[] { - return []; + return EMPTY_ARRAY; } get formatter(): Formatter { @@ -119,16 +120,16 @@ class EmptyIrisGridModel extends IrisGridModel { return ''; } - get sort(): Sort[] { - return []; + get sort(): readonly Sort[] { + return EMPTY_ARRAY; } - set sort(sort: Sort[]) { + set sort(sort: readonly Sort[]) { // No-op } get customColumns(): readonly ColumnName[] { - return []; + return EMPTY_ARRAY; } set customColumns(customColumns: readonly ColumnName[]) { @@ -136,7 +137,7 @@ class EmptyIrisGridModel extends IrisGridModel { } get formatColumns(): readonly CustomColumn[] { - return []; + return EMPTY_ARRAY; } updateFrozenColumns(columns: readonly ColumnName[]): void { @@ -168,7 +169,7 @@ class EmptyIrisGridModel extends IrisGridModel { } get selectDistinctColumns(): readonly ColumnName[] { - return []; + return EMPTY_ARRAY; } set selectDistinctColumns(selectDistinctColumns: readonly ColumnName[]) { @@ -176,7 +177,7 @@ class EmptyIrisGridModel extends IrisGridModel { } get pendingDataMap(): PendingDataMap { - return new Map(); + return EMPTY_MAP; } set pendingDataMap(map: PendingDataMap) { @@ -192,7 +193,7 @@ class EmptyIrisGridModel extends IrisGridModel { } get pendingDataErrors(): PendingDataErrorMap { - return new Map(); + return EMPTY_MAP; } commitPending(): Promise { @@ -240,7 +241,7 @@ class EmptyIrisGridModel extends IrisGridModel { } get columnHeaderGroups(): readonly ColumnHeaderGroup[] { - return []; + return EMPTY_ARRAY; } set columnHeaderGroups(groups: readonly ColumnHeaderGroup[]) { @@ -248,7 +249,7 @@ class EmptyIrisGridModel extends IrisGridModel { } get columnHeaderGroupMap(): ReadonlyMap { - return new Map(); + return EMPTY_MAP; } getColumnHeaderParentGroup( diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.tsx b/packages/iris-grid/src/IrisGridPartitionSelector.tsx index 5a079fa092..f1b4b60d3f 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.tsx +++ b/packages/iris-grid/src/IrisGridPartitionSelector.tsx @@ -1,4 +1,5 @@ import React, { Component } from 'react'; +import memoizee from 'memoizee'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button } from '@deephaven/components'; import { vsChevronRight, vsMerge, vsKey } from '@deephaven/icons'; @@ -271,6 +272,15 @@ class IrisGridPartitionSelector extends Component< this.setState({ partitionFilters }); } + getCachedChangeCallback = memoizee( + (index: number) => (value: unknown) => + this.handlePartitionSelect(index, value) + ); + + getCachedFormatValueCallback = memoizee( + (index: number) => (value: unknown) => this.getDisplayValue(index, value) + ); + render(): JSX.Element { const { model, partitionConfig } = this.props; const { isLoading, partitionFilters, partitionTables } = this.state; @@ -285,12 +295,12 @@ class IrisGridPartitionSelector extends Component< table={partitionTables?.[index]} column={column} filter={partitionFilters?.[index]} - onChange={value => this.handlePartitionSelect(index, value)} + onChange={this.getCachedChangeCallback(index)} selectedValue={mode === 'partition' ? partitions[index] : undefined} disabled={ (index > 0 && partitionConfig.mode !== 'partition') || isLoading } - formatValue={value => this.getDisplayValue(index, value)} + formatValue={this.getCachedFormatValueCallback(index)} /> {model.partitionColumns.length - 1 === index || ( diff --git a/packages/iris-grid/src/IrisGridProxyModel.ts b/packages/iris-grid/src/IrisGridProxyModel.ts index 9f97ec2a5b..9bc0cf6c1d 100644 --- a/packages/iris-grid/src/IrisGridProxyModel.ts +++ b/packages/iris-grid/src/IrisGridProxyModel.ts @@ -91,6 +91,12 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { selectDistinct: ColumnName[]; + currentViewport?: { + top: number; + bottom: number; + columns?: Column[]; + }; + constructor( dh: DhType, table: Table | TreeTable | PartitionedTable, @@ -132,6 +138,7 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { log.debug('setModel', model); const oldModel = this.model; + const { columns: oldColumns } = oldModel; if (oldModel !== this.originalModel) { oldModel.close(); @@ -143,11 +150,17 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { this.addListeners(model); } - this.dispatchEvent( - new EventShimCustomEvent(IrisGridModel.EVENT.COLUMNS_CHANGED, { - detail: model.columns, - }) - ); + if (oldColumns !== model.columns) { + this.dispatchEvent( + new EventShimCustomEvent(IrisGridModel.EVENT.COLUMNS_CHANGED, { + detail: model.columns, + }) + ); + } else if (this.currentViewport != null) { + // If the columns haven't changed, the current viewport should still valid, and needs to be set on the new model + const { top, bottom, columns } = this.currentViewport; + model.setViewport(top, bottom, columns); + } if (isIrisGridTableModelTemplate(model)) { this.dispatchEvent( @@ -710,8 +723,10 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { isFilterable: IrisGridTableModel['isFilterable'] = (...args) => this.model.isFilterable(...args); - setViewport = (top: number, bottom: number, columns?: Column[]): void => + setViewport = (top: number, bottom: number, columns?: Column[]): void => { + this.currentViewport = { top, bottom, columns }; this.model.setViewport(top, bottom, columns); + }; snapshot: IrisGridModel['snapshot'] = (...args) => this.model.snapshot(...args); diff --git a/packages/iris-grid/src/IrisGridTableModel.ts b/packages/iris-grid/src/IrisGridTableModel.ts index 9d8ebbcd2b..614f959fbf 100644 --- a/packages/iris-grid/src/IrisGridTableModel.ts +++ b/packages/iris-grid/src/IrisGridTableModel.ts @@ -210,30 +210,16 @@ class IrisGridTableModel async partitionTable(partitions: unknown[]): Promise
{ log.debug('getting partition table for partitions', partitions); - const partitionFilters = []; + const partitionFilters: FilterCondition[] = []; for (let i = 0; i < this.partitionColumns.length; i += 1) { const partition = partitions[i]; const partitionColumn = this.partitionColumns[i]; - if ( - partition != null && - !(TableUtils.isCharType(partitionColumn.type) && partition === '') - ) { - const partitionText = TableUtils.isCharType(partitionColumn.type) - ? this.displayString( - partition, - partitionColumn.type, - partitionColumn.name - ) - : partition.toString(); - const partitionFilter = this.tableUtils.makeQuickFilterFromComponent( - partitionColumn, - partitionText - ); - if (partitionFilter !== null) { - partitionFilters.push(partitionFilter); - } - } + const partitionFilter = this.tableUtils.makeFilterRawValue( + partitionColumn.type, + partition + ); + partitionFilters.push(partitionColumn.filter().eq(partitionFilter)); } const t = await this.table.copy(); diff --git a/packages/iris-grid/src/IrisGridUtils.ts b/packages/iris-grid/src/IrisGridUtils.ts index bbe4aa5389..c601703a7a 100644 --- a/packages/iris-grid/src/IrisGridUtils.ts +++ b/packages/iris-grid/src/IrisGridUtils.ts @@ -100,6 +100,8 @@ export type DehydratedUserColumnWidth = [ColumnName, number]; export type DehydratedUserRowHeight = [number, number]; +export type DehydratedPartitionConfig = PartitionConfig; + /** @deprecated Use `DehydratedSort` instead */ export interface LegacyDehydratedSort { column: ModelIndex; @@ -143,7 +145,7 @@ export interface DehydratedIrisGridState { pendingDataMap: DehydratedPendingDataMap; frozenColumns: readonly ColumnName[]; columnHeaderGroups?: readonly ColumnGroup[]; - partitionConfig?: PartitionConfig; + partitionConfig?: DehydratedPartitionConfig; } export interface DehydratedIrisGridPanelStateV1 { @@ -1208,7 +1210,7 @@ class IrisGridUtils { children: item.children, color: item.color, })), - partitionConfig, + partitionConfig: this.dehydratePartitionConfig(columns, partitionConfig), }; } @@ -1304,7 +1306,7 @@ class IrisGridUtils { model, columnHeaderGroups ?? model.layoutHints?.columnGroups ?? [] ).groups, - partitionConfig, + partitionConfig: this.hydratePartitionConfig(columns, partitionConfig), }; } @@ -1498,6 +1500,38 @@ class IrisGridUtils { ); } + dehydratePartitionConfig( + columns: readonly Column[], + partitionConfig: PartitionConfig | undefined + ): PartitionConfig | undefined { + if (partitionConfig == null) { + return partitionConfig; + } + + return { + ...partitionConfig, + partitions: partitionConfig.partitions.map((partition, index) => + this.dehydrateValue(partition, columns[index].type) + ), + }; + } + + hydratePartitionConfig( + columns: readonly Column[], + partitionConfig: PartitionConfig | undefined + ): PartitionConfig | undefined { + if (partitionConfig == null) { + return partitionConfig; + } + + return { + ...partitionConfig, + partitions: partitionConfig.partitions.map((partition, index) => + this.hydrateValue(partition, columns[index].type) + ), + }; + } + /** * Dehydrates/serializes a value for storage. * @param value The value to dehydrate diff --git a/packages/jsapi-components/src/TableDropdown.tsx b/packages/jsapi-components/src/TableDropdown.tsx index 6c051463c4..a2f53e8854 100644 --- a/packages/jsapi-components/src/TableDropdown.tsx +++ b/packages/jsapi-components/src/TableDropdown.tsx @@ -7,6 +7,20 @@ import { Table, ViewportData, } from '@deephaven/jsapi-types'; +import { EMPTY_ARRAY } from '@deephaven/utils'; + +type JavaObject = { + equals: (other: unknown) => boolean; +}; + +function isJavaObject(value: unknown): value is JavaObject { + return ( + typeof value === 'object' && + value != null && + 'equals' in value && + typeof value.equals === 'function' + ); +} function defaultFormatValue(value: unknown): string { return `${value}`; @@ -23,7 +37,7 @@ export type TableDropdownProps = { onChange: (value: unknown) => void; /** Filter to apply on the table */ - filter?: FilterCondition[]; + filter?: readonly FilterCondition[]; /** The currently selected value */ selectedValue?: unknown; @@ -36,6 +50,9 @@ export type TableDropdownProps = { /** Optional function to format the value for display */ formatValue?: (value: unknown) => string; + + /** Maximum number of elements to load */ + maxSize?: number; }; /** @@ -44,12 +61,13 @@ export type TableDropdownProps = { export function TableDropdown({ column, table, - filter = [], + filter = EMPTY_ARRAY, onChange, selectedValue, disabled, className, formatValue = defaultFormatValue, + maxSize = 1000, }: TableDropdownProps): JSX.Element { const dh = useApi(); const [values, setValues] = useState([]); @@ -61,10 +79,8 @@ export function TableDropdown({ } // Need to set a viewport on the table and start listening to get the values to populate the dropdown - table.applyFilter(filter); - const subscription = table.setViewport(0, Number.MAX_SAFE_INTEGER - 5, [ - column, - ]); + table.applyFilter(filter as FilterCondition[]); + const subscription = table.setViewport(0, maxSize, [column]); subscription.addEventListener( dh.Table.EVENT_UPDATED, @@ -78,7 +94,7 @@ export function TableDropdown({ return () => { subscription.close(); }; - }, [column, dh, filter, table]); + }, [column, dh, filter, maxSize, table]); // If the selected value is undefined, add a placeholder item const allValues = useMemo(() => { @@ -90,7 +106,13 @@ export function TableDropdown({ // Since values could be anything, not just strings, track the selected index based on the current data const selectedIndex = useMemo( - () => allValues.findIndex(value => value === selectedValue), + // eslint-disable-next-line eqeqeq + () => + allValues.findIndex( + value => + value === selectedValue || + (isJavaObject(value) && value.equals(selectedValue)) + ), [selectedValue, allValues] ); From 19a1b35381831328bf8c3e1bef12fd6e607f0363 Mon Sep 17 00:00:00 2001 From: mikebender Date: Fri, 5 Jan 2024 12:47:51 -0500 Subject: [PATCH 27/33] Fix bad import --- packages/grid/src/DataBarGridModel.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/grid/src/DataBarGridModel.ts b/packages/grid/src/DataBarGridModel.ts index 1e96c909eb..0bad337fe7 100644 --- a/packages/grid/src/DataBarGridModel.ts +++ b/packages/grid/src/DataBarGridModel.ts @@ -1,7 +1,6 @@ -import { GridThemeType } from '.'; import { ModelIndex } from './GridMetrics'; import GridModel from './GridModel'; -import { GridColor } from './GridTheme'; +import { GridColor, type GridTheme as GridThemeType } from './GridTheme'; export type Marker = { value: number; color: string }; export type AxisOption = 'proportional' | 'middle' | 'directional'; From a9e834b337fec38afcca06291d5112208bc663c2 Mon Sep 17 00:00:00 2001 From: mikebender Date: Fri, 5 Jan 2024 13:46:54 -0500 Subject: [PATCH 28/33] Fix failing test --- packages/iris-grid/src/IrisGridTableModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/iris-grid/src/IrisGridTableModel.ts b/packages/iris-grid/src/IrisGridTableModel.ts index 614f959fbf..6089446201 100644 --- a/packages/iris-grid/src/IrisGridTableModel.ts +++ b/packages/iris-grid/src/IrisGridTableModel.ts @@ -13,7 +13,7 @@ import type { ValueTypeUnion, } from '@deephaven/jsapi-types'; import Log from '@deephaven/log'; -import { Formatter, TableUtils } from '@deephaven/jsapi-utils'; +import { Formatter } from '@deephaven/jsapi-utils'; import { EventShimCustomEvent, PromiseUtils, From dd761a198b8e32e98fa4f0371c201089956ec3fe Mon Sep 17 00:00:00 2001 From: mikebender Date: Fri, 5 Jan 2024 14:14:41 -0500 Subject: [PATCH 29/33] Fix rehydration and char partitions - Return the partitioned tables columns instead of no columns, so rehydration will succeed - Fix char type partitions - needed to create the raw filter value better --- packages/iris-grid/src/IrisGridPartitionedTableModel.ts | 4 ++++ packages/jsapi-utils/src/TableUtils.ts | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/packages/iris-grid/src/IrisGridPartitionedTableModel.ts b/packages/iris-grid/src/IrisGridPartitionedTableModel.ts index 21aa0e1d56..c9164c681e 100644 --- a/packages/iris-grid/src/IrisGridPartitionedTableModel.ts +++ b/packages/iris-grid/src/IrisGridPartitionedTableModel.ts @@ -50,6 +50,10 @@ class IrisGridPartitionedTableModel this.partitionedTable.close(); } + get columns(): readonly Column[] { + return this.partitionedTable.columns; + } + get partitionColumns(): readonly Column[] { return this.partitionedTable.keyColumns; } diff --git a/packages/jsapi-utils/src/TableUtils.ts b/packages/jsapi-utils/src/TableUtils.ts index c1df46b0b8..c439eccef3 100644 --- a/packages/jsapi-utils/src/TableUtils.ts +++ b/packages/jsapi-utils/src/TableUtils.ts @@ -1788,6 +1788,11 @@ export class TableUtils { */ makeFilterRawValue(columnType: string, rawValue: unknown): FilterValue { const { dh } = this; + if (TableUtils.isCharType(columnType)) { + return dh.FilterValue.ofString( + typeof rawValue === 'number' ? String.fromCharCode(rawValue) : rawValue + ); + } if (TableUtils.isTextType(columnType)) { return dh.FilterValue.ofString(rawValue); } From f4cba3aff4f17758e4047185ddf56a65e477b7c9 Mon Sep 17 00:00:00 2001 From: mikebender Date: Mon, 8 Jan 2024 09:40:52 -0500 Subject: [PATCH 30/33] Fix review comments - Add async to methods for consistency --- packages/iris-grid/src/IrisGridPartitionedTableModel.ts | 4 ++-- packages/iris-grid/src/IrisGridTableModel.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/iris-grid/src/IrisGridPartitionedTableModel.ts b/packages/iris-grid/src/IrisGridPartitionedTableModel.ts index c9164c681e..57cc1ef7ac 100644 --- a/packages/iris-grid/src/IrisGridPartitionedTableModel.ts +++ b/packages/iris-grid/src/IrisGridPartitionedTableModel.ts @@ -58,11 +58,11 @@ class IrisGridPartitionedTableModel return this.partitionedTable.keyColumns; } - partitionKeysTable(): Promise
{ + async partitionKeysTable(): Promise
{ return this.partitionedTable.getKeyTable(); } - partitionMergedTable(): Promise
{ + async partitionMergedTable(): Promise
{ return this.partitionedTable.getMergedTable(); } diff --git a/packages/iris-grid/src/IrisGridTableModel.ts b/packages/iris-grid/src/IrisGridTableModel.ts index 6089446201..80be071b2d 100644 --- a/packages/iris-grid/src/IrisGridTableModel.ts +++ b/packages/iris-grid/src/IrisGridTableModel.ts @@ -197,7 +197,7 @@ class IrisGridTableModel return this.getCachedPartitionColumns(this.columns); } - partitionKeysTable(): Promise
{ + async partitionKeysTable(): Promise
{ return this.valuesTable(this.partitionColumns); } From 02542dd8e7a94172a7240f93615f8e816b3fd638 Mon Sep 17 00:00:00 2001 From: mikebender Date: Tue, 16 Jan 2024 15:24:11 -0500 Subject: [PATCH 31/33] Fix the columns used in the partition selector - Was referencing the columns from the original table instead of the actual table - Was causing a mismatch when fetching the viewport, where it would just stall instead of throwing an error --- .../src/IrisGridPartitionSelector.tsx | 34 ++++++++++++------- packages/iris-grid/src/IrisGridUtils.ts | 31 ++++++++++++----- .../jsapi-components/src/TableDropdown.tsx | 9 ++--- 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.tsx b/packages/iris-grid/src/IrisGridPartitionSelector.tsx index f1b4b60d3f..4b1dc380e8 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.tsx +++ b/packages/iris-grid/src/IrisGridPartitionSelector.tsx @@ -77,8 +77,12 @@ class IrisGridPartitionSelector extends Component< ) ); - this.setState({ isLoading: false, keysTable, partitionTables }, () => { - this.updatePartitionFilters(); + const partitionFilters = this.getPartitionFilters(partitionTables); + this.setState({ + isLoading: false, + keysTable, + partitionFilters, + partitionTables, }); } catch (e) { if (!PromiseUtils.isCanceled(e)) { @@ -167,7 +171,7 @@ class IrisGridPartitionSelector extends Component< const partitionFilters = newPartitions .slice(0, index + 1) .map((partition, i) => { - const partitionColumn = model.partitionColumns[i]; + const partitionColumn = t.columns[i]; return partitionColumn .filter() .eq( @@ -180,14 +184,11 @@ class IrisGridPartitionSelector extends Component< t.applyFilter(partitionFilters); t.setViewport(0, 0, t.columns); const data = await this.pending.add(t.getViewportData()); - t.close(); - const newConfig: PartitionConfig = { - partitions: model.partitionColumns.map(column => - data.rows[0].get(column) - ), + partitions: t.columns.map(column => data.rows[0].get(column)), mode: 'partition', }; + t.close(); this.sendUpdate(newConfig); } catch (e) { if (!PromiseUtils.isCanceled(e)) { @@ -230,8 +231,8 @@ class IrisGridPartitionSelector extends Component< const { partitionTables } = this.state; assertNotNull(partitionTables); - const { model, partitionConfig } = this.props; - const { mode, partitions } = partitionConfig; + const { partitionConfig } = this.props; + const { mode } = partitionConfig; log.debug('updatePartitionFilters', partitionConfig); if (mode !== 'partition') { // We only need to update the filters if the mode is `partitions` @@ -239,6 +240,15 @@ class IrisGridPartitionSelector extends Component< return; } + const partitionFilters = this.getPartitionFilters(partitionTables); + this.setState({ partitionFilters }); + } + + getPartitionFilters(partitionTables: Table[]): FilterCondition[][] { + const { model, partitionConfig } = this.props; + const { partitions } = partitionConfig; + log.debug('getPartitionFilters', partitionConfig); + if (partitions.length !== partitionTables.length) { throw new Error( `Invalid partition config set. Expected ${partitionTables.length} partitions, but got ${partitions.length}` @@ -269,7 +279,7 @@ class IrisGridPartitionSelector extends Component< partitionFilters.push(partitionFilter); } } - this.setState({ partitionFilters }); + return partitionFilters; } getCachedChangeCallback = memoizee( @@ -293,7 +303,7 @@ class IrisGridPartitionSelector extends Component< - this.dehydrateValue(partition, columns[index].type) + this.dehydrateValue(partition, partitionColumns[index].type) ), }; } hydratePartitionConfig( - columns: readonly Column[], + partitionColumns: readonly Column[], partitionConfig: PartitionConfig | undefined ): PartitionConfig | undefined { if (partitionConfig == null) { @@ -1527,7 +1542,7 @@ class IrisGridUtils { return { ...partitionConfig, partitions: partitionConfig.partitions.map((partition, index) => - this.hydrateValue(partition, columns[index].type) + this.hydrateValue(partition, partitionColumns[index].type) ), }; } diff --git a/packages/jsapi-components/src/TableDropdown.tsx b/packages/jsapi-components/src/TableDropdown.tsx index a2f53e8854..67cbe0a390 100644 --- a/packages/jsapi-components/src/TableDropdown.tsx +++ b/packages/jsapi-components/src/TableDropdown.tsx @@ -30,8 +30,8 @@ export type TableDropdownProps = { /** Table to use as the source of data. Does not own the table, does not close it on unmount. */ table?: Table; - /** Column to read data from the table */ - column: Column; + /** Column to read data from the table. Defaults to the first column in the table if it's not provided. */ + column?: Column; /** Triggered when the dropdown selection has changed */ onChange: (value: unknown) => void; @@ -78,15 +78,16 @@ export function TableDropdown({ return undefined; } + const tableColumn = column ?? table.columns[0]; // Need to set a viewport on the table and start listening to get the values to populate the dropdown table.applyFilter(filter as FilterCondition[]); - const subscription = table.setViewport(0, maxSize, [column]); + const subscription = table.setViewport(0, maxSize, [tableColumn]); subscription.addEventListener( dh.Table.EVENT_UPDATED, (event: CustomEvent) => { const { detail } = event; - const newValues = detail.rows.map(row => row.get(column)); + const newValues = detail.rows.map(row => row.get(tableColumn)); setValues(newValues); } ); From 5ce6ff907b81289518265f9a3a28ea1b0cd5ec6b Mon Sep 17 00:00:00 2001 From: mikebender Date: Thu, 18 Jan 2024 11:42:27 -0500 Subject: [PATCH 32/33] Fix issues found in review with ticking partition tables - Wait for a key to be added to the keys table if one does not initially exist - If the reloaded partition config is invalid, will reload the initial partition config --- packages/iris-grid/src/IrisGrid.tsx | 133 ++++++++++++------ .../src/IrisGridPartitionedTableModel.ts | 18 ++- .../iris-grid/src/MissingPartitionError.ts | 11 ++ 3 files changed, 115 insertions(+), 47 deletions(-) create mode 100644 packages/iris-grid/src/MissingPartitionError.ts diff --git a/packages/iris-grid/src/IrisGrid.tsx b/packages/iris-grid/src/IrisGrid.tsx index 8d5b2d5ead..e8a392ba39 100644 --- a/packages/iris-grid/src/IrisGrid.tsx +++ b/packages/iris-grid/src/IrisGrid.tsx @@ -72,6 +72,7 @@ import type { Sort, Table, TableViewportSubscription, + ViewportData, } from '@deephaven/jsapi-types'; import { DateUtils, @@ -195,6 +196,7 @@ import { } from './CommonTypes'; import ColumnHeaderGroup from './ColumnHeaderGroup'; import { IrisGridThemeContext } from './IrisGridThemeProvider'; +import { isMissingPartitionError } from './MissingPartitionError'; const log = Log.module('IrisGrid'); @@ -855,15 +857,7 @@ export class IrisGrid extends Component { componentDidMount(): void { const { model } = this.props; - try { - if (isPartitionedGridModel(model) && model.isPartitionRequired) { - this.loadPartitionsTable(model); - } else { - this.initState(); - } - } catch (error) { - this.handleTableLoadError(error); - } + this.initState(); this.startListening(model); } @@ -1909,6 +1903,19 @@ export class IrisGrid extends Component { } initState(): void { + const { model } = this.props; + try { + if (isPartitionedGridModel(model) && model.isPartitionRequired) { + this.loadPartitionsTable(model); + } else { + this.loadTableState(); + } + } catch (error) { + this.handleTableLoadError(error); + } + } + + loadTableState(): void { const { applyInputFiltersOnInit, inputFilters, @@ -1950,43 +1957,70 @@ export class IrisGrid extends Component { } async loadPartitionsTable(model: PartitionedGridModel): Promise { + try { + const partitionConfig = await this.getInitialPartitionConfig(model); + this.setState( + { isSelectingPartition: true, partitionConfig }, + this.loadTableState + ); + } catch (error) { + if (!PromiseUtils.isCanceled(error)) { + this.handleTableLoadError(error); + } + } + } + + /** + * Gets the initial partition config for the currently set model. + * Sorts the key table and gets the first key. + * If the table is ticking, it will wait for the first tick. + */ + async getInitialPartitionConfig( + model: PartitionedGridModel + ): Promise { const { partitionConfig } = this.state; if (partitionConfig !== undefined) { - this.setState({ isSelectingPartition: true }, this.initState); - return; + // User already has a partition selected, just use that + return partitionConfig; } - try { - const keyTable = await this.pending.add( - model.partitionKeysTable(), - resolved => resolved.close() - ); - - const sorts = keyTable.columns.map(column => column.sort().desc()); - keyTable.applySort(sorts); - keyTable.setViewport(0, 0); - - const data = await this.pending.add(keyTable.getViewportData()); - if (data.rows.length > 0) { - const row = data.rows[0]; - const values = keyTable.columns.map(column => row.get(column)); - const newPartition: PartitionConfig = { - partitions: values, - mode: 'partition', - }; + const keyTable = await this.pending.add( + model.partitionKeysTable(), + resolved => resolved.close() + ); + const { dh } = model; - this.setState( - { isSelectingPartition: true, partitionConfig: newPartition }, - this.initState - ); - } else { - log.info('Table does not have any data'); - this.setState({ isSelectingPartition: false }, this.initState); - } - keyTable.close(); - } catch (error) { - this.handleTableLoadError(error); - } + const sorts = keyTable.columns.map(column => column.sort().desc()); + keyTable.applySort(sorts); + keyTable.setViewport(0, 0); + + return new Promise((resolve, reject) => { + // We want to wait for the first UPDATED event instead of just getting viewport data here + // It's possible that the key table does not have any rows of data yet, so just wait until it does have one + keyTable.addEventListener( + dh.Table.EVENT_UPDATED, + (event: CustomEvent) => { + try { + const { detail: data } = event; + if (data.rows.length === 0) { + // Table is empty, wait for the next updated event + return; + } + const row = data.rows[0]; + const values = keyTable.columns.map(column => row.get(column)); + const newPartition: PartitionConfig = { + partitions: values, + mode: 'partition', + }; + keyTable.close(); + resolve(newPartition); + } catch (e) { + keyTable.close(); + reject(e); + } + } + ); + }); } copyCell( @@ -2881,16 +2915,23 @@ export class IrisGrid extends Component { } handleRequestFailed(event: Event): void { - const customEvent = event as CustomEvent; - log.error('request failed:', customEvent.detail); + const { detail: error } = event as CustomEvent; + log.error('request failed:', error); this.stopLoading(); - if (this.canRollback()) { + const { partitionConfig } = this.state; + if (isMissingPartitionError(error) && partitionConfig != null) { + // We'll try loading the initial partition again + this.startLoading('Reloading partition...', true); + this.setState({ partitionConfig: undefined }, () => { + this.initState(); + }); + } else if (this.canRollback()) { this.startLoading('Rolling back changes...', true); this.rollback(); } else { log.error('Table failed and unable to rollback'); const { onError } = this.props; - onError(new Error(`Error displaying table: ${customEvent.detail}`)); + onError(new Error(`Error displaying table: ${error}`)); } } @@ -3172,7 +3213,7 @@ export class IrisGrid extends Component { this.stopLoading(); this.grid?.forceUpdate(); } else { - this.initState(); + this.loadTableState(); } } diff --git a/packages/iris-grid/src/IrisGridPartitionedTableModel.ts b/packages/iris-grid/src/IrisGridPartitionedTableModel.ts index 57cc1ef7ac..2eb88a34bc 100644 --- a/packages/iris-grid/src/IrisGridPartitionedTableModel.ts +++ b/packages/iris-grid/src/IrisGridPartitionedTableModel.ts @@ -8,6 +8,9 @@ import type { import { Formatter } from '@deephaven/jsapi-utils'; import { ColumnName } from './CommonTypes'; import EmptyIrisGridModel from './EmptyIrisGridModel'; +import MissingPartitionError, { + isMissingPartitionError, +} from './MissingPartitionError'; import { PartitionedGridModelProvider } from './PartitionedGridModel'; class IrisGridPartitionedTableModel @@ -67,7 +70,20 @@ class IrisGridPartitionedTableModel } async partitionTable(partitions: unknown[]): Promise
{ - return this.partitionedTable.getTable(partitions); + try { + const table = await this.partitionedTable.getTable(partitions); + if (table == null) { + // TODO: Will be unnecessary with https://github.com/deephaven/deephaven-core/pull/5050 + throw new MissingPartitionError('Partition not found'); + } + return table; + } catch (e) { + if (!isMissingPartitionError(e)) { + throw new MissingPartitionError('Unable to retrieve partition'); + } else { + throw e; + } + } } } diff --git a/packages/iris-grid/src/MissingPartitionError.ts b/packages/iris-grid/src/MissingPartitionError.ts new file mode 100644 index 0000000000..2c9c17b61d --- /dev/null +++ b/packages/iris-grid/src/MissingPartitionError.ts @@ -0,0 +1,11 @@ +class MissingPartitionError extends Error { + isMissingPartitionError = true; +} + +export function isMissingPartitionError( + err: unknown +): err is MissingPartitionError { + return (err as MissingPartitionError)?.isMissingPartitionError === true; +} + +export default MissingPartitionError; From 681bc2433a6acf7c7774e84a7290ceaa61aa6e0a Mon Sep 17 00:00:00 2001 From: Don McKenzie Date: Thu, 18 Jan 2024 16:48:04 -0500 Subject: [PATCH 33/33] adjust color variables --- packages/iris-grid/src/IrisGridPartitionSelector.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/iris-grid/src/IrisGridPartitionSelector.scss b/packages/iris-grid/src/IrisGridPartitionSelector.scss index 1af044a9f5..ce46a1a98d 100644 --- a/packages/iris-grid/src/IrisGridPartitionSelector.scss +++ b/packages/iris-grid/src/IrisGridPartitionSelector.scss @@ -2,7 +2,7 @@ .iris-grid-partition-selector { display: flex; flex-wrap: wrap; - background: $gray-850; + background: var(--dh-color-surface-bg); align-items: center; padding: $spacer-2; gap: $spacer-2; @@ -18,7 +18,7 @@ } .partition-button-group { display: flex; - border: 1px $gray-900; + border: 1px var(--dh-color-hr); border-style: none solid; padding: 0 $spacer-2; gap: $spacer-2;