@finos/legend-data-cube 0.1.6 → 0.1.8

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 (59) hide show
  1. package/lib/components/core/DataCubeExecutionErrorAlert.d.ts +23 -0
  2. package/lib/components/core/DataCubeExecutionErrorAlert.d.ts.map +1 -0
  3. package/lib/components/core/DataCubeExecutionErrorAlert.js +40 -0
  4. package/lib/components/core/DataCubeExecutionErrorAlert.js.map +1 -0
  5. package/lib/components/core/DataCubeFormUtils.d.ts +6 -0
  6. package/lib/components/core/DataCubeFormUtils.d.ts.map +1 -1
  7. package/lib/components/core/DataCubeFormUtils.js +6 -1
  8. package/lib/components/core/DataCubeFormUtils.js.map +1 -1
  9. package/lib/components/view/editor/DataCubeEditorColumnPropertiesPanel.js +1 -1
  10. package/lib/components/view/editor/DataCubeEditorColumnPropertiesPanel.js.map +1 -1
  11. package/lib/components/view/editor/DataCubeEditorDimensionsPanel.d.ts +32 -0
  12. package/lib/components/view/editor/DataCubeEditorDimensionsPanel.d.ts.map +1 -0
  13. package/lib/components/view/editor/DataCubeEditorDimensionsPanel.js +311 -0
  14. package/lib/components/view/editor/DataCubeEditorDimensionsPanel.js.map +1 -0
  15. package/lib/components/view/grid/DataCubeMultidimensionalGrid.d.ts +22 -0
  16. package/lib/components/view/grid/DataCubeMultidimensionalGrid.d.ts.map +1 -0
  17. package/lib/components/view/grid/DataCubeMultidimensionalGrid.js +20 -0
  18. package/lib/components/view/grid/DataCubeMultidimensionalGrid.js.map +1 -0
  19. package/lib/index.css +1 -1
  20. package/lib/package.json +4 -4
  21. package/lib/stores/core/DataCubeEngine.d.ts +5 -1
  22. package/lib/stores/core/DataCubeEngine.d.ts.map +1 -1
  23. package/lib/stores/core/DataCubeEngine.js +7 -1
  24. package/lib/stores/core/DataCubeEngine.js.map +1 -1
  25. package/lib/stores/core/model/AdhocQueryDataCubeSource.d.ts +3 -2
  26. package/lib/stores/core/model/AdhocQueryDataCubeSource.d.ts.map +1 -1
  27. package/lib/stores/core/model/AdhocQueryDataCubeSource.js.map +1 -1
  28. package/lib/stores/core/model/CachedDataCubeSource.d.ts +2 -2
  29. package/lib/stores/core/model/CachedDataCubeSource.d.ts.map +1 -1
  30. package/lib/stores/core/model/CachedDataCubeSource.js +0 -1
  31. package/lib/stores/core/model/CachedDataCubeSource.js.map +1 -1
  32. package/lib/stores/services/DataCubeAlertService.d.ts +2 -0
  33. package/lib/stores/services/DataCubeAlertService.d.ts.map +1 -1
  34. package/lib/stores/services/DataCubeAlertService.js +13 -0
  35. package/lib/stores/services/DataCubeAlertService.js.map +1 -1
  36. package/lib/stores/view/DataCubeViewState.d.ts.map +1 -1
  37. package/lib/stores/view/DataCubeViewState.js +2 -1
  38. package/lib/stores/view/DataCubeViewState.js.map +1 -1
  39. package/lib/stores/view/editor/DataCubeEditorDimensionsPanelState.d.ts +34 -0
  40. package/lib/stores/view/editor/DataCubeEditorDimensionsPanelState.d.ts.map +1 -0
  41. package/lib/stores/view/editor/DataCubeEditorDimensionsPanelState.js +86 -0
  42. package/lib/stores/view/editor/DataCubeEditorDimensionsPanelState.js.map +1 -0
  43. package/lib/stores/view/grid/DataCubeGridClientEngine.d.ts.map +1 -1
  44. package/lib/stores/view/grid/DataCubeGridClientEngine.js +23 -6
  45. package/lib/stores/view/grid/DataCubeGridClientEngine.js.map +1 -1
  46. package/package.json +9 -9
  47. package/src/components/core/DataCubeExecutionErrorAlert.tsx +134 -0
  48. package/src/components/core/DataCubeFormUtils.tsx +37 -2
  49. package/src/components/view/editor/DataCubeEditorColumnPropertiesPanel.tsx +1 -1
  50. package/src/components/view/editor/DataCubeEditorDimensionsPanel.tsx +606 -0
  51. package/src/components/view/grid/DataCubeMultidimensionalGrid.tsx +24 -0
  52. package/src/stores/core/DataCubeEngine.tsx +9 -1
  53. package/src/stores/core/model/AdhocQueryDataCubeSource.ts +3 -2
  54. package/src/stores/core/model/CachedDataCubeSource.ts +2 -3
  55. package/src/stores/services/DataCubeAlertService.tsx +27 -0
  56. package/src/stores/view/DataCubeViewState.ts +1 -0
  57. package/src/stores/view/editor/DataCubeEditorDimensionsPanelState.ts +134 -0
  58. package/src/stores/view/grid/DataCubeGridClientEngine.ts +24 -7
  59. package/tsconfig.json +4 -0
