@finos/legend-data-cube 0.1.31 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/lib/components/DataCubeStatusBar.d.ts.map +1 -1
  2. package/lib/components/DataCubeStatusBar.js +4 -2
  3. package/lib/components/DataCubeStatusBar.js.map +1 -1
  4. package/lib/components/view/editor/DataCubeEditor.d.ts.map +1 -1
  5. package/lib/components/view/editor/DataCubeEditor.js +5 -1
  6. package/lib/components/view/editor/DataCubeEditor.js.map +1 -1
  7. package/lib/components/view/grid/DataCubeMultidimensionalGrid.d.ts.map +1 -1
  8. package/lib/components/view/grid/DataCubeMultidimensionalGrid.js +40 -2
  9. package/lib/components/view/grid/DataCubeMultidimensionalGrid.js.map +1 -1
  10. package/lib/index.css +1 -1
  11. package/lib/package.json +1 -1
  12. package/lib/stores/DataCubeAPI.d.ts +6 -0
  13. package/lib/stores/DataCubeAPI.d.ts.map +1 -1
  14. package/lib/stores/DataCubeAPI.js +4 -0
  15. package/lib/stores/DataCubeAPI.js.map +1 -1
  16. package/lib/stores/core/DataCubeQueryBuilder.d.ts +12 -0
  17. package/lib/stores/core/DataCubeQueryBuilder.d.ts.map +1 -1
  18. package/lib/stores/core/DataCubeQueryBuilder.js +141 -4
  19. package/lib/stores/core/DataCubeQueryBuilder.js.map +1 -1
  20. package/lib/stores/core/DataCubeQueryBuilderUtils.d.ts +2 -0
  21. package/lib/stores/core/DataCubeQueryBuilderUtils.d.ts.map +1 -1
  22. package/lib/stores/core/DataCubeQueryBuilderUtils.js +18 -1
  23. package/lib/stores/core/DataCubeQueryBuilderUtils.js.map +1 -1
  24. package/lib/stores/core/DataCubeQueryEngine.d.ts +5 -0
  25. package/lib/stores/core/DataCubeQueryEngine.d.ts.map +1 -1
  26. package/lib/stores/core/DataCubeQueryEngine.js +8 -0
  27. package/lib/stores/core/DataCubeQueryEngine.js.map +1 -1
  28. package/lib/stores/core/DataCubeSnapshot.d.ts +2 -0
  29. package/lib/stores/core/DataCubeSnapshot.d.ts.map +1 -1
  30. package/lib/stores/core/DataCubeSnapshot.js.map +1 -1
  31. package/lib/stores/core/DataCubeSnapshotBuilder.d.ts.map +1 -1
  32. package/lib/stores/core/DataCubeSnapshotBuilder.js +2 -2
  33. package/lib/stores/core/DataCubeSnapshotBuilder.js.map +1 -1
  34. package/lib/stores/core/DataCubeSnapshotBuilderUtils.d.ts +2 -1
  35. package/lib/stores/core/DataCubeSnapshotBuilderUtils.d.ts.map +1 -1
  36. package/lib/stores/core/DataCubeSnapshotBuilderUtils.js +3 -3
  37. package/lib/stores/core/DataCubeSnapshotBuilderUtils.js.map +1 -1
  38. package/lib/stores/view/editor/DataCubeEditorDimensionsPanelState.d.ts.map +1 -1
  39. package/lib/stores/view/editor/DataCubeEditorDimensionsPanelState.js +7 -4
  40. package/lib/stores/view/editor/DataCubeEditorDimensionsPanelState.js.map +1 -1
  41. package/lib/stores/view/grid/DataCubeGridClientEngine.d.ts +163 -1
  42. package/lib/stores/view/grid/DataCubeGridClientEngine.d.ts.map +1 -1
  43. package/lib/stores/view/grid/DataCubeGridClientEngine.js +134 -2
  44. package/lib/stores/view/grid/DataCubeGridClientEngine.js.map +1 -1
  45. package/lib/stores/view/grid/DataCubeGridConfigurationBuilder.d.ts +155 -1
  46. package/lib/stores/view/grid/DataCubeGridConfigurationBuilder.d.ts.map +1 -1
  47. package/lib/stores/view/grid/DataCubeGridConfigurationBuilder.js +170 -2
  48. package/lib/stores/view/grid/DataCubeGridConfigurationBuilder.js.map +1 -1
  49. package/lib/stores/view/grid/DataCubeGridControllerState.d.ts.map +1 -1
  50. package/lib/stores/view/grid/DataCubeGridControllerState.js +5 -3
  51. package/lib/stores/view/grid/DataCubeGridControllerState.js.map +1 -1
  52. package/lib/stores/view/grid/DataCubeGridDimensionalTree.d.ts +48 -0
  53. package/lib/stores/view/grid/DataCubeGridDimensionalTree.d.ts.map +1 -0
  54. package/lib/stores/view/grid/DataCubeGridDimensionalTree.js +168 -0
  55. package/lib/stores/view/grid/DataCubeGridDimensionalTree.js.map +1 -0
  56. package/lib/stores/view/grid/DataCubeGridMenuBuilder.d.ts +1 -0
  57. package/lib/stores/view/grid/DataCubeGridMenuBuilder.d.ts.map +1 -1
  58. package/lib/stores/view/grid/DataCubeGridMenuBuilder.js +197 -0
  59. package/lib/stores/view/grid/DataCubeGridMenuBuilder.js.map +1 -1
  60. package/lib/stores/view/grid/DataCubeGridQueryBuilder.d.ts +2 -0
  61. package/lib/stores/view/grid/DataCubeGridQueryBuilder.d.ts.map +1 -1
  62. package/lib/stores/view/grid/DataCubeGridQueryBuilder.js +7 -1
  63. package/lib/stores/view/grid/DataCubeGridQueryBuilder.js.map +1 -1
  64. package/lib/stores/view/grid/DataCubeGridState.d.ts +4 -1
  65. package/lib/stores/view/grid/DataCubeGridState.d.ts.map +1 -1
  66. package/lib/stores/view/grid/DataCubeGridState.js +24 -2
  67. package/lib/stores/view/grid/DataCubeGridState.js.map +1 -1
  68. package/package.json +4 -4
  69. package/src/components/DataCubeStatusBar.tsx +9 -2
  70. package/src/components/view/editor/DataCubeEditor.tsx +8 -1
  71. package/src/components/view/grid/DataCubeMultidimensionalGrid.tsx +63 -1
  72. package/src/stores/DataCubeAPI.ts +10 -0
  73. package/src/stores/core/DataCubeQueryBuilder.ts +256 -2
  74. package/src/stores/core/DataCubeQueryBuilderUtils.ts +22 -0
  75. package/src/stores/core/DataCubeQueryEngine.ts +9 -0
  76. package/src/stores/core/DataCubeSnapshot.ts +2 -0
  77. package/src/stores/core/DataCubeSnapshotBuilder.ts +2 -0
  78. package/src/stores/core/DataCubeSnapshotBuilderUtils.ts +6 -2
  79. package/src/stores/view/editor/DataCubeEditorDimensionsPanelState.ts +8 -7
  80. package/src/stores/view/grid/DataCubeGridClientEngine.ts +252 -5
  81. package/src/stores/view/grid/DataCubeGridConfigurationBuilder.tsx +244 -3
  82. package/src/stores/view/grid/DataCubeGridControllerState.ts +10 -2
  83. package/src/stores/view/grid/DataCubeGridDimensionalTree.ts +268 -0
  84. package/src/stores/view/grid/DataCubeGridMenuBuilder.tsx +240 -0
  85. package/src/stores/view/grid/DataCubeGridQueryBuilder.ts +22 -1
  86. package/src/stores/view/grid/DataCubeGridState.ts +52 -2
  87. package/tsconfig.json +1 -0