@@ -284,7 +284,7 @@ export const DataCubeEditorColumnPropertiesPanel = observer(
284
284
  )}
285
285
  </div>
286
286
 
287
- <div className="-ml-2 mb-2 mt-3 h-[1px] w-[calc(100%_+_16px)] bg-neutral-200" />
287
+ <div className="mb-2 ml-2 mt-3 h-[1px] w-[calc(100%_-_16px)] bg-neutral-200" />
288
288
 
289
289
  {selectedColumn && (
290
290
  <>
@@ -0,0 +1,606 @@
1
+ /**
2
+ * Copyright (c) 2020-present, Goldman Sachs
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { observer } from 'mobx-react-lite';
18
+ import { DataCubeIcon } from '@finos/legend-art';
19
+ import {
20
+ type ColDef,
21
+ type ColDefField,
22
+ type GridApi,
23
+ type ModelUpdatedEvent,
24
+ type RowDragEndEvent,
25
+ type SelectionChangedEvent,
26
+ AllCommunityModule,
27
+ } from 'ag-grid-community';
28
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
29
+ import {
30
+ AgGridReact,
31
+ type AgGridReactProps,
32
+ type CustomCellRendererProps,
33
+ type CustomNoRowsOverlayProps,
34
+ } from 'ag-grid-react';
35
+ import {
36
+ type DataCubeEditorColumnSelectorColumnState,
37
+ type DataCubeEditorColumnSelectorState,
38
+ } from '../../../stores/view/editor/DataCubeEditorColumnSelectorState.js';
39
+ import { isNonNullable } from '@finos/legend-shared';
40
+ import {
41
+ getDataForAllFilteredNodes,
42
+ getDataForAllNodes,
43
+ } from '../../../stores/view/grid/DataCubeGridClientEngine.js';
44
+
45
+ function getBaseGridProps<
46
+ T extends DataCubeEditorColumnSelectorColumnState,
47
+ >(): AgGridReactProps<T> {
48
+ return {
49
+ modules: [AllCommunityModule],
50
+ className: 'ag-theme-quartz',
51
+ animateRows: false,
52
+ getRowId: (params) => params.data.name,
53
+ editType: 'fullRow',
54
+ rowDragMultiRow: true,
55
+ rowDragEntireRow: true,
56
+ rowSelection: {
57
+ mode: 'multiRow',
58
+ checkboxes: true,
59
+ headerCheckbox: true,
60
+ enableClickSelection: true,
61
+ },
62
+ selectionColumnDef: {
63
+ width: 40,
64
+ headerClass: '!pl-[23px]',
65
+ cellClass: '!pl-1.5',
66
+ rowDrag: true,
67
+ rowDragText: (params, dragItemCount) => {
68
+ if (dragItemCount > 1) {
69
+ return `${dragItemCount} columns`;
70
+ }
71
+ return (params.rowNode?.data as T).name;
72
+ },
73
+ sortable: false,
74
+ resizable: false,
75
+ suppressHeaderMenuButton: true,
76
+ },
77
+ suppressMoveWhenRowDragging: true,
78
+ rowHeight: 20,
79
+ headerHeight: 20,
80
+ suppressRowHoverHighlight: false,
81
+ noRowsOverlayComponent: (
82
+ params: CustomNoRowsOverlayProps<T> & {
83
+ noColumnsSelectedRenderer?: (() => React.ReactNode) | undefined;
84
+ },
85
+ ) => {
86
+ if (params.api.getQuickFilter()) {
87
+ return (
88
+ <div className="flex items-center border-[1.5px] border-neutral-300 p-2 font-semibold text-neutral-400">
89
+ <div>
90
+ <DataCubeIcon.WarningCircle className="mr-1 text-lg" />
91
+ </div>
92
+ No match found
93
+ </div>
94
+ );
95
+ }
96
+ if (params.noColumnsSelectedRenderer) {
97
+ return params.noColumnsSelectedRenderer();
98
+ }
99
+ return <div />;
100
+ },
101
+ // Show no rows overlay when there are no search results
102
+ // See https://stackoverflow.com/a/72637410
103
+ onModelUpdated: (event: ModelUpdatedEvent<T>) => {
104
+ if (event.api.getDisplayedRowCount() === 0) {
105
+ event.api.showNoRowsOverlay();
106
+ } else {
107
+ event.api.hideOverlay();
108
+ }
109
+ },
110
+ };
111
+ }
112
+
113
+ function getBaseColumnDef<
114
+ T extends DataCubeEditorColumnSelectorColumnState,
115
+ >(): ColDef<T> {
116
+ return {
117
+ field: 'name' as ColDefField<T>,
118
+ colId: 'name',
119
+ flex: 1,
120
+ minWidth: 100,
121
+ filter: true,
122
+ sortable: false,
123
+ resizable: false,
124
+ suppressHeaderMenuButton: true,
125
+ getQuickFilterText: (params) => params.value,
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Move this display to a separate component to avoid re-rendering the header too frequently
131
+ */
132
+ const ColumnsSearchResultCountBadge = observer(
133
+ function ColumnsSearchResultCountBadge<
134
+ T extends DataCubeEditorColumnSelectorColumnState,
135
+ >(props: {
136
+ selector: DataCubeEditorColumnSelectorState<T>;
137
+ gridApi: GridApi<T>;
138
+ scope: 'available' | 'selected';
139
+ }) {
140
+ const { selector, gridApi, scope } = props;
141
+ return (
142
+ <div className="flex items-center justify-center rounded-lg bg-neutral-500 px-1 py-0.5 font-mono text-xs font-bold text-white">
143
+ {`${getDataForAllFilteredNodes(gridApi).length}/${scope === 'available' ? selector.availableColumnsForDisplay.length : selector.selectedColumnsForDisplay.length}`}
144
+ <span className="hidden">
145
+ {scope === 'available'
146
+ ? // subscribing to the search text to trigger re-render as it changes
147
+ selector.availableColumnsSearchText
148
+ : selector.selectedColumnsSearchText}
149
+ </span>
150
+ </div>
151
+ );
152
+ },
153
+ );
154
+
155
+ export const DataCubeEditorColumnSelector = observer(
156
+ function DataCubeEditorColumnSelector<
157
+ T extends DataCubeEditorColumnSelectorColumnState,
158
+ >(props: {
159
+ selector: DataCubeEditorColumnSelectorState<T>;
160
+ columnLabelRenderer?:
161
+ | ((p: {
162
+ selector: DataCubeEditorColumnSelectorState<T>;
163
+ column: T;
164
+ }) => React.ReactNode)
165
+ | undefined;
166
+ columnActionRenderer?:
167
+ | ((p: {
168
+ selector: DataCubeEditorColumnSelectorState<T>;
169
+ column: T;
170
+ }) => React.ReactNode)
171
+ | undefined;
172
+ noColumnsSelectedRenderer?: (() => React.ReactNode) | undefined;
173
+ }) {
174
+ const {
175
+ selector,
176
+ columnLabelRenderer,
177
+ columnActionRenderer,
178
+ noColumnsSelectedRenderer,
179
+ } = props;
180
+ const [selectedAvailableColumns, setSelectedAvailableColumns] = useState<
181
+ T[]
182
+ >([]);
183
+ const [selectedSelectedColumns, setSelectedSelectedColumns] = useState<T[]>(
184
+ [],
185
+ );
186
+ const [availableColumnsGridApi, setAvailableColumnsGridApi] =
187
+ useState<GridApi | null>(null);
188
+ const [selectedColumnsGridApi, setSelectedColumnsGridApi] =
189
+ useState<GridApi | null>(null);
190
+ const searchAvailableColumnsInputRef = useRef<HTMLInputElement | null>(
191
+ null,
192
+ );
193
+ const searchSelectedColumnsInputRef = useRef<HTMLInputElement | null>(null);
194
+
195
+ /**
196
+ * Since we use managed row dragging for selected columns,
197
+ * we just need to sync the row data with the state.
198
+ * Dragging (multiple) rows to specific position have been
199
+ * handled by ag-grid.
200
+ */
201
+ const onSelectedColumnsDragStop = useCallback(
202
+ (params: RowDragEndEvent<T>) => {
203
+ selector.setSelectedColumns(getDataForAllNodes(params.api));
204
+ },
205
+ [selector],
206
+ );
207
+
208
+ const onAvailableColumnsDragStop = useCallback(
209
+ (params: RowDragEndEvent<T>) => {
210
+ const nodes = params.nodes;
211
+ const columnsToMove = nodes
212
+ .map((node) => node.data)
213
+ .filter(isNonNullable);
214
+ selector.setSelectedColumns(
215
+ selector.selectedColumns.filter(
216
+ (column) => !columnsToMove.includes(column),
217
+ ),
218
+ );
219
+ },
220
+ [selector],
221
+ );
222
+
223
+ /**
224
+ * Since we use managed row dragging for selected columns,
225
+ * we just need to sync the row data with the state
226
+ * Dragging (multiple) rows to specific position have been
227
+ * handled by ag-grid.
228
+ */
229
+ const onSelectedColumnsDragEnd = useCallback(
230
+ (event: RowDragEndEvent) => {
231
+ if (event.overIndex === -1) {
232
+ return;
233
+ }
234
+ selector.setSelectedColumns(getDataForAllNodes(event.api));
235
+ },
236
+ [selector],
237
+ );
238
+
239
+ /**
240
+ * Setup row drop zones for each grid to be the other
241
+ * See https://www.ag-grid.com/react-data-grid/row-dragging-to-grid/
242
+ */
243
+ useEffect(() => {
244
+ if (!availableColumnsGridApi || !selectedColumnsGridApi) {
245
+ return;
246
+ }
247
+ const selectedColumnsDropZoneParams =
248
+ selectedColumnsGridApi.getRowDropZoneParams({
249
+ onDragStop: (event) => {
250
+ onSelectedColumnsDragStop(event);
251
+ availableColumnsGridApi.clearFocusedCell();
252
+ },
253
+ });
254
+ if (selectedColumnsDropZoneParams) {
255
+ availableColumnsGridApi.removeRowDropZone(
256
+ selectedColumnsDropZoneParams,
257
+ );
258
+ availableColumnsGridApi.addRowDropZone(selectedColumnsDropZoneParams);
259
+ }
260
+
261
+ const availableColumnsDropZoneParams =
262
+ availableColumnsGridApi.getRowDropZoneParams({
263
+ onDragStop: (event) => {
264
+ onAvailableColumnsDragStop(event);
265
+ selectedColumnsGridApi.clearFocusedCell();
266
+ },
267
+ });
268
+ if (availableColumnsDropZoneParams) {
269
+ selectedColumnsGridApi.removeRowDropZone(
270
+ availableColumnsDropZoneParams,
271
+ );
272
+ selectedColumnsGridApi.addRowDropZone(availableColumnsDropZoneParams);
273
+ }
274
+ }, [
275
+ availableColumnsGridApi,
276
+ selectedColumnsGridApi,
277
+ onSelectedColumnsDragStop,
278
+ onAvailableColumnsDragStop,
279
+ ]);
280
+
281
+ return (
282
+ <div className="data-cube-column-selector flex h-full w-full">
283
+ <div className="h-full w-[calc(50%_-_20px)]">
284
+ <div className="flex h-5 items-center text-sm">
285
+ Available columns:
286
+ </div>
287
+ <div className="h-[calc(100%_-_20px)] rounded-sm border border-neutral-200">
288
+ <div className="relative h-6 border-b border-neutral-200">
289
+ <input
290
+ className="h-full w-full pl-10 pr-6 placeholder-neutral-400"
291
+ ref={searchAvailableColumnsInputRef}
292
+ placeholder="Click here to search..."
293
+ value={selector.availableColumnsSearchText}
294
+ onChange={(event) =>
295
+ selector.setAvailableColumnsSearchText(event.target.value)
296
+ }
297
+ onKeyDown={(event) => {
298
+ if (event.code === 'Escape') {
299
+ event.stopPropagation();
300
+ searchAvailableColumnsInputRef.current?.select();
301
+ selector.setAvailableColumnsSearchText('');
302
+ }
303
+ }}
304
+ />
305
+ <div className="absolute left-0 top-0 flex h-6 w-10 items-center justify-center">
306
+ <DataCubeIcon.Search className="stroke-[3px] text-lg text-neutral-500" />
307
+ </div>
308
+ <button
309
+ className="absolute right-0 top-0 flex h-6 w-6 items-center justify-center text-neutral-500 disabled:text-neutral-300"
310
+ disabled={!selector.availableColumnsSearchText}
311
+ title="Clear search [Esc]"
312
+ onClick={() => {
313
+ selector.setAvailableColumnsSearchText('');
314
+ searchAvailableColumnsInputRef.current?.focus();
315
+ }}
316
+ >
317
+ <DataCubeIcon.X className="text-lg" />
318
+ </button>
319
+ </div>
320
+ <div className="h-[calc(100%_-_24px)]">
321
+ <AgGridReact
322
+ theme="legacy"
323
+ {...getBaseGridProps<T>()}
324
+ // Disable managed row-dragging to disallow changing the order of columns
325
+ // and to make sure the row data and the available columns state are in sync
326
+ rowDragManaged={false}
327
+ onGridReady={(params) => setAvailableColumnsGridApi(params.api)}
328
+ onSelectionChanged={(event: SelectionChangedEvent<T>) => {
329
+ setSelectedAvailableColumns(
330
+ event.api
331
+ .getSelectedNodes()
332
+ .map((node) => node.data)
333
+ .filter(isNonNullable),
334
+ );
335
+ }}
336
+ // Using ag-grid quick filter is a cheap way to implement search
337
+ quickFilterText={selector.availableColumnsSearchText}
338
+ rowData={selector.availableColumnsForDisplay}
339
+ columnDefs={[
340
+ {
341
+ ...getBaseColumnDef<T>(),
342
+ /**
343
+ * Support double-click to add all (filtered by search) columns
344
+ */
345
+ headerComponent: (params: CustomCellRendererProps<T>) => (
346
+ <button
347
+ title="Double-click to add all columns"
348
+ className="flex h-full w-full items-center justify-between pl-0.5"
349
+ onDoubleClick={() => {
350
+ // The columns being moved are scoped by the current search
351
+ const filteredData = getDataForAllFilteredNodes(
352
+ params.api,
353
+ );
354
+ selector.setSelectedColumns([
355
+ ...selector.selectedColumns,
356
+ ...filteredData,
357
+ ]);
358
+ params.api.clearFocusedCell();
359
+ }}
360
+ >
361
+ <div>{`[All Columns]`}</div>
362
+ <ColumnsSearchResultCountBadge
363
+ selector={selector}
364
+ gridApi={params.api}
365
+ scope="available"
366
+ />
367
+ </button>
368
+ ),
369
+ cellRenderer: (params: CustomCellRendererProps<T>) => {
370
+ const data = params.data;
371
+ if (!data) {
372
+ return null;
373
+ }
374
+
375
+ return (
376
+ <div
377
+ className="flex h-full w-full cursor-pointer items-center"
378
+ title={`[${data.name}]\nDouble-click to add column`}
379
+ onDoubleClick={() => {
380
+ selector.setSelectedColumns([
381
+ ...selector.selectedColumns,
382
+ data,
383
+ ]);
384
+ params.api.clearFocusedCell();
385
+ }}
386
+ >
387
+ {columnLabelRenderer?.({
388
+ selector,
389
+ column: data,
390
+ }) ?? (
391
+ <div className="h-full flex-1 items-center overflow-hidden overflow-ellipsis whitespace-nowrap pl-2">
392
+ {data.name}
393
+ </div>
394
+ )}
395
+ <div className="flex h-full">
396
+ {columnActionRenderer?.({
397
+ selector,
398
+ column: data,
399
+ }) ?? null}
400
+ </div>
401
+ </div>
402
+ );
403
+ },
404
+ },
405
+ ]}
406
+ />
407
+ </div>
408
+ </div>
409
+ </div>
410
+ <div className="flex h-full w-10 items-center justify-center">
411
+ <div className="flex flex-col">
412
+ <button
413
+ className="flex items-center justify-center rounded-sm border border-neutral-300 bg-neutral-100 text-neutral-500 hover:bg-neutral-200 disabled:bg-neutral-200 disabled:text-neutral-400"
414
+ title="Add selected columns"
415
+ /**
416
+ * Support add selected (filtered by search) columns
417
+ * We reset the selection after this operation
418
+ */
419
+ onClick={() => {
420
+ if (!availableColumnsGridApi) {
421
+ return;
422
+ }
423
+ // The columns being moved are scoped by the current search
424
+ const filteredData = getDataForAllFilteredNodes(
425
+ availableColumnsGridApi,
426
+ );
427
+ const columnsToMove = selectedAvailableColumns.filter(
428
+ (column) => filteredData.includes(column),
429
+ );
430
+ selector.setSelectedColumns([
431
+ ...selector.selectedColumns,
432
+ ...columnsToMove,
433
+ ]);
434
+ availableColumnsGridApi.clearFocusedCell();
435
+ }}
436
+ disabled={selectedAvailableColumns.length === 0}
437
+ >
438
+ <DataCubeIcon.ChevronRight className="text-2xl" />
439
+ </button>
440
+ <button
441
+ className="mt-2 flex items-center justify-center rounded-sm border border-neutral-300 bg-neutral-100 text-neutral-500 hover:bg-neutral-200 disabled:bg-neutral-200 disabled:text-neutral-400"
442
+ title="Remove selected columns"
443
+ /**
444
+ * Support remove selected (filtered by search) columns
445
+ * We reset the selection after this operation
446
+ */
447
+ onClick={() => {
448
+ if (!selectedColumnsGridApi) {
449
+ return;
450
+ }
451
+ // The columns being moved are scoped by the current search
452
+ const filteredData = getDataForAllFilteredNodes(
453
+ selectedColumnsGridApi,
454
+ );
455
+ const columnsToMove = selectedSelectedColumns.filter((column) =>
456
+ filteredData.includes(column),
457
+ );
458
+ selector.setSelectedColumns(
459
+ selector.selectedColumns.filter(
460
+ (column) => !columnsToMove.includes(column),
461
+ ),
462
+ );
463
+ selectedColumnsGridApi.clearFocusedCell();
464
+ }}
465
+ disabled={selectedSelectedColumns.length === 0}
466
+ >
467
+ <DataCubeIcon.ChevronLeft className="text-2xl" />
468
+ </button>
469
+ </div>
470
+ </div>
471
+ <div className="h-full w-[calc(50%_-_20px)]">
472
+ <div className="flex h-5 items-center text-sm">Selected columns:</div>
473
+ <div className="h-[calc(100%_-_20px)] rounded-sm border border-neutral-200">
474
+ <div className="relative h-6 border-b border-neutral-200">
475
+ <input
476
+ className="h-full w-full pl-10 placeholder-neutral-400"
477
+ ref={searchSelectedColumnsInputRef}
478
+ placeholder="Click here to search..."
479
+ value={selector.selectedColumnsSearchText}
480
+ onChange={(event) =>
481
+ selector.setSelectedColumnsSearchText(event.target.value)
482
+ }
483
+ onKeyDown={(event) => {
484
+ if (event.code === 'Escape') {
485
+ event.stopPropagation();
486
+ selector.setSelectedColumnsSearchText('');
487
+ }
488
+ }}
489
+ />
490
+ <div className="absolute left-0 top-0 flex h-6 w-10 items-center justify-center">
491
+ <DataCubeIcon.Search className="stroke-[3px] text-lg text-neutral-500" />
492
+ </div>
493
+ <button
494
+ className="absolute right-0 top-0 flex h-6 w-6 items-center justify-center text-neutral-500 disabled:text-neutral-300"
495
+ disabled={!selector.selectedColumnsSearchText}
496
+ title="Clear search [Esc]"
497
+ onClick={() => {
498
+ selector.setSelectedColumnsSearchText('');
499
+ searchSelectedColumnsInputRef.current?.focus();
500
+ }}
501
+ >
502
+ <DataCubeIcon.X className="text-lg" />
503
+ </button>
504
+ </div>
505
+ <div className="h-[calc(100%_-_24px)]">
506
+ <AgGridReact
507
+ theme="legacy"
508
+ {...getBaseGridProps<T>()}
509
+ // NOTE: technically, we don't want to enable managed row-dragging here
510
+ // but enabling this gives us free row moving management and interaction
511
+ // comes out of the box from ag-grid, we will just sync the state with
512
+ // grid row data afterwards to ensure consistency
513
+ rowDragManaged={true}
514
+ onRowDragEnd={onSelectedColumnsDragEnd}
515
+ onGridReady={(params) => setSelectedColumnsGridApi(params.api)}
516
+ onSelectionChanged={(event: SelectionChangedEvent<T>) => {
517
+ setSelectedSelectedColumns(
518
+ event.api
519
+ .getSelectedNodes()
520
+ .map((node) => node.data)
521
+ .filter(isNonNullable),
522
+ );
523
+ }}
524
+ // Using ag-grid quick filter is a cheap way to implement search
525
+ quickFilterText={selector.selectedColumnsSearchText}
526
+ noRowsOverlayComponentParams={{
527
+ noColumnsSelectedRenderer,
528
+ }}
529
+ rowData={selector.selectedColumnsForDisplay}
530
+ columnDefs={[
531
+ {
532
+ ...getBaseColumnDef<T>(),
533
+ /**
534
+ * Support double-click to remove all (filtered by search) columns
535
+ */
536
+ headerComponent: (params: CustomCellRendererProps<T>) => (
537
+ <button
538
+ title="Double-click to remove all columns"
539
+ className="flex h-full w-full items-center justify-between pl-0.5"
540
+ onDoubleClick={() => {
541
+ // The columns being moved are scoped by the current search
542
+ const filteredData = getDataForAllFilteredNodes(
543
+ params.api,
544
+ );
545
+ selector.setSelectedColumns(
546
+ selector.selectedColumns.filter(
547
+ (column) => !filteredData.includes(column),
548
+ ),
549
+ );
550
+ params.api.clearFocusedCell();
551
+ }}
552
+ >
553
+ <div>{`[All Columns]`}</div>
554
+ <ColumnsSearchResultCountBadge
555
+ selector={selector}
556
+ gridApi={params.api}
557
+ scope="selected"
558
+ />
559
+ </button>
560
+ ),
561
+ cellRenderer: (params: CustomCellRendererProps<T>) => {
562
+ const data = params.data;
563
+ if (!data) {
564
+ return null;
565
+ }
566
+
567
+ return (
568
+ <div
569
+ className="flex h-full w-full cursor-pointer items-center"
570
+ title={`[${data.name}]\nDouble-click to add column`}
571
+ onDoubleClick={() => {
572
+ selector.setSelectedColumns(
573
+ selector.selectedColumns.filter(
574
+ (column) => column !== data,
575
+ ),
576
+ );
577
+ params.api.clearFocusedCell();
578
+ }}
579
+ >
580
+ {columnLabelRenderer?.({
581
+ selector,
582
+ column: data,
583
+ }) ?? (
584
+ <div className="h-full flex-1 items-center overflow-hidden overflow-ellipsis whitespace-nowrap pl-2">
585
+ {data.name}
586
+ </div>
587
+ )}
588
+ <div className="flex h-full">
589
+ {columnActionRenderer?.({
590
+ selector,
591
+ column: data,
592
+ }) ?? null}
593
+ </div>
594
+ </div>
595
+ );
596
+ },
597
+ },
598
+ ]}
599
+ />
600
+ </div>
601
+ </div>
602
+ </div>
603
+ </div>
604
+ );
605
+ },
606
+ );
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Copyright (c) 2020-present, Goldman Sachs
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { observer } from 'mobx-react-lite';
18
+ import type { DataCubeViewState } from '../../../stores/view/DataCubeViewState.js';
19
+
20
+ export const DataCubeMultidimensionalGrid = observer(
21
+ (props: { view: DataCubeViewState }) => {
22
+ return null;
23
+ },
24
+ );
@@ -22,6 +22,7 @@ import {
22
22
  PRIMITIVE_TYPE,
23
23
  V1_deserializeValueSpecification,
24
24
  V1_serializeValueSpecification,
25
+ type V1_ExecuteInput,
25
26
  } from '@finos/legend-graph';
26
27
  import {
27
28
  getFilterOperation,
@@ -113,6 +114,11 @@ export type DataCubeExecutionResult = {
113
114
  executionTime: number;
114
115
  };
115
116
 
117
+ export class DataCubeExecutionError extends Error {
118
+ executeInput?: PlainObject<V1_ExecuteInput> | undefined;
119
+ queryCode?: string | undefined;
120
+ }
121
+
116
122
  /**
117
123
  * This is the base engine of DataCube, it provides capabilities that DataCube cannot itself
118
124
  * handle, such as query execution, compilation, etc.
@@ -279,7 +285,9 @@ export abstract class DataCubeEngine {
279
285
  return undefined;
280
286
  }
281
287
 
282
- async disposeCache(source: CachedDataCubeSource) {}
288
+ async disposeCache(source: CachedDataCubeSource) {
289
+ // do nothing
290
+ }
283
291
 
284
292
  // ---------------------------------- APPLICATION ----------------------------------
285
293
 
@@ -21,10 +21,11 @@ import {
21
21
  } from '@finos/legend-shared';
22
22
  import { DataCubeSource } from './DataCubeSource.js';
23
23
  import { createModelSchema, primitive, raw } from 'serializr';
24
+ import type { V1_PureModelContext } from '@finos/legend-graph';
24
25
 
25
26
  export class AdhocQueryDataCubeSource extends DataCubeSource {
26
27
  runtime!: string;
27
- model!: PlainObject;
28
+ model!: PlainObject<V1_PureModelContext>;
28
29
  }
29
30
 
30
31
  export const ADHOC_QUERY_DATA_CUBE_SOURCE_TYPE = 'adhocQuery';
@@ -32,7 +33,7 @@ export const ADHOC_QUERY_DATA_CUBE_SOURCE_TYPE = 'adhocQuery';
32
33
  export class RawAdhocQueryDataCubeSource {
33
34
  query!: string;
34
35
  runtime!: string;
35
- model!: PlainObject;
36
+ model!: PlainObject<V1_PureModelContext>;
36
37
 
37
38
  static readonly serialization = new SerializationFactory(
38
39
  createModelSchema(RawAdhocQueryDataCubeSource, {