@@ -226,13 +226,14 @@ export class DataCubeEditorDimensionsPanelState
226
226
  name: dimension.name,
227
227
  columns: dimension.columns.map((column) => column.name),
228
228
  }));
229
+ newSnapshot.data.configuration = {
230
+ ...newSnapshot.data.configuration,
231
+ dimensions: DataCubeDimensionsConfiguration.serialization.toJson(
232
+ dimensionsConfiguration,
233
+ ),
234
+ };
235
+ } else {
236
+ newSnapshot = baseSnapshot;
229
237
  }
230
-
231
- newSnapshot.data.configuration = {
232
- ...newSnapshot.data.configuration,
233
- dimensions: DataCubeDimensionsConfiguration.serialization.toJson(
234
- dimensionsConfiguration,
235
- ),
236
- };
237
238
  }
238
239
  }
@@ -25,6 +25,7 @@ import {
25
25
  IllegalStateError,
26
26
  assertErrorThrown,
27
27
  guaranteeNonNullable,
28
+ guaranteeType,
28
29
  hashObject,
29
30
  isBoolean,
30
31
  isNonNullable,
@@ -35,11 +36,13 @@ import {
35
36
  PRIMITIVE_TYPE,
36
37
  PureClientVersion,
37
38
  type TabularDataSet,
39
+ type TDSRow,
38
40
  } from '@finos/legend-graph';
39
41
  import { makeObservable, observable, runInAction } from 'mobx';
40
- import type {
42
+ import {
41
43
  DataCubeConfiguration,
42
- DataCubeConfigurationColorKey,
44
+ type DataCubeConfigurationColorKey,
45
+ type DataCubeDimensionConfiguration,
43
46
  } from '../../core/model/DataCubeConfiguration.js';
44
47
  import { type DataCubeSnapshot } from '../../core/DataCubeSnapshot.js';
45
48
  import { _findCol, _sortByColName } from '../../core/model/DataCubeColumn.js';
@@ -47,7 +50,10 @@ import { isPivotResultColumnName } from '../../core/DataCubeQueryEngine.js';
47
50
  import { buildSnapshotFromGridState } from './DataCubeGridSnapshotBuilder.js';
48
51
  import { AlertType } from '../../services/DataCubeAlertService.js';
49
52
  import type { DataCubeViewState } from '../DataCubeViewState.js';
50
- import { buildGridDataFetchExecutableQuery } from './DataCubeGridQueryBuilder.js';
53
+ import {
54
+ buildDimensionalGridDataFetchExecutableQuery,
55
+ buildGridDataFetchExecutableQuery,
56
+ } from './DataCubeGridQueryBuilder.js';
51
57
  import { DataCubeSettingKey } from '../../../__lib__/DataCubeSetting.js';
52
58
  import { DEFAULT_ALERT_WINDOW_CONFIG } from '../../services/DataCubeLayoutService.js';
53
59
  import {
@@ -57,8 +63,22 @@ import {
57
63
  import { _lambda } from '../../core/DataCubeQueryBuilderUtils.js';
58
64
  import { sum } from 'mathjs';
59
65
  import { DataCubeEvent } from '../../../__lib__/DataCubeEvent.js';
60
-
61
- type DataCubeGridClientCellValue = string | number | boolean | null | undefined;
66
+ import { generateColumnDefsForDimensions } from './DataCubeGridConfigurationBuilder.js';
67
+ import {
68
+ DataCubeDimensionalMetadata,
69
+ generateDimensionalPaths,
70
+ type DataCubeDimensionalNode,
71
+ type DataCubeDimensionalGroupByNode,
72
+ DIMENSIONAL_L0_COLUMN,
73
+ } from './DataCubeGridDimensionalTree.js';
74
+
75
+ type DataCubeGridClientCellValue =
76
+ | string
77
+ | number
78
+ | boolean
79
+ | Map<string, DataCubeDimensionalMetadata>
80
+ | null
81
+ | undefined;
62
82
  type DataCubeGridClientRowData = {
63
83
  [key: string]: DataCubeGridClientCellValue;
64
84
  };
@@ -259,6 +279,136 @@ function buildRowData(
259
279
  });
260
280
  }
261
281
 
282
+ function buildDimensionalRowData(
283
+ result: TabularDataSet,
284
+ snapshot: DataCubeSnapshot,
285
+ dimensionalNodes: DataCubeDimensionalNode[],
286
+ rowData: DataCubeGridClientRowData[],
287
+ dimensions: DataCubeDimensionConfiguration[],
288
+ ): DataCubeGridClientRowData[] {
289
+ const newRows = result.rows.map((rawRow) =>
290
+ _createClientRow(rawRow, result.columns, dimensionalNodes, dimensions),
291
+ );
292
+
293
+ const updated = [...rowData];
294
+ for (const newRow of newRows) {
295
+ const insertIndex = _findInsertIndex(newRow, updated, dimensionalNodes);
296
+ updated.splice(insertIndex, 0, newRow);
297
+ }
298
+
299
+ return updated;
300
+ }
301
+
302
+ function _createClientRow(
303
+ rawRow: TDSRow,
304
+ columns: string[],
305
+ nodes: DataCubeDimensionalNode[],
306
+ configs: DataCubeDimensionConfiguration[],
307
+ ): DataCubeGridClientRowData {
308
+ const row: DataCubeGridClientRowData = {};
309
+ rawRow.values.forEach((value, i) => {
310
+ row[guaranteeNonNullable(columns[i])] = isBoolean(value)
311
+ ? String(value)
312
+ : isNonNullable(value)
313
+ ? value
314
+ : INTERNAL__GRID_CLIENT_MISSING_VALUE;
315
+ });
316
+
317
+ const metadata = new Map<string, DataCubeDimensionalMetadata>();
318
+ for (const node of nodes) {
319
+ const config = guaranteeNonNullable(
320
+ configs.find((d) => d.name === node.dimension),
321
+ );
322
+ metadata.set(
323
+ node.dimension,
324
+ new DataCubeDimensionalMetadata(
325
+ node.column,
326
+ config.columns.indexOf(node.column),
327
+ node.groupByNodes,
328
+ ),
329
+ );
330
+ }
331
+ row.metadata = metadata;
332
+ return row;
333
+ }
334
+
335
+ function _findInsertIndex(
336
+ newRow: DataCubeGridClientRowData,
337
+ existing: DataCubeGridClientRowData[],
338
+ dimensionalNodes: DataCubeDimensionalNode[],
339
+ ): number {
340
+ const metadata = guaranteeType(
341
+ newRow.metadata,
342
+ Map<string, DataCubeDimensionalMetadata>,
343
+ );
344
+ const groupByNodes = [...metadata.values()].flatMap((m) => m.groupByNodes);
345
+ const dimensions = dimensionalNodes.map((d) => d.dimension);
346
+ const latest = _latestGroupByFilters(groupByNodes);
347
+
348
+ for (let i = existing.length - 1; i >= 0; i--) {
349
+ const existingRow = guaranteeNonNullable(existing[i]);
350
+ if (
351
+ _isParentRow(existingRow, groupByNodes.length, newRow, latest, dimensions)
352
+ ) {
353
+ return i + 1;
354
+ }
355
+ }
356
+
357
+ return existing.length;
358
+ }
359
+
360
+ function _isParentRow(
361
+ existing: DataCubeGridClientRowData,
362
+ groupByCount: number,
363
+ newRow: DataCubeGridClientRowData,
364
+ filters: DataCubeDimensionalGroupByNode[],
365
+ dimensions: string[],
366
+ ): boolean {
367
+ if (groupByCount === 0) {
368
+ let allCount = 0;
369
+ const match = dimensions.every((d) => {
370
+ const e = existing[d],
371
+ n = newRow[d];
372
+ if (e === null || n === null) {
373
+ return false;
374
+ }
375
+ if (String(e) === DIMENSIONAL_L0_COLUMN) {
376
+ allCount++;
377
+ return true;
378
+ }
379
+ return String(e) === String(n);
380
+ });
381
+ return match && allCount >= 1 && allCount < dimensions.length;
382
+ }
383
+
384
+ return dimensions.every((d) => {
385
+ const filter = filters.find((f) => f.dimension === d);
386
+ const e = existing[d],
387
+ n = newRow[d];
388
+ if (e === null || n === null) {
389
+ return false;
390
+ }
391
+ if (filter) {
392
+ return (
393
+ String(e) === String(filter.filter) ||
394
+ String(e) === String(n) ||
395
+ String(e) === DIMENSIONAL_L0_COLUMN
396
+ );
397
+ }
398
+ return String(e) === String(n);
399
+ });
400
+ }
401
+
402
+ function _latestGroupByFilters(
403
+ nodes: DataCubeDimensionalGroupByNode[],
404
+ ): DataCubeDimensionalGroupByNode[] {
405
+ const map = new Map<string, DataCubeDimensionalGroupByNode>();
406
+ for (const node of nodes) {
407
+ map.set(node.dimension, node);
408
+ }
409
+ return [...map.values()];
410
+ }
411
+
262
412
  async function getCastColumns(
263
413
  currentSnapshot: DataCubeSnapshot,
264
414
  view: DataCubeViewState,
@@ -302,6 +452,103 @@ async function getCastColumns(
302
452
  });
303
453
  }
304
454
 
455
+ export async function fetchDimensionalQueryRows(
456
+ snapshot: DataCubeSnapshot,
457
+ view: DataCubeViewState,
458
+ ) {
459
+ const task = view.taskService.newTask('Fetching data...');
460
+
461
+ let rowData: DataCubeGridClientRowData[] = [];
462
+
463
+ const configuration = DataCubeConfiguration.serialization.fromJson(
464
+ snapshot.data.configuration,
465
+ );
466
+
467
+ // upgrade here by not shooting extra sqls
468
+ const paths = generateDimensionalPaths(
469
+ guaranteeNonNullable(snapshot.data.tree),
470
+ );
471
+
472
+ for (const path of paths) {
473
+ const executableQuery = buildDimensionalGridDataFetchExecutableQuery(
474
+ snapshot,
475
+ view.source,
476
+ view.engine,
477
+ path,
478
+ );
479
+ let result: DataCubeExecutionResult;
480
+
481
+ try {
482
+ result = await view.engine.executeQuery(
483
+ _lambda([], [executableQuery]),
484
+ view.source,
485
+ {
486
+ debug: view.settingService.getBooleanValue(
487
+ DataCubeSettingKey.DEBUGGER__ENABLE_DEBUG_MODE,
488
+ ),
489
+ clientVersion: view.settingService.getBooleanValue(
490
+ DataCubeSettingKey.DEBUGGER__USE_DEV_CLIENT_PROTOCOL_VERSION,
491
+ )
492
+ ? PureClientVersion.VX_X_X
493
+ : undefined,
494
+ },
495
+ );
496
+
497
+ rowData = buildDimensionalRowData(
498
+ result.result.result,
499
+ snapshot,
500
+ path,
501
+ rowData,
502
+ configuration.dimensions.dimensions,
503
+ );
504
+
505
+ view.taskService.endTask(task);
506
+ } catch (error) {
507
+ assertErrorThrown(error);
508
+ if (error instanceof DataCubeExecutionError) {
509
+ view.alertService.alertExecutionError(error, {
510
+ message: `Data Fetch Failure: Can't execute query.`,
511
+ text: `Error: ${error.message}`,
512
+ });
513
+ } else {
514
+ view.alertService.alertError(error, {
515
+ message: `Data Fetch Failure: ${error.message}`,
516
+ });
517
+ }
518
+ view.taskService.endTask(task);
519
+ return {
520
+ rowData: [],
521
+ columnDefs: [],
522
+ };
523
+ }
524
+
525
+ if (
526
+ view.settingService.getBooleanValue(
527
+ DataCubeSettingKey.DEBUGGER__ENABLE_DEBUG_MODE,
528
+ )
529
+ ) {
530
+ view.engine.debugProcess(
531
+ `Execution`,
532
+ ['Query', result.executedQuery],
533
+ [
534
+ 'Stats',
535
+ `${rowData.length} rows, ${result.result.result.columns.length} columns`,
536
+ ],
537
+ [
538
+ 'SQL',
539
+ result.executedSQL ?? `-- Error: failed to extract executed SQL`,
540
+ ],
541
+ ['SQL Execution Time', result.executionTime],
542
+ );
543
+ }
544
+ }
545
+
546
+ return {
547
+ rowData: rowData,
548
+ columnDefs: generateColumnDefsForDimensions(snapshot, configuration),
549
+ };
550
+ }
551
+
305
552
  export type DataCubeGridClientDataFetchRequest = {
306
553
  startRow: number | undefined;
307
554
  endRow: number | undefined;
@@ -69,10 +69,12 @@ import {
69
69
  isNumber,
70
70
  isValidUrl,
71
71
  assertTrue,
72
+ guaranteeNonNullable,
72
73
  } from '@finos/legend-shared';
73
- import type {
74
+ import {
74
75
  DataCubeColumnConfiguration,
75
- DataCubeConfiguration,
76
+ type DataCubeConfiguration,
77
+ type DataCubeDimensionConfiguration,
76
78
  } from '../../core/model/DataCubeConfiguration.js';
77
79
  import {
78
80
  DataCubeColumnDataType,
@@ -91,6 +93,7 @@ import {
91
93
  import type { CustomLoadingCellRendererProps } from 'ag-grid-react';
92
94
  import { DataCubeIcon } from '@finos/legend-art';
93
95
  import type { DataCubeViewState } from '../DataCubeViewState.js';
96
+ import type { DataCubeDimensionalMetadata } from './DataCubeGridDimensionalTree.js';
94
97
 
95
98
  // --------------------------------- UTILITIES ---------------------------------
96
99
 
@@ -181,6 +184,51 @@ function getCellRenderer(columnData: ColumnData) {
181
184
  return null;
182
185
  }
183
186
 
187
+ function getDimensionCellRenderer(
188
+ columnData: ColumnData,
189
+ dimensions: DataCubeDimensionConfiguration[],
190
+ ) {
191
+ const { column } = columnData;
192
+ const dataType = getDataType(column.type);
193
+ if (
194
+ dataType === DataCubeColumnDataType.TEXT &&
195
+ dimensions.filter((dim) => dim.name === column.name).length === 1
196
+ ) {
197
+ return function ValueRenderer(params: ICellRendererParams) {
198
+ const level = guaranteeNonNullable(
199
+ (params.data.metadata as Map<string, DataCubeDimensionalMetadata>).get(
200
+ column.name,
201
+ )?.level,
202
+ );
203
+ const indent = '\u00A0'.repeat((level + 1) * 2); // 4 non-breaking spaces per level
204
+ return `${indent}${params.value}`;
205
+ };
206
+ } else if (dataType === DataCubeColumnDataType.TEXT && column.displayAsLink) {
207
+ return function LinkRenderer(params: ICellRendererParams) {
208
+ const isUrl = isValidUrl(params.value);
209
+ if (!isUrl) {
210
+ return params.value;
211
+ }
212
+ const url = params.value as string;
213
+ const label = getQueryParameterValue(
214
+ column.linkLabelParameter ?? DEFAULT_URL_LABEL_QUERY_PARAM,
215
+ getQueryParameters(url, true),
216
+ );
217
+ return (
218
+ <a
219
+ href={url}
220
+ target="_blank"
221
+ rel="noreferrer"
222
+ className="text-blue-600 underline"
223
+ >
224
+ {label ?? url}
225
+ </a>
226
+ );
227
+ };
228
+ }
229
+ return null;
230
+ }
231
+
184
232
  function _displaySpec(columnData: ColumnData) {
185
233
  const { name, snapshot, column, configuration } = columnData;
186
234
  const dataType = getDataType(column.type);
@@ -341,7 +389,13 @@ function _displaySpec(columnData: ColumnData) {
341
389
  params.value !== INTERNAL__GRID_CLIENT_MISSING_VALUE
342
390
  ? `Value = ${params.value === '' ? "''" : params.value === true ? 'TRUE' : params.value === false ? 'FALSE' : params.value}`
343
391
  : `Missing Value`,
344
- cellRenderer: getCellRenderer(columnData),
392
+ cellRenderer:
393
+ configuration.dimensions.dimensions.length > 0
394
+ ? getDimensionCellRenderer(
395
+ columnData,
396
+ configuration.dimensions.dimensions,
397
+ )
398
+ : getCellRenderer(columnData),
345
399
  } satisfies ColDef;
346
400
  }
347
401
 
@@ -631,6 +685,121 @@ export function generateBaseGridOptions(view: DataCubeViewState): GridOptions {
631
685
  };
632
686
  }
633
687
 
688
+ export function generateDimensionalBaseGridOptions(
689
+ view: DataCubeViewState,
690
+ ): GridOptions {
691
+ const grid = view.grid;
692
+
693
+ return {
694
+ // -------------------------------------- README --------------------------------------
695
+ // NOTE: we observe performance degradataion when configuring the grid via React component
696
+ // props when the options is non-static, i.e. changed when the query configuration changes.
697
+ // As such, we must ONLY ADD STATIC CONFIGURATION HERE, and dynamic configuration should be
698
+ // programatically updated when the query is modified.
699
+ //
700
+ //
701
+ // -------------------------------------- DISPLAY --------------------------------------
702
+ rowHeight: INTERNAL__GRID_CLIENT_ROW_HEIGHT,
703
+ headerHeight: INTERNAL__GRID_CLIENT_HEADER_HEIGHT,
704
+ noRowsOverlayComponent: () => (
705
+ <div className="flex items-center border-[1.5px] border-neutral-300 p-2 font-medium text-neutral-400">
706
+ <div>
707
+ <DataCubeIcon.WarningCircle className="mr-1 stroke-2 text-lg" />
708
+ </div>
709
+ 0 rows
710
+ </div>
711
+ ),
712
+ loadingOverlayComponent: () => (
713
+ <div className="flex items-center border-[1.5px] border-neutral-300 p-2 font-medium text-neutral-400">
714
+ <div>
715
+ <DataCubeIcon.Loader className="mr-1 animate-spin stroke-2 text-lg" />
716
+ </div>
717
+ Loading...
718
+ </div>
719
+ ),
720
+ // Show cursor position when scrolling
721
+ onBodyScroll: (event) => {
722
+ const rowCount = event.api.getDisplayedRowCount();
723
+ const range = event.api.getVerticalPixelRange();
724
+ const start = Math.max(
725
+ 1,
726
+ Math.ceil(range.top / INTERNAL__GRID_CLIENT_ROW_HEIGHT) + 1,
727
+ );
728
+ const end = Math.min(
729
+ rowCount,
730
+ Math.floor(range.bottom / INTERNAL__GRID_CLIENT_ROW_HEIGHT),
731
+ );
732
+ grid.setScrollHintText(`${start}-${end}/${rowCount}`);
733
+ event.api.hidePopupMenu(); // hide context-menu while scrolling
734
+ },
735
+ onBodyScrollEnd: () => grid.setScrollHintText(undefined),
736
+ // -------------------------------------- CONTEXT MENU --------------------------------------
737
+ // figure out the context menu
738
+ preventDefaultOnContextMenu: true, // prevent showing the browser's context menu
739
+ columnMenu: 'new', // ensure context menu works on header
740
+ // NOTE: dynamically generate the content of the context menu to make sure the items are not stale
741
+ getContextMenuItems: (params) =>
742
+ grid.controller.menuBuilder?.(params, false) ?? [],
743
+ getMainMenuItems: (params) =>
744
+ grid.controller.menuBuilder?.(params, true) ?? [],
745
+ // NOTE: when right-clicking empty space in the header, a menu will show up
746
+ // with 2 default options: 'Choose Columns` and `Reset Columns`, which is not
747
+ // a desired behavior, so we hide the popup menu immediately
748
+ onColumnMenuVisibleChanged: (event) => {
749
+ if (!event.column) {
750
+ const menuElement = document.querySelector(
751
+ `.${INTERNAL__GridClientUtilityCssClassName.ROOT} .ag-popup .ag-menu`,
752
+ ) as HTMLElement | undefined;
753
+ if (menuElement) {
754
+ menuElement.style.display = 'none';
755
+ }
756
+ event.api.hidePopupMenu();
757
+ }
758
+ },
759
+ // -------------------------------------- COLUMN SIZING --------------------------------------
760
+ autoSizePadding: INTERNAL__GRID_CLIENT_AUTO_RESIZE_PADDING,
761
+ autoSizeStrategy: {
762
+ type: 'fitCellContents',
763
+ },
764
+ defaultColDef: {
765
+ sortable: false,
766
+ },
767
+ // -------------------------------------- TOOLTIP --------------------------------------
768
+ tooltipShowDelay: INTERNAL__GRID_CLIENT_TOOLTIP_SHOW_DELAY,
769
+ // though this is a nice behavior to have enabled, ag-grid not dismissing tooltip
770
+ // when context-menu is triggered makes it an undesirable interaction.
771
+ tooltipInteraction: false,
772
+ // -------------------------------------- COLUMN MOVING --------------------------------------
773
+ suppressDragLeaveHidesColumns: true, // disable this since it's quite easy to accidentally hide columns while moving
774
+ // -------------------------------------- SELECTION --------------------------------------
775
+ cellSelection: true,
776
+ // -------------------------------------- SIDEBAR --------------------------------------
777
+ sideBar: {
778
+ toolPanels: [
779
+ {
780
+ id: 'columns',
781
+ labelDefault: 'Columns',
782
+ labelKey: 'columns',
783
+ iconKey: 'columns',
784
+ toolPanel: 'agColumnsToolPanel',
785
+ minWidth: INTERNAL__GRID_CLIENT_SIDE_BAR_WIDTH,
786
+ width: INTERNAL__GRID_CLIENT_SIDE_BAR_WIDTH,
787
+ toolPanelParams: {
788
+ suppressValues: true,
789
+ suppressPivotMode: true,
790
+ suppressRowGroups: true,
791
+ },
792
+ },
793
+ ],
794
+ position: 'right',
795
+ },
796
+ allowDragFromColumnsToolPanel: true,
797
+ // -------------------------------------- PERFORMANCE --------------------------------------
798
+ animateRows: false, // improve performance
799
+ suppressColumnMoveAnimation: true, // improve performance
800
+ };
801
+ }
802
+
634
803
  function generatePivotResultColumnHeaderTooltip(
635
804
  id: string,
636
805
  snapshot: DataCubeSnapshot,
@@ -922,6 +1091,78 @@ export function generateColumnDefs(
922
1091
  return columnDefs;
923
1092
  }
924
1093
 
1094
+ export function generateColumnDefsForDimensions(
1095
+ snapshot: DataCubeSnapshot,
1096
+ configuration: DataCubeConfiguration,
1097
+ ) {
1098
+ // NOTE: only show columns which are fetched in select() or are dimensions as we
1099
+ // can't solely rely on column selection because of certain restrictions
1100
+ // from ag-grid, e.g. in the case of row grouping tree column: the columns
1101
+ // which are grouped must be present in the column definitions, so even
1102
+ // when some of these might not be selected explicitly by the users, they
1103
+ // must still be included in the column definitions, and made hidden instead.
1104
+ const dimensionColNames = configuration.dimensions.dimensions.flatMap(
1105
+ (col) => col.columns,
1106
+ );
1107
+ const dimCols = configuration.dimensions.dimensions.map(
1108
+ (col) => new DataCubeColumnConfiguration(col.name, 'String'),
1109
+ );
1110
+
1111
+ let columns = configuration.columns.filter(
1112
+ (col) =>
1113
+ _findCol(snapshot.data.selectColumns, col.name) ??
1114
+ _findCol(snapshot.data.groupExtendedColumns, col.name),
1115
+ );
1116
+ columns = columns.filter((col) => !dimensionColNames.includes(col.name));
1117
+ columns.unshift(...dimCols);
1118
+
1119
+ const columnDefs = [
1120
+ ...(configuration.showRootAggregation
1121
+ ? [
1122
+ {
1123
+ colId: INTERNAL__GRID_CLIENT_ROOT_AGGREGATION_COLUMN_ID,
1124
+ headerName: 'Root',
1125
+ field: INTERNAL__GRID_CLIENT_ROOT_AGGREGATION_COLUMN_ID,
1126
+ hide: true,
1127
+ enableValue: false, // disable GUI interactions to modify this column's aggregate function
1128
+ allowedAggFuncs: [], // disable GUI for options of the agg functions
1129
+ enablePivot: false,
1130
+ enableRowGroup: false,
1131
+
1132
+ suppressColumnsToolPanel: true,
1133
+ rowGroup: true,
1134
+ rowGroupIndex: 0,
1135
+ } satisfies ColDef,
1136
+ ]
1137
+ : []),
1138
+ ...columns.map((column) => {
1139
+ const columnData = {
1140
+ name: column.name,
1141
+ snapshot,
1142
+ column,
1143
+ configuration,
1144
+ };
1145
+ return {
1146
+ headerName: column.displayName ?? column.name,
1147
+ headerTooltip: `Column = ${
1148
+ column.displayName
1149
+ ? `${column.displayName} (${column.name})`
1150
+ : column.name
1151
+ }`,
1152
+ suppressSpanHeaderHeight: true,
1153
+ colId: column.name,
1154
+ field: column.name,
1155
+ menuTabs: [],
1156
+
1157
+ ..._displaySpec(columnData),
1158
+ ..._sizeSpec(columnData),
1159
+ } satisfies ColDef;
1160
+ }),
1161
+ ] satisfies (ColDef | ColGroupDef)[];
1162
+
1163
+ return columnDefs;
1164
+ }
1165
+
925
1166
  export function generateGridOptionsFromSnapshot(
926
1167
  snapshot: DataCubeSnapshot,
927
1168
  configuration: DataCubeConfiguration,
@@ -38,6 +38,7 @@ import {
38
38
  DataCubeQuerySortDirection,
39
39
  isPivotResultColumnName,
40
40
  getPivotResultColumnBaseColumnName,
41
+ isDimensionalGridMode,
41
42
  } from '../../core/DataCubeQueryEngine.js';
42
43
  import type {
43
44
  DefaultMenuItem,
@@ -46,7 +47,10 @@ import type {
46
47
  MenuItemDef,
47
48
  } from 'ag-grid-community';
48
49
  import type { DataCubeViewState } from '../DataCubeViewState.js';
49
- import { generateMenuBuilder } from './DataCubeGridMenuBuilder.js';
50
+ import {
51
+ generateDimensionalMenuBuilder,
52
+ generateMenuBuilder,
53
+ } from './DataCubeGridMenuBuilder.js';
50
54
  import {
51
55
  buildFilterEditorTree,
52
56
  buildFilterSnapshot,
@@ -427,7 +431,11 @@ export class DataCubeGridControllerState extends DataCubeSnapshotController {
427
431
 
428
432
  this.sortColumns = snapshot.data.sortColumns;
429
433
 
430
- this.menuBuilder = generateMenuBuilder(this);
434
+ this.menuBuilder = isDimensionalGridMode(
435
+ this.view.info.configuration.gridMode,
436
+ )
437
+ ? generateDimensionalMenuBuilder(this)
438
+ : generateMenuBuilder(this);
431
439
  }
432
440
 
433
441
  private propagateChanges(baseSnapshot: DataCubeSnapshot) {