@finos/legend-data-cube 0.1.11 → 0.1.13

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 (175) hide show
  1. package/lib/components/DataCubePlaceholder.d.ts.map +1 -1
  2. package/lib/components/DataCubePlaceholder.js +2 -1
  3. package/lib/components/DataCubePlaceholder.js.map +1 -1
  4. package/lib/components/core/DataCubePureCodeEditorUtils.d.ts +2 -1
  5. package/lib/components/core/DataCubePureCodeEditorUtils.d.ts.map +1 -1
  6. package/lib/components/core/DataCubePureCodeEditorUtils.js.map +1 -1
  7. package/lib/components/view/editor/DataCubeEditor.d.ts.map +1 -1
  8. package/lib/components/view/editor/DataCubeEditor.js +4 -2
  9. package/lib/components/view/editor/DataCubeEditor.js.map +1 -1
  10. package/lib/components/view/editor/DataCubeEditorColumnsPanel.d.ts.map +1 -1
  11. package/lib/components/view/editor/DataCubeEditorColumnsPanel.js +3 -3
  12. package/lib/components/view/editor/DataCubeEditorColumnsPanel.js.map +1 -1
  13. package/lib/components/view/editor/DataCubeEditorColumnsSelector.d.ts +47 -0
  14. package/lib/components/view/editor/DataCubeEditorColumnsSelector.d.ts.map +1 -0
  15. package/lib/components/view/editor/{DataCubeEditorColumnSelector.js → DataCubeEditorColumnsSelector.js} +70 -55
  16. package/lib/components/view/editor/DataCubeEditorColumnsSelector.js.map +1 -0
  17. package/lib/components/view/editor/DataCubeEditorDimensionsPanel.d.ts +3 -13
  18. package/lib/components/view/editor/DataCubeEditorDimensionsPanel.d.ts.map +1 -1
  19. package/lib/components/view/editor/DataCubeEditorDimensionsPanel.js +707 -264
  20. package/lib/components/view/editor/DataCubeEditorDimensionsPanel.js.map +1 -1
  21. package/lib/components/view/editor/DataCubeEditorHorizontalPivotsPanel.js +2 -2
  22. package/lib/components/view/editor/DataCubeEditorHorizontalPivotsPanel.js.map +1 -1
  23. package/lib/components/view/editor/DataCubeEditorSortsPanel.js +2 -2
  24. package/lib/components/view/editor/DataCubeEditorSortsPanel.js.map +1 -1
  25. package/lib/components/view/editor/DataCubeEditorVerticalPivotsPanel.js +2 -2
  26. package/lib/components/view/editor/DataCubeEditorVerticalPivotsPanel.js.map +1 -1
  27. package/lib/components/view/{grid/DataCubeGridShared.d.ts → extend/DataCubeCodeEditor.d.ts} +7 -3
  28. package/lib/components/view/extend/DataCubeCodeEditor.d.ts.map +1 -0
  29. package/lib/components/view/extend/DataCubeCodeEditor.js +114 -0
  30. package/lib/components/view/extend/DataCubeCodeEditor.js.map +1 -0
  31. package/lib/components/view/extend/DataCubeCodeEditorState.d.ts +43 -0
  32. package/lib/components/view/extend/DataCubeCodeEditorState.d.ts.map +1 -0
  33. package/lib/components/view/extend/DataCubeCodeEditorState.js +37 -0
  34. package/lib/components/view/extend/DataCubeCodeEditorState.js.map +1 -0
  35. package/lib/components/view/extend/DataCubeColumnEditor.d.ts.map +1 -1
  36. package/lib/components/view/extend/DataCubeColumnEditor.js +5 -84
  37. package/lib/components/view/extend/DataCubeColumnEditor.js.map +1 -1
  38. package/lib/components/view/grid/DataCubeGrid.d.ts +2 -1
  39. package/lib/components/view/grid/DataCubeGrid.d.ts.map +1 -1
  40. package/lib/components/view/grid/DataCubeGrid.js +29 -19
  41. package/lib/components/view/grid/DataCubeGrid.js.map +1 -1
  42. package/lib/components/view/grid/DataCubeMultidimensionalGrid.d.ts +1 -1
  43. package/lib/components/view/grid/DataCubeMultidimensionalGrid.d.ts.map +1 -1
  44. package/lib/components/view/grid/DataCubeMultidimensionalGrid.js +2 -1
  45. package/lib/components/view/grid/DataCubeMultidimensionalGrid.js.map +1 -1
  46. package/lib/index.css +2 -2
  47. package/lib/index.css.map +1 -1
  48. package/lib/index.d.ts +3 -0
  49. package/lib/index.d.ts.map +1 -1
  50. package/lib/index.js +3 -0
  51. package/lib/index.js.map +1 -1
  52. package/lib/package.json +1 -1
  53. package/lib/stores/DataCubeAPI.d.ts.map +1 -1
  54. package/lib/stores/DataCubeAPI.js +3 -1
  55. package/lib/stores/DataCubeAPI.js.map +1 -1
  56. package/lib/stores/core/DataCubeEngine.d.ts +1 -1
  57. package/lib/stores/core/DataCubeEngine.d.ts.map +1 -1
  58. package/lib/stores/core/DataCubeQueryEngine.d.ts +5 -0
  59. package/lib/stores/core/DataCubeQueryEngine.d.ts.map +1 -1
  60. package/lib/stores/core/DataCubeQueryEngine.js +6 -0
  61. package/lib/stores/core/DataCubeQueryEngine.js.map +1 -1
  62. package/lib/stores/core/DataCubeSnapshot.d.ts +1 -1
  63. package/lib/stores/core/DataCubeSnapshot.d.ts.map +1 -1
  64. package/lib/stores/core/model/AdhocQueryDataCubeSource.d.ts +2 -0
  65. package/lib/stores/core/model/AdhocQueryDataCubeSource.d.ts.map +1 -1
  66. package/lib/stores/core/model/AdhocQueryDataCubeSource.js +3 -0
  67. package/lib/stores/core/model/AdhocQueryDataCubeSource.js.map +1 -1
  68. package/lib/stores/core/model/DataCubeConfiguration.d.ts +11 -0
  69. package/lib/stores/core/model/DataCubeConfiguration.d.ts.map +1 -1
  70. package/lib/stores/core/model/DataCubeConfiguration.js +19 -1
  71. package/lib/stores/core/model/DataCubeConfiguration.js.map +1 -1
  72. package/lib/stores/services/DataCubeSettingService.d.ts.map +1 -1
  73. package/lib/stores/services/DataCubeSettingService.js +10 -6
  74. package/lib/stores/services/DataCubeSettingService.js.map +1 -1
  75. package/lib/stores/view/DataCubeInfoState.d.ts +2 -0
  76. package/lib/stores/view/DataCubeInfoState.d.ts.map +1 -1
  77. package/lib/stores/view/DataCubeInfoState.js +3 -0
  78. package/lib/stores/view/DataCubeInfoState.js.map +1 -1
  79. package/lib/stores/view/editor/DataCubeEditorColumnsPanelState.d.ts +8 -8
  80. package/lib/stores/view/editor/DataCubeEditorColumnsPanelState.d.ts.map +1 -1
  81. package/lib/stores/view/editor/DataCubeEditorColumnsPanelState.js +6 -6
  82. package/lib/stores/view/editor/DataCubeEditorColumnsPanelState.js.map +1 -1
  83. package/lib/stores/view/editor/{DataCubeEditorColumnSelectorState.d.ts → DataCubeEditorColumnsSelectorState.d.ts} +6 -6
  84. package/lib/stores/view/editor/DataCubeEditorColumnsSelectorState.d.ts.map +1 -0
  85. package/lib/stores/view/editor/{DataCubeEditorColumnSelectorState.js → DataCubeEditorColumnsSelectorState.js} +4 -4
  86. package/lib/stores/view/editor/DataCubeEditorColumnsSelectorState.js.map +1 -0
  87. package/lib/stores/view/editor/DataCubeEditorDimensionsPanelState.d.ts +29 -6
  88. package/lib/stores/view/editor/DataCubeEditorDimensionsPanelState.d.ts.map +1 -1
  89. package/lib/stores/view/editor/DataCubeEditorDimensionsPanelState.js +102 -39
  90. package/lib/stores/view/editor/DataCubeEditorDimensionsPanelState.js.map +1 -1
  91. package/lib/stores/view/editor/DataCubeEditorHorizontalPivotsPanelState.d.ts +5 -5
  92. package/lib/stores/view/editor/DataCubeEditorHorizontalPivotsPanelState.d.ts.map +1 -1
  93. package/lib/stores/view/editor/DataCubeEditorHorizontalPivotsPanelState.js +7 -6
  94. package/lib/stores/view/editor/DataCubeEditorHorizontalPivotsPanelState.js.map +1 -1
  95. package/lib/stores/view/editor/DataCubeEditorMutableConfiguration.d.ts +2 -1
  96. package/lib/stores/view/editor/DataCubeEditorMutableConfiguration.d.ts.map +1 -1
  97. package/lib/stores/view/editor/DataCubeEditorMutableConfiguration.js +5 -0
  98. package/lib/stores/view/editor/DataCubeEditorMutableConfiguration.js.map +1 -1
  99. package/lib/stores/view/editor/DataCubeEditorSortsPanelState.d.ts +5 -5
  100. package/lib/stores/view/editor/DataCubeEditorSortsPanelState.d.ts.map +1 -1
  101. package/lib/stores/view/editor/DataCubeEditorSortsPanelState.js +6 -6
  102. package/lib/stores/view/editor/DataCubeEditorSortsPanelState.js.map +1 -1
  103. package/lib/stores/view/editor/DataCubeEditorState.d.ts +3 -0
  104. package/lib/stores/view/editor/DataCubeEditorState.d.ts.map +1 -1
  105. package/lib/stores/view/editor/DataCubeEditorState.js +6 -0
  106. package/lib/stores/view/editor/DataCubeEditorState.js.map +1 -1
  107. package/lib/stores/view/editor/DataCubeEditorVerticalPivotsPanelState.d.ts +5 -5
  108. package/lib/stores/view/editor/DataCubeEditorVerticalPivotsPanelState.d.ts.map +1 -1
  109. package/lib/stores/view/editor/DataCubeEditorVerticalPivotsPanelState.js +6 -6
  110. package/lib/stores/view/editor/DataCubeEditorVerticalPivotsPanelState.js.map +1 -1
  111. package/lib/stores/view/extend/DataCubeColumnEditorState.d.ts +7 -14
  112. package/lib/stores/view/extend/DataCubeColumnEditorState.d.ts.map +1 -1
  113. package/lib/stores/view/extend/DataCubeColumnEditorState.js +14 -22
  114. package/lib/stores/view/extend/DataCubeColumnEditorState.js.map +1 -1
  115. package/lib/stores/view/grid/DataCubeGridClientEngine.d.ts +2 -1
  116. package/lib/stores/view/grid/DataCubeGridClientEngine.d.ts.map +1 -1
  117. package/lib/stores/view/grid/DataCubeGridClientEngine.js +7 -0
  118. package/lib/stores/view/grid/DataCubeGridClientEngine.js.map +1 -1
  119. package/lib/stores/view/grid/DataCubeGridClientExportEngine.js +1 -1
  120. package/lib/stores/view/grid/DataCubeGridClientExportEngine.js.map +1 -1
  121. package/lib/stores/view/grid/DataCubeGridMenuBuilder.d.ts +2 -0
  122. package/lib/stores/view/grid/DataCubeGridMenuBuilder.d.ts.map +1 -1
  123. package/lib/stores/view/grid/DataCubeGridMenuBuilder.js +9 -16
  124. package/lib/stores/view/grid/DataCubeGridMenuBuilder.js.map +1 -1
  125. package/lib/stores/view/grid/DataCubeGridState.d.ts +2 -2
  126. package/lib/stores/view/grid/DataCubeGridState.d.ts.map +1 -1
  127. package/lib/stores/view/grid/DataCubeGridState.js +19 -6
  128. package/lib/stores/view/grid/DataCubeGridState.js.map +1 -1
  129. package/package.json +5 -5
  130. package/src/components/DataCubePlaceholder.tsx +2 -0
  131. package/src/components/core/DataCubePureCodeEditorUtils.tsx +2 -1
  132. package/src/components/view/editor/DataCubeEditor.tsx +8 -3
  133. package/src/components/view/editor/DataCubeEditorColumnsPanel.tsx +6 -8
  134. package/src/components/view/editor/{DataCubeEditorColumnSelector.tsx → DataCubeEditorColumnsSelector.tsx} +108 -83
  135. package/src/components/view/editor/DataCubeEditorDimensionsPanel.tsx +1259 -503
  136. package/src/components/view/editor/DataCubeEditorHorizontalPivotsPanel.tsx +5 -5
  137. package/src/components/view/editor/DataCubeEditorSortsPanel.tsx +10 -10
  138. package/src/components/view/editor/DataCubeEditorVerticalPivotsPanel.tsx +2 -2
  139. package/src/components/view/extend/DataCubeCodeEditor.tsx +175 -0
  140. package/src/components/view/extend/DataCubeCodeEditorState.tsx +72 -0
  141. package/src/components/view/extend/DataCubeColumnEditor.tsx +5 -133
  142. package/src/components/view/grid/DataCubeGrid.tsx +70 -53
  143. package/src/components/view/grid/DataCubeMultidimensionalGrid.tsx +3 -1
  144. package/src/index.tsx +3 -0
  145. package/src/stores/DataCubeAPI.ts +3 -1
  146. package/src/stores/core/DataCubeEngine.tsx +1 -1
  147. package/src/stores/core/DataCubeQueryEngine.ts +6 -0
  148. package/src/stores/core/DataCubeSnapshot.ts +1 -1
  149. package/src/stores/core/model/AdhocQueryDataCubeSource.ts +3 -0
  150. package/src/stores/core/model/DataCubeConfiguration.ts +31 -0
  151. package/src/stores/services/DataCubeSettingService.tsx +10 -6
  152. package/src/stores/view/DataCubeInfoState.ts +4 -0
  153. package/src/stores/view/editor/DataCubeEditorColumnsPanelState.ts +13 -13
  154. package/src/stores/view/editor/{DataCubeEditorColumnSelectorState.ts → DataCubeEditorColumnsSelectorState.ts} +6 -6
  155. package/src/stores/view/editor/DataCubeEditorDimensionsPanelState.ts +162 -58
  156. package/src/stores/view/editor/DataCubeEditorHorizontalPivotsPanelState.ts +12 -11
  157. package/src/stores/view/editor/DataCubeEditorMutableConfiguration.ts +8 -0
  158. package/src/stores/view/editor/DataCubeEditorSortsPanelState.ts +10 -10
  159. package/src/stores/view/editor/DataCubeEditorState.tsx +6 -0
  160. package/src/stores/view/editor/DataCubeEditorVerticalPivotsPanelState.ts +11 -11
  161. package/src/stores/view/extend/DataCubeColumnEditorState.tsx +16 -26
  162. package/src/stores/view/grid/DataCubeGridClientEngine.ts +8 -0
  163. package/src/stores/view/grid/DataCubeGridClientExportEngine.ts +1 -1
  164. package/src/stores/view/grid/DataCubeGridMenuBuilder.tsx +26 -1
  165. package/src/stores/view/grid/DataCubeGridState.ts +23 -6
  166. package/tsconfig.json +4 -3
  167. package/lib/components/view/editor/DataCubeEditorColumnSelector.d.ts +0 -38
  168. package/lib/components/view/editor/DataCubeEditorColumnSelector.d.ts.map +0 -1
  169. package/lib/components/view/editor/DataCubeEditorColumnSelector.js.map +0 -1
  170. package/lib/components/view/grid/DataCubeGridShared.d.ts.map +0 -1
  171. package/lib/components/view/grid/DataCubeGridShared.js +0 -25
  172. package/lib/components/view/grid/DataCubeGridShared.js.map +0 -1
  173. package/lib/stores/view/editor/DataCubeEditorColumnSelectorState.d.ts.map +0 -1
  174. package/lib/stores/view/editor/DataCubeEditorColumnSelectorState.js.map +0 -1
  175. package/src/components/view/grid/DataCubeGridShared.tsx +0 -42
@@ -15,590 +15,1346 @@
15
15
  */
16
16
 
17
17
  import { observer } from 'mobx-react-lite';
18
- import { DataCubeIcon } from '@finos/legend-art';
18
+ import { cn, DataCubeIcon } from '@finos/legend-art';
19
19
  import {
20
- type ColDef,
21
- type ColDefField,
22
20
  type GridApi,
23
- type ModelUpdatedEvent,
24
- type RowDragEndEvent,
21
+ type IRowNode,
22
+ type RowDragEvent,
25
23
  type SelectionChangedEvent,
26
24
  AllCommunityModule,
27
25
  } from 'ag-grid-community';
28
- import React, { useCallback, useEffect, useRef, useState } from 'react';
26
+ import { useCallback, useEffect, useRef, useState } from 'react';
29
27
  import {
30
28
  AgGridReact,
31
- type AgGridReactProps,
32
29
  type CustomCellRendererProps,
33
30
  type CustomNoRowsOverlayProps,
34
31
  } from 'ag-grid-react';
32
+ import { filterByType, isNonNullable } from '@finos/legend-shared';
35
33
  import {
36
- type DataCubeEditorColumnSelectorColumnState,
37
- type DataCubeEditorColumnSelectorState,
38
- } from '../../../stores/view/editor/DataCubeEditorColumnSelectorState.js';
39
- import { isNonNullable } from '@finos/legend-shared';
40
- import {
34
+ getAllNodes,
41
35
  getDataForAllFilteredNodes,
42
- getDataForAllNodes,
43
36
  } from '../../../stores/view/grid/DataCubeGridClientEngine.js';
37
+ import type { DataCubeViewState } from '../../../stores/view/DataCubeViewState.js';
38
+ import {
39
+ FormAlert,
40
+ FormBadge_WIP,
41
+ FormCheckbox,
42
+ } from '../../core/DataCubeFormUtils.js';
43
+ import { DataCubeGridMode } from '../../../stores/core/DataCubeQueryEngine.js';
44
+ import { DataCubeEditorColumnsSelectorColumnState } from '../../../stores/view/editor/DataCubeEditorColumnsSelectorState.js';
45
+ import {
46
+ getColumnsSelectorBaseGridProps,
47
+ INTERNAL__EDITOR_COLUMNS_SELECTOR_ROW_HEIGHT,
48
+ } from './DataCubeEditorColumnsSelector.js';
49
+ import {
50
+ DataCubeEditorDimensionState,
51
+ type DataCubeEditorDimensionsPanelState,
52
+ type DataCubeEditorDimensionsTreeNode,
53
+ } from '../../../stores/view/editor/DataCubeEditorDimensionsPanelState.js';
54
+ import { TreeDataModule } from 'ag-grid-enterprise';
55
+ import { _findCol } from '../../../stores/core/model/DataCubeColumn.js';
56
+ import { AlertType } from '../../../stores/services/DataCubeAlertService.js';
57
+ import { DEFAULT_ALERT_WINDOW_CONFIG } from '../../../stores/services/DataCubeLayoutService.js';
44
58
 
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
- }
59
+ // NOTE: This is a workaround to prevent ag-grid license key check from flooding the console screen
60
+ // with its stack trace in Chrome.
61
+ // We MUST NEVER completely surpress this warning in production, else it's a violation of the ag-grid license!
62
+ // See https://www.ag-grid.com/react-data-grid/licensing/
63
+ const __INTERNAL__original_console_error = console.error; // eslint-disable-line no-console
112
64
 
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
- }
65
+ /**
66
+ * Move this display to a separate component to avoid re-rendering the header too frequently
67
+ */
68
+ const AvailableColumnsSearchResultCountBadge = observer(
69
+ (props: { panel: DataCubeEditorDimensionsPanelState; gridApi: GridApi }) => {
70
+ const { panel, gridApi } = props;
71
+ return (
72
+ <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">
73
+ {`${getDataForAllFilteredNodes(gridApi).length}/${panel.availableColumnsForDisplay.length}`}
74
+ <span className="hidden">
75
+ {/* subscribing to the search text to trigger re-render as it changes */}
76
+ {panel.availableColumnsSearchText}
77
+ </span>
78
+ </div>
79
+ );
80
+ },
81
+ );
128
82
 
129
83
  /**
130
84
  * Move this display to a separate component to avoid re-rendering the header too frequently
131
85
  */
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;
86
+ const DimensionsTreeSearchResultCountBadge = observer(
87
+ (props: { panel: DataCubeEditorDimensionsPanelState; gridApi: GridApi }) => {
88
+ const { panel, gridApi } = props;
141
89
  return (
142
90
  <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}`}
91
+ {`${
92
+ getDataForAllFilteredNodes(gridApi)
93
+ .map((node) => node.data)
94
+ .filter((node) => node instanceof DataCubeEditorDimensionState)
95
+ .length
96
+ }/${panel.dimensions.length}`}
144
97
  <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}
98
+ {/* subscribing to the search text to trigger re-render as it changes */}
99
+ {panel.dimensionsTreeSearchText}
149
100
  </span>
150
101
  </div>
151
102
  );
152
103
  },
153
104
  );
154
105
 
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;
106
+ /**
107
+ * Move this display to a separate component to avoid re-rendering too frequently
108
+ */
109
+ const DimensionLabel = observer(
110
+ (props: {
111
+ panel: DataCubeEditorDimensionsPanelState;
112
+ dimension: DataCubeEditorDimensionState;
113
+ gridApi: GridApi;
114
+ }) => {
115
+ const { panel, dimension, gridApi } = props;
116
+ const nameInputRef = useRef<HTMLInputElement>(null);
117
+ const [name, setName] = useState(dimension.name);
118
+ const validationError =
119
+ name.trim() === ''
120
+ ? `Name is required`
121
+ : Boolean(
122
+ panel.dimensions
123
+ .filter((dim) => dim.name !== dimension.name)
124
+ .find((dim) => dim.name === name),
125
+ )
126
+ ? `Name must be unique`
127
+ : undefined;
128
+ const isNameValid = !validationError;
129
+
130
+ const updateName = () => {
131
+ if (!isNameValid) {
132
+ return;
133
+ }
134
+ dimension.setName(name);
135
+ dimension.setIsRenaming(false);
136
+ gridApi.clearFocusedCell();
137
+ };
138
+
139
+ useEffect(() => {
140
+ setName(dimension.name);
141
+ }, [dimension.name]);
142
+
143
+ useEffect(() => {
144
+ if (dimension.isRenaming) {
145
+ nameInputRef.current?.focus();
146
+ }
147
+ }, [dimension.isRenaming]);
148
+
149
+ return (
150
+ <div
151
+ className="flex h-full w-full items-center pl-1"
152
+ title={`[Dimension: ${dimension.name}]\nDouble-click to remove all columns`}
153
+ >
154
+ <div className="text-2xs flex h-3 flex-shrink-0 items-center justify-center rounded-sm bg-neutral-500 px-1 font-bold text-white">
155
+ DIM
156
+ </div>
157
+ {!dimension.isRenaming && (
158
+ <div
159
+ className="h-full flex-1 cursor-pointer items-center overflow-hidden overflow-ellipsis whitespace-nowrap pl-1"
160
+ title="Double-click to remove dimension"
161
+ /**
162
+ * ag-grid row select event listener is at a deeper layer so we need to stop
163
+ * the propagation as event capturing is happening, not when it's bubbling.
164
+ */
165
+ onDoubleClickCapture={(event) => {
166
+ event.stopPropagation();
167
+ dimension.setIsRenaming(true);
168
+ gridApi.clearFocusedCell();
169
+ }}
170
+ >
171
+ {dimension.name}
172
+ </div>
173
+ )}
174
+ {dimension.isRenaming && (
175
+ <div
176
+ className="relative flex items-center"
177
+ /**
178
+ * ag-grid row select event listener is at a deeper layer so we need to stop
179
+ * the propagation as event capturing is happening, not when it's bubbling.
180
+ */
181
+ onDoubleClickCapture={(event) => {
182
+ event.stopPropagation();
183
+ gridApi.clearFocusedCell();
184
+ }}
185
+ >
186
+ <input
187
+ ref={nameInputRef}
188
+ className={cn(
189
+ 'ml-0.5 h-4 border border-neutral-300 pl-[1px] pr-6 focus:border-sky-600 focus:!outline-none',
190
+ {
191
+ 'border-red-600 focus:border-red-600': !isNameValid,
192
+ },
193
+ )}
194
+ value={name}
195
+ onChange={(event) => {
196
+ setName(event.target.value);
197
+ }}
198
+ onKeyDown={(event) => {
199
+ if (event.code === 'Escape') {
200
+ nameInputRef.current?.focus();
201
+ nameInputRef.current?.select();
202
+ }
203
+ }}
204
+ onKeyDownCapture={(event) => {
205
+ if (event.code === 'Enter') {
206
+ event.stopPropagation();
207
+ updateName();
208
+ }
209
+ }}
210
+ onBlur={() => {
211
+ dimension.setIsRenaming(false);
212
+ setName(dimension.name);
213
+ }}
214
+ />
215
+ {isNameValid && (
216
+ <button
217
+ className="text-2xs absolute right-0.5 flex h-3 w-5 items-center justify-center rounded-sm bg-sky-600 text-white"
218
+ onClick={(event) => {
219
+ event.stopPropagation();
220
+ updateName();
221
+ }}
222
+ >
223
+ Save
224
+ </button>
225
+ )}
226
+ </div>
227
+ )}
228
+ </div>
229
+ );
230
+ },
231
+ );
232
+
233
+ function confirmPopulateStubDimensionWhenAddingColumns(
234
+ view: DataCubeViewState,
235
+ handler: () => void,
236
+ ) {
237
+ view.alertService.alert({
238
+ message: `No target dimensions to add columns to`,
239
+ text: `Columns need to be added to a dimension, but no dimension has been specified.\nDo you want to proceed with creating a new dimension and add the columns to it?`,
240
+ type: AlertType.WARNING,
241
+ actions: [
242
+ {
243
+ label: 'Proceed',
244
+ handler,
245
+ },
246
+ {
247
+ label: 'Cancel',
248
+ handler: () => {},
249
+ },
250
+ ],
251
+ windowConfig: {
252
+ ...DEFAULT_ALERT_WINDOW_CONFIG,
253
+ width: 550,
254
+ height: 180,
255
+ },
256
+ });
257
+ }
258
+
259
+ function computeDragOverNode<T = DataCubeEditorDimensionsTreeNode>(
260
+ event: RowDragEvent,
261
+ ) {
262
+ let overIndex = event.overIndex === -1 ? undefined : event.overIndex;
263
+
264
+ // if the drag point passes the middle of the row, we consider
265
+ // the spot to be the one before the row
266
+ if (
267
+ overIndex !== undefined &&
268
+ event.y % INTERNAL__EDITOR_COLUMNS_SELECTOR_ROW_HEIGHT <
269
+ INTERNAL__EDITOR_COLUMNS_SELECTOR_ROW_HEIGHT / 2
270
+ ) {
271
+ overIndex -= 1;
272
+ }
273
+
274
+ return {
275
+ overIndex,
276
+ overNode: (overIndex !== undefined
277
+ ? getAllNodes(event.api).at(overIndex === -1 ? 0 : overIndex)
278
+ : undefined) as IRowNode<T> | undefined,
279
+ };
280
+ }
281
+
282
+ export const DataCubeEditorDimensionsPanel = observer(
283
+ (props: { view: DataCubeViewState }) => {
284
+ const { view } = props;
285
+ const editor = view.editor;
286
+ const panel = editor.dimensions;
287
+
288
+ const searchAvailableColumnsInputRef = useRef<HTMLInputElement | null>(
289
+ null,
290
+ );
291
+ const searchDimensionsTreeInputRef = useRef<HTMLInputElement | null>(null);
180
292
  const [selectedAvailableColumns, setSelectedAvailableColumns] = useState<
181
- T[]
293
+ DataCubeEditorColumnsSelectorColumnState[]
294
+ >([]);
295
+ const [selectedSelectedColumns, setSelectedSelectedColumns] = useState<
296
+ DataCubeEditorColumnsSelectorColumnState[]
297
+ >([]);
298
+ const [selectedDimensions, setSelectedDimensions] = useState<
299
+ DataCubeEditorDimensionState[]
182
300
  >([]);
183
- const [selectedSelectedColumns, setSelectedSelectedColumns] = useState<T[]>(
184
- [],
185
- );
186
301
  const [availableColumnsGridApi, setAvailableColumnsGridApi] =
187
302
  useState<GridApi | null>(null);
188
- const [selectedColumnsGridApi, setSelectedColumnsGridApi] =
303
+
304
+ const [dimensionsTreeGridApi, setDimensionsTreeGridApi] =
189
305
  useState<GridApi | null>(null);
190
- const searchAvailableColumnsInputRef = useRef<HTMLInputElement | null>(
191
- null,
192
- );
193
- const searchSelectedColumnsInputRef = useRef<HTMLInputElement | null>(null);
194
306
 
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));
307
+ const onAvailableColumnsExternalDragStop = useCallback(
308
+ (params: RowDragEvent<DataCubeEditorDimensionsTreeNode>) => {
309
+ // NOTE: here, we do not scope the columns being moved by the search
310
+ // this is a complicated behavior to implement and it's not clear if
311
+ // it's necessary, the current behavior is sensible in its own way.
312
+ const columnsToMove = params.nodes
313
+ .map((node) => node.data?.data)
314
+ .filter(filterByType(DataCubeEditorColumnsSelectorColumnState));
315
+ panel.deselectColumns(columnsToMove);
316
+ panel.refreshDimensionsTreeData();
204
317
  },
205
- [selector],
318
+ [panel],
206
319
  );
207
320
 
208
- const onAvailableColumnsDragStop = useCallback(
209
- (params: RowDragEndEvent<T>) => {
210
- const nodes = params.nodes;
211
- const columnsToMove = nodes
321
+ const onDimensionsTreeExternalDragStop = useCallback(
322
+ (event: RowDragEvent<DataCubeEditorColumnsSelectorColumnState>) => {
323
+ const { overNode } = computeDragOverNode(event);
324
+
325
+ // NOTE: here, we do not scope the columns being moved by the search
326
+ // this is a complicated behavior to implement and it's not clear if
327
+ // it's necessary, the current behavior is sensible in its own way.
328
+ const columnsToMove = event.nodes
212
329
  .map((node) => node.data)
213
330
  .filter(isNonNullable);
214
- selector.setSelectedColumns(
215
- selector.selectedColumns.filter(
216
- (column) => !columnsToMove.includes(column),
217
- ),
218
- );
219
- },
220
- [selector],
221
- );
222
331
 
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;
332
+ const dimension =
333
+ (!overNode?.data
334
+ ? undefined
335
+ : overNode.data.data instanceof DataCubeEditorDimensionState
336
+ ? overNode.data.data
337
+ : overNode.parent?.data?.data instanceof
338
+ DataCubeEditorDimensionState
339
+ ? overNode.parent.data.data
340
+ : undefined) ?? panel.dimensions.at(-1);
341
+ if (dimension) {
342
+ // dropping position will be honored and affect columns
343
+ // ordering within the dimension accordingly
344
+ const colIdx =
345
+ overNode?.data?.data instanceof
346
+ DataCubeEditorColumnsSelectorColumnState
347
+ ? dimension.columns.indexOf(overNode.data.data)
348
+ : overNode?.data?.data instanceof DataCubeEditorDimensionState
349
+ ? -1
350
+ : undefined;
351
+ dimension.setColumns(
352
+ colIdx !== undefined
353
+ ? [
354
+ ...dimension.columns.slice(0, colIdx + 1),
355
+ ...columnsToMove,
356
+ ...dimension.columns.slice(colIdx + 1),
357
+ ]
358
+ : [...dimension.columns, ...columnsToMove],
359
+ );
360
+
361
+ panel.refreshDimensionsTreeData();
362
+ } else {
363
+ confirmPopulateStubDimensionWhenAddingColumns(view, () => {
364
+ const newDimension = panel.newDimension();
365
+ newDimension.setColumns([...columnsToMove]);
366
+ panel.refreshDimensionsTreeData();
367
+ });
233
368
  }
234
- selector.setSelectedColumns(getDataForAllNodes(event.api));
235
369
  },
236
- [selector],
370
+ [panel, view],
237
371
  );
238
372
 
239
373
  /**
240
- * Setup row drop zones for each grid to be the other
374
+ * Setup drop zones for each grid to allow moving columns between them
241
375
  * See https://www.ag-grid.com/react-data-grid/row-dragging-to-grid/
242
376
  */
243
377
  useEffect(() => {
244
- if (!availableColumnsGridApi || !selectedColumnsGridApi) {
378
+ if (!availableColumnsGridApi || !dimensionsTreeGridApi) {
245
379
  return;
246
380
  }
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);
381
+
382
+ const dimensionstreeDropZoneParams = !dimensionsTreeGridApi.isDestroyed()
383
+ ? dimensionsTreeGridApi.getRowDropZoneParams({
384
+ onDragStop: (event) => {
385
+ onDimensionsTreeExternalDragStop(event);
386
+ availableColumnsGridApi.clearFocusedCell();
387
+ },
388
+ })
389
+ : undefined;
390
+ if (
391
+ dimensionstreeDropZoneParams &&
392
+ !availableColumnsGridApi.isDestroyed()
393
+ ) {
394
+ availableColumnsGridApi.addRowDropZone(dimensionstreeDropZoneParams);
259
395
  }
260
396
 
261
397
  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);
398
+ !availableColumnsGridApi.isDestroyed()
399
+ ? availableColumnsGridApi.getRowDropZoneParams({
400
+ onDragStop: (event) => {
401
+ onAvailableColumnsExternalDragStop(event);
402
+ dimensionsTreeGridApi.clearFocusedCell();
403
+ },
404
+ })
405
+ : undefined;
406
+ if (
407
+ availableColumnsDropZoneParams &&
408
+ !dimensionsTreeGridApi.isDestroyed()
409
+ ) {
410
+ dimensionsTreeGridApi.addRowDropZone(availableColumnsDropZoneParams);
273
411
  }
274
412
  }, [
275
413
  availableColumnsGridApi,
276
- selectedColumnsGridApi,
277
- onSelectedColumnsDragStop,
278
- onAvailableColumnsDragStop,
414
+ dimensionsTreeGridApi,
415
+ onDimensionsTreeExternalDragStop,
416
+ onAvailableColumnsExternalDragStop,
279
417
  ]);
280
418
 
419
+ const [dimensionsTreeOnHoverIndex, setDimensionsTreeOnHoverIndex] =
420
+ useState<number | undefined>(undefined);
421
+
422
+ // This event will be triggered when user drops any node on the dimensions tree,
423
+ // regardless of the source, i.e. this includes columns from available columns grid
424
+ // so we have to guard against that case because the logic should already been handled
425
+ // by the drag stop hook in the external drop zone config for dimensions tree.
426
+ const isDimensionsTreeRowDragEnabled =
427
+ selectedDimensions.length === 0 || selectedSelectedColumns.length === 0;
428
+ const onDimensionsTreeRowDragEnd = useCallback(
429
+ (event: RowDragEvent<DataCubeEditorDimensionsTreeNode>) => {
430
+ const { overIndex, overNode } = computeDragOverNode(event);
431
+
432
+ setDimensionsTreeOnHoverIndex(undefined);
433
+
434
+ // NOTE: here, we do not scope the columns being moved by the search
435
+ // this is a complicated behavior to implement and it's not clear if
436
+ // it's necessary, the current behavior is sensible in its own way.
437
+ const dragEntities = event.nodes
438
+ .map(
439
+ (node) =>
440
+ // this assertion is valid because we also need to handle columns being dragged from external sources
441
+ node.data as
442
+ | DataCubeEditorDimensionsTreeNode
443
+ | DataCubeEditorColumnsSelectorColumnState,
444
+ )
445
+ .filter(
446
+ (data): data is DataCubeEditorDimensionsTreeNode =>
447
+ !(data instanceof DataCubeEditorDimensionState),
448
+ );
449
+
450
+ // we guard against DnD from the available columns grid to the dimensions tree
451
+ if (dragEntities.length === 0) {
452
+ return;
453
+ }
454
+
455
+ const dragColumns: DataCubeEditorColumnsSelectorColumnState[] = [];
456
+ const dragDimensions: DataCubeEditorDimensionState[] = [];
457
+ dragEntities.forEach((node) => {
458
+ if (node.data instanceof DataCubeEditorDimensionState) {
459
+ dragDimensions.push(node.data);
460
+ } else if (
461
+ node.data instanceof DataCubeEditorColumnsSelectorColumnState
462
+ ) {
463
+ dragColumns.push(node.data);
464
+ }
465
+ });
466
+
467
+ if (dragColumns.length > 0 && dragDimensions.length > 0) {
468
+ // we don't support DnD for a mixture of columns and dimensions
469
+ return;
470
+ }
471
+
472
+ // moving dimensions
473
+ if (dragDimensions.length > 0) {
474
+ // dropping position will be honored and affect dimensions ordering
475
+ let dimensionIdx: number | undefined = undefined;
476
+ if (overIndex === -1) {
477
+ dimensionIdx = -1;
478
+ } else if (overNode !== undefined) {
479
+ const dimension = !overNode.data
480
+ ? undefined
481
+ : overNode.data.data instanceof DataCubeEditorDimensionState
482
+ ? overNode.data.data
483
+ : overNode.parent?.data?.data instanceof
484
+ DataCubeEditorDimensionState
485
+ ? overNode.parent.data.data
486
+ : undefined;
487
+ if (dimension) {
488
+ const _idx = panel.dimensions.indexOf(dimension);
489
+ // when compute the move position, account for dimensions which will be moved out
490
+ // these would shift the move position up.
491
+ dimensionIdx =
492
+ _idx === -1
493
+ ? -1
494
+ : _idx -
495
+ panel.dimensions
496
+ .slice(0, _idx + 1)
497
+ .filter((dim) =>
498
+ dragDimensions.find((_dim) => dim.name === _dim.name),
499
+ ).length;
500
+ }
501
+ }
502
+
503
+ const dimensions = panel.dimensions.filter(
504
+ (dim) => !dragDimensions.find((_dim) => dim.name === _dim.name),
505
+ );
506
+ panel.setDimensions(
507
+ dimensionIdx !== undefined
508
+ ? [
509
+ ...dimensions.slice(0, dimensionIdx + 1),
510
+ ...dragDimensions,
511
+ ...dimensions.slice(dimensionIdx + 1),
512
+ ]
513
+ : [...dimensions, ...dragDimensions],
514
+ );
515
+ panel.refreshDimensionsTreeData();
516
+ return;
517
+ }
518
+
519
+ // moving columns
520
+ if (dragColumns.length > 0) {
521
+ const dimension =
522
+ (!overNode?.data
523
+ ? undefined
524
+ : overNode.data.data instanceof DataCubeEditorDimensionState
525
+ ? overNode.data.data
526
+ : overNode.parent?.data?.data instanceof
527
+ DataCubeEditorDimensionState
528
+ ? overNode.parent.data.data
529
+ : undefined) ?? panel.dimensions.at(-1);
530
+ if (dimension) {
531
+ // dropping position will be honored and affect columns
532
+ // ordering within the dimension accordingly
533
+ let colIdx: number | undefined = undefined;
534
+ if (overNode?.data?.data instanceof DataCubeEditorDimensionState) {
535
+ colIdx = -1;
536
+ } else if (
537
+ overNode?.data?.data instanceof
538
+ DataCubeEditorColumnsSelectorColumnState
539
+ ) {
540
+ const _idx = dimension.columns.indexOf(overNode.data.data);
541
+ // when compute the move position, account for columns which will be moved out
542
+ // these would shift the move position up.
543
+ colIdx =
544
+ _idx === -1
545
+ ? -1
546
+ : _idx -
547
+ dimension.columns
548
+ .slice(0, _idx + 1)
549
+ .filter((col) => _findCol(dragColumns, col.name)).length;
550
+ }
551
+
552
+ panel.deselectColumns(dragColumns);
553
+ dimension.setColumns(
554
+ colIdx !== undefined
555
+ ? [
556
+ ...dimension.columns.slice(0, colIdx + 1),
557
+ ...dragColumns,
558
+ ...dimension.columns.slice(colIdx + 1),
559
+ ]
560
+ : [...dimension.columns, ...dragColumns],
561
+ );
562
+ panel.refreshDimensionsTreeData();
563
+ }
564
+
565
+ return;
566
+ }
567
+ },
568
+ [panel],
569
+ );
570
+ const onDimensionsTreeRowDragMove = useCallback(
571
+ (event: RowDragEvent<DataCubeEditorDimensionsTreeNode>) => {
572
+ const { overIndex, overNode } = computeDragOverNode(event);
573
+
574
+ setDimensionsTreeOnHoverIndex(overIndex);
575
+ event.api.refreshCells({
576
+ rowNodes: overNode ? [overNode] : [],
577
+ force: true, // since no data change is happening, we need to force refresh
578
+ });
579
+ },
580
+ [],
581
+ );
582
+ const onDimensionsTreeRowDragCancel = useCallback(
583
+ (event: RowDragEvent<DataCubeEditorDimensionsTreeNode>) => {
584
+ setDimensionsTreeOnHoverIndex(undefined);
585
+ },
586
+ [],
587
+ );
588
+ const onDimensionsTreeRowDragLeave = useCallback(
589
+ (event: RowDragEvent<DataCubeEditorDimensionsTreeNode>) => {
590
+ setDimensionsTreeOnHoverIndex(undefined);
591
+ },
592
+ [],
593
+ );
594
+
595
+ useEffect(() => {
596
+ // on load, reset all renaming state for all dimensions
597
+ panel.dimensions.forEach((dimension) => dimension.setIsRenaming(false));
598
+
599
+ // reset transient grid states
600
+ setDimensionsTreeOnHoverIndex(undefined);
601
+ }, [panel]);
602
+
603
+ // eslint-disable-next-line no-process-env
604
+ if (process.env.NODE_ENV !== 'production') {
605
+ // eslint-disable-next-line no-console
606
+ console.error = (message?: unknown, ...agrs: unknown[]) => {
607
+ console.debug(`%c ${message}`, 'color: silver'); // eslint-disable-line no-console
608
+ };
609
+ }
610
+
281
611
  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:
612
+ <div className="h-full w-full select-none p-2">
613
+ <div className="flex h-6">
614
+ <div className="flex h-6 items-center text-xl font-medium">
615
+ <DataCubeIcon.TableDimension />
616
+ </div>
617
+ <div className="ml-1 flex h-6 items-center text-xl font-medium">
618
+ Dimensions
619
+ <FormBadge_WIP />
286
620
  </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)
621
+ </div>
622
+ <div className="h-[calc(100%_-_24px)] w-full">
623
+ <div className="h-10 py-2">
624
+ <div className="flex h-6 w-full items-center">
625
+ <FormCheckbox
626
+ label="Enable multidimensional grid mode"
627
+ checked={
628
+ editor.generalProperties.configuration.gridMode ===
629
+ DataCubeGridMode.MULTIDIMENSIONAL
296
630
  }
297
- onKeyDown={(event) => {
298
- if (event.code === 'Escape') {
299
- event.stopPropagation();
300
- searchAvailableColumnsInputRef.current?.select();
301
- selector.setAvailableColumnsSearchText('');
631
+ onChange={() => {
632
+ if (
633
+ editor.generalProperties.configuration.gridMode ===
634
+ DataCubeGridMode.MULTIDIMENSIONAL
635
+ ) {
636
+ editor.generalProperties.configuration.setGridMode(
637
+ DataCubeGridMode.STANDARD,
638
+ );
639
+ } else {
640
+ if (
641
+ // only suggest converting vertical pivots if no dimensions have been specified
642
+ editor.verticalPivots.selector.selectedColumns.length !==
643
+ 0 &&
644
+ panel.dimensions.length === 0
645
+ ) {
646
+ view.alertService.alert({
647
+ message: `Specified vertical pivots will be ignored in multidimensional grid mode.`,
648
+ text: `Multidimensional grid mode will ignore any specified vertical pivots, essentially, vertical pivot columns can be considered as one dimension.\nFor convenience, these vertical pivots can be converted into a dimension, do you want this conversion to happen?`,
649
+ type: AlertType.WARNING,
650
+ actions: [
651
+ {
652
+ label: 'Yes',
653
+ handler: () => {
654
+ const groupByColumns =
655
+ editor.verticalPivots.selector.selectedColumns;
656
+ const newDimension = panel.newDimension();
657
+ newDimension.setColumns(
658
+ panel.availableColumns.filter((column) =>
659
+ _findCol(groupByColumns, column.name),
660
+ ),
661
+ );
662
+ editor.verticalPivots.selector.setSelectedColumns(
663
+ [],
664
+ );
665
+ panel.refreshDimensionsTreeData();
666
+
667
+ editor.generalProperties.configuration.setGridMode(
668
+ DataCubeGridMode.MULTIDIMENSIONAL,
669
+ );
670
+ },
671
+ },
672
+ {
673
+ label: 'No',
674
+ handler: () => {
675
+ editor.generalProperties.configuration.setGridMode(
676
+ DataCubeGridMode.MULTIDIMENSIONAL,
677
+ );
678
+ },
679
+ },
680
+ ],
681
+ windowConfig: {
682
+ ...DEFAULT_ALERT_WINDOW_CONFIG,
683
+ width: 550,
684
+ height: 220,
685
+ },
686
+ });
687
+ } else {
688
+ editor.generalProperties.configuration.setGridMode(
689
+ DataCubeGridMode.MULTIDIMENSIONAL,
690
+ );
691
+ }
302
692
  }
303
693
  }}
304
694
  />
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
695
  </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>) => (
696
+ </div>
697
+
698
+ {editor.generalProperties.configuration.gridMode !==
699
+ DataCubeGridMode.MULTIDIMENSIONAL && (
700
+ <FormAlert
701
+ message="Multidimensional grid mode is experimental"
702
+ type={AlertType.WARNING}
703
+ text={`This is an alternative grid view for DataCube with a different mode of interaction: dimensions (ordered list of columns to group by) are defined and will be used to explore data "top-down". Initially, the grid will show the root aggregation, then user can drill down into each dimension. This grid mode comes with the following limitations:
704
+ - Specified vertical pivots will be ignored: essentially, vertical pivot can be considered as one dimension in this mode
705
+ - No support for pagination
706
+ - Initial view can take longer to materialize if drilldown is deep`}
707
+ />
708
+ )}
709
+ {editor.generalProperties.configuration.gridMode ===
710
+ DataCubeGridMode.MULTIDIMENSIONAL && (
711
+ <div className="flex h-[calc(100%_-_40px)] w-full">
712
+ <div className="data-cube-column-selector flex h-full w-full">
713
+ <div className="h-full w-[calc(50%_-_20px)]">
714
+ <div className="flex h-5 items-center text-sm">
715
+ Available columns:
716
+ </div>
717
+ <div className="h-[calc(100%_-_20px)] rounded-sm border border-neutral-200">
718
+ <div className="relative h-6 border-b border-neutral-200">
719
+ <input
720
+ className="h-full w-full pl-10 pr-6 placeholder-neutral-400"
721
+ ref={searchAvailableColumnsInputRef}
722
+ placeholder="Search columns..."
723
+ value={panel.availableColumnsSearchText}
724
+ onChange={(event) =>
725
+ panel.setAvailableColumnsSearchText(
726
+ event.target.value,
727
+ )
728
+ }
729
+ onKeyDown={(event) => {
730
+ if (event.code === 'Escape') {
731
+ event.stopPropagation();
732
+ searchAvailableColumnsInputRef.current?.select();
733
+ panel.setAvailableColumnsSearchText('');
734
+ }
735
+ }}
736
+ />
737
+ <div className="absolute left-0 top-0 flex h-6 w-10 items-center justify-center">
738
+ <DataCubeIcon.Search className="stroke-[3px] text-lg text-neutral-500" />
739
+ </div>
346
740
  <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();
741
+ className="absolute right-0 top-0 flex h-6 w-6 items-center justify-center text-neutral-500 disabled:text-neutral-300"
742
+ disabled={!panel.availableColumnsSearchText}
743
+ title="Clear search [Esc]"
744
+ onClick={() => {
745
+ panel.setAvailableColumnsSearchText('');
746
+ searchAvailableColumnsInputRef.current?.focus();
359
747
  }}
360
748
  >
361
- <div>{`[All Columns]`}</div>
362
- <ColumnsSearchResultCountBadge
363
- selector={selector}
364
- gridApi={params.api}
365
- scope="available"
366
- />
749
+ <DataCubeIcon.X className="text-lg" />
367
750
  </button>
368
- ),
369
- cellRenderer: (params: CustomCellRendererProps<T>) => {
370
- const data = params.data;
371
- if (!data) {
372
- return null;
751
+ </div>
752
+ <div className="h-[calc(100%_-_24px)]">
753
+ <AgGridReact
754
+ {...getColumnsSelectorBaseGridProps()}
755
+ // Disable managed row-dragging to disallow changing the order of columns
756
+ // and to make sure the row data and the available columns state are in sync
757
+ rowDragManaged={false}
758
+ onGridReady={(params) =>
759
+ setAvailableColumnsGridApi(params.api)
760
+ }
761
+ onSelectionChanged={(event) => {
762
+ setSelectedAvailableColumns(
763
+ event.api
764
+ .getSelectedNodes()
765
+ .map((node) => node.data)
766
+ .filter(isNonNullable),
767
+ );
768
+ }}
769
+ // Using ag-grid quick filter is a cheap way to implement search
770
+ quickFilterText={panel.availableColumnsSearchText}
771
+ rowData={panel.availableColumnsForDisplay}
772
+ rowClass="border-t border-b border-1 border-transparent"
773
+ columnDefs={[
774
+ {
775
+ field: 'name',
776
+ colId: 'name',
777
+ flex: 1,
778
+ minWidth: 100,
779
+ filter: true,
780
+ sortable: false,
781
+ resizable: false,
782
+ suppressHeaderMenuButton: true,
783
+ getQuickFilterText: (params) => params.value,
784
+ /**
785
+ * Support double-click to add all (filtered by search) columns
786
+ */
787
+ headerComponent: (
788
+ params: CustomCellRendererProps<DataCubeEditorColumnsSelectorColumnState>,
789
+ ) => (
790
+ <button
791
+ title="Double-click to add all columns"
792
+ className="flex h-full w-full items-center justify-between pl-0.5"
793
+ onDoubleClick={() => {
794
+ // The columns being moved are scoped by the current search
795
+ const filteredNodes =
796
+ getDataForAllFilteredNodes(params.api);
797
+
798
+ const dimension = panel.dimensions.at(-1);
799
+ if (dimension) {
800
+ dimension.setColumns([
801
+ ...dimension.columns,
802
+ ...filteredNodes,
803
+ ]);
804
+ panel.refreshDimensionsTreeData();
805
+ } else {
806
+ confirmPopulateStubDimensionWhenAddingColumns(
807
+ view,
808
+ () => {
809
+ const newDimension =
810
+ panel.newDimension();
811
+ newDimension.setColumns([
812
+ ...filteredNodes,
813
+ ]);
814
+ panel.refreshDimensionsTreeData();
815
+ },
816
+ );
817
+ }
818
+
819
+ params.api.clearFocusedCell();
820
+ }}
821
+ >
822
+ <div>{`[All Columns]`}</div>
823
+ <AvailableColumnsSearchResultCountBadge
824
+ panel={panel}
825
+ gridApi={params.api}
826
+ />
827
+ </button>
828
+ ),
829
+ cellRenderer: (
830
+ params: CustomCellRendererProps<DataCubeEditorColumnsSelectorColumnState>,
831
+ ) => {
832
+ const data = params.data;
833
+ if (!data) {
834
+ return null;
835
+ }
836
+
837
+ return (
838
+ <div
839
+ className="flex h-full w-full cursor-pointer items-center"
840
+ title={`[${data.name}]\nDouble-click to add column`}
841
+ onDoubleClick={() => {
842
+ const dimension = panel.dimensions.at(-1);
843
+ if (dimension) {
844
+ dimension.setColumns([
845
+ ...dimension.columns,
846
+ data,
847
+ ]);
848
+ panel.refreshDimensionsTreeData();
849
+ } else {
850
+ confirmPopulateStubDimensionWhenAddingColumns(
851
+ view,
852
+ () => {
853
+ const newDimension =
854
+ panel.newDimension();
855
+ newDimension.setColumns([data]);
856
+ panel.refreshDimensionsTreeData();
857
+ },
858
+ );
859
+ }
860
+
861
+ params.api.clearFocusedCell();
862
+ }}
863
+ >
864
+ <div className="h-full flex-1 items-center overflow-hidden overflow-ellipsis whitespace-nowrap pl-2">
865
+ {data.name}
866
+ </div>
867
+ </div>
868
+ );
869
+ },
870
+ },
871
+ ]}
872
+ />
873
+ </div>
874
+ </div>
875
+ </div>
876
+ <div className="flex h-full w-10 items-center justify-center">
877
+ <div className="flex flex-col">
878
+ <button
879
+ 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"
880
+ title="Add selected column(s)"
881
+ /**
882
+ * Support add selected (filtered by search) columns
883
+ * We reset the selection after this operation
884
+ */
885
+ onClick={() => {
886
+ if (
887
+ !availableColumnsGridApi ||
888
+ selectedAvailableColumns.length === 0
889
+ ) {
890
+ return;
891
+ }
892
+
893
+ // The columns being moved are scoped by the current search
894
+ const filteredNodes = getDataForAllFilteredNodes(
895
+ availableColumnsGridApi,
896
+ );
897
+ const columnsToMove = selectedAvailableColumns.filter(
898
+ (column) => _findCol(filteredNodes, column.name),
899
+ );
900
+
901
+ const dimension = panel.dimensions.at(-1);
902
+ if (dimension) {
903
+ dimension.setColumns([
904
+ ...dimension.columns,
905
+ ...columnsToMove,
906
+ ]);
907
+ panel.refreshDimensionsTreeData();
908
+ } else {
909
+ confirmPopulateStubDimensionWhenAddingColumns(
910
+ view,
911
+ () => {
912
+ const newDimension = panel.newDimension();
913
+ newDimension.setColumns([...columnsToMove]);
914
+ panel.refreshDimensionsTreeData();
915
+ },
916
+ );
917
+ }
918
+
919
+ availableColumnsGridApi.clearFocusedCell();
920
+ }}
921
+ disabled={
922
+ !availableColumnsGridApi ||
923
+ selectedAvailableColumns.length === 0
373
924
  }
925
+ >
926
+ <DataCubeIcon.ChevronRight className="text-2xl" />
927
+ </button>
928
+ <button
929
+ 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"
930
+ title="Remove selected column(s)"
931
+ /**
932
+ * Support remove selected (filtered by search) columns
933
+ * We reset the selection after this operation
934
+ */
935
+ onClick={() => {
936
+ if (
937
+ !dimensionsTreeGridApi ||
938
+ selectedSelectedColumns.length === 0
939
+ ) {
940
+ return;
941
+ }
374
942
 
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,
943
+ // The columns being moved are scoped by the current search
944
+ const filteredNodes = getDataForAllFilteredNodes(
945
+ dimensionsTreeGridApi,
946
+ );
947
+ const columnsToMove = selectedSelectedColumns.filter(
948
+ (column) => _findCol(filteredNodes, column.name),
949
+ );
950
+
951
+ panel.deselectColumns(columnsToMove);
952
+ panel.refreshDimensionsTreeData();
953
+ dimensionsTreeGridApi.clearFocusedCell();
954
+ }}
955
+ disabled={
956
+ !dimensionsTreeGridApi ||
957
+ selectedSelectedColumns.length === 0
958
+ }
959
+ >
960
+ <DataCubeIcon.ChevronLeft className="text-2xl" />
961
+ </button>
962
+ </div>
963
+ </div>
964
+ <div className="data-cube-tree-selector h-full w-[calc(50%_-_20px)]">
965
+ <div className="flex h-5 items-center text-sm">
966
+ Dimensions:
967
+ </div>
968
+ <div className="h-[calc(100%_-_20px)] rounded-sm border border-neutral-200">
969
+ <div className="flex h-6">
970
+ <div className="relative h-6 w-[calc(100%_-_124px)] border-b border-r border-neutral-200">
971
+ <input
972
+ className="h-full w-full pl-10 pr-6 placeholder-neutral-400"
973
+ ref={searchDimensionsTreeInputRef}
974
+ placeholder="Search..."
975
+ value={panel.dimensionsTreeSearchText}
976
+ onChange={(event) =>
977
+ panel.setDimensionsTreeSearchText(
978
+ event.target.value,
979
+ )
980
+ }
981
+ onKeyDown={(event) => {
982
+ if (event.code === 'Escape') {
983
+ event.stopPropagation();
984
+ searchDimensionsTreeInputRef.current?.select();
985
+ panel.setDimensionsTreeSearchText('');
986
+ }
987
+ }}
988
+ />
989
+ <div className="absolute left-0 top-0 flex h-6 w-10 items-center justify-center">
990
+ <DataCubeIcon.Search className="stroke-[3px] text-lg text-neutral-500" />
991
+ </div>
992
+ <button
993
+ className="absolute right-0 top-0 flex h-6 w-6 items-center justify-center text-neutral-500 disabled:text-neutral-300"
994
+ disabled={!panel.dimensionsTreeSearchText}
995
+ title="Clear search [Esc]"
996
+ onClick={() => {
997
+ panel.setDimensionsTreeSearchText('');
998
+ searchDimensionsTreeInputRef.current?.focus();
999
+ }}
1000
+ >
1001
+ <DataCubeIcon.X className="text-lg" />
1002
+ </button>
1003
+ </div>
1004
+ <div className="flex h-6 items-center border-b border-neutral-200">
1005
+ <button
1006
+ className="flex h-6 w-6 items-center justify-center text-neutral-500 disabled:text-neutral-300"
1007
+ title="Add new dimension"
1008
+ onClick={() => {
1009
+ panel.newDimension();
1010
+ panel.refreshDimensionsTreeData();
1011
+ }}
1012
+ >
1013
+ <DataCubeIcon.Plus className="text-lg" />
1014
+ </button>
1015
+ <div className="h-3 w-[1px] bg-neutral-200" />
1016
+ <button
1017
+ className="flex h-6 w-6 items-center justify-center text-neutral-500 disabled:text-neutral-300"
1018
+ title="Move dimension(s) up"
1019
+ disabled={selectedDimensions.length === 0}
1020
+ onClick={() => {
1021
+ if (selectedDimensions.length === 0) {
1022
+ return;
1023
+ }
1024
+
1025
+ const minIdx = Math.min(
1026
+ ...selectedDimensions.map((dimension) =>
1027
+ panel.dimensions.indexOf(dimension),
1028
+ ),
1029
+ );
1030
+ const precedings = [
1031
+ ...panel.dimensions.slice(
1032
+ 0,
1033
+ Math.max(minIdx - 1, 0),
1034
+ ),
1035
+ // ensure moving dimensions maintain their relative ordering
1036
+ ...panel.dimensions
1037
+ .map((dimension) =>
1038
+ selectedDimensions.find(
1039
+ (dim) => dim.name === dimension.name,
1040
+ ),
1041
+ )
1042
+ .filter(isNonNullable),
1043
+ ];
1044
+
1045
+ panel.setDimensions([
1046
+ ...precedings,
1047
+ ...panel.dimensions.filter(
1048
+ (dimension) =>
1049
+ !precedings.find(
1050
+ (dim) => dim.name === dimension.name,
1051
+ ),
1052
+ ),
383
1053
  ]);
384
- params.api.clearFocusedCell();
1054
+
1055
+ panel.refreshDimensionsTreeData();
385
1056
  }}
386
1057
  >
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
- }
1058
+ <DataCubeIcon.ChevronUp className="text-lg" />
1059
+ </button>
1060
+ <div className="h-3 w-[1px] bg-neutral-200" />
1061
+ <button
1062
+ className="flex h-6 w-6 items-center justify-center text-neutral-500 disabled:text-neutral-300"
1063
+ title="Move dimension(s) down"
1064
+ disabled={selectedDimensions.length === 0}
1065
+ onClick={() => {
1066
+ if (selectedDimensions.length === 0) {
1067
+ return;
1068
+ }
566
1069
 
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,
1070
+ const maxIdx = Math.max(
1071
+ ...selectedDimensions.map((dimension) =>
1072
+ panel.dimensions.indexOf(dimension),
575
1073
  ),
576
1074
  );
577
- params.api.clearFocusedCell();
1075
+ const followings = [
1076
+ // ensure moving dimensions maintain their relative ordering
1077
+ ...panel.dimensions
1078
+ .map((dimension) =>
1079
+ selectedDimensions.find(
1080
+ (dim) => dim.name === dimension.name,
1081
+ ),
1082
+ )
1083
+ .filter(isNonNullable),
1084
+ ...panel.dimensions.slice(
1085
+ Math.min(maxIdx + 1, panel.dimensions.length) +
1086
+ 1,
1087
+ ),
1088
+ ];
1089
+
1090
+ panel.setDimensions([
1091
+ ...panel.dimensions.filter(
1092
+ (dimension) =>
1093
+ !followings.find(
1094
+ (dim) => dim.name === dimension.name,
1095
+ ),
1096
+ ),
1097
+ ...followings,
1098
+ ]);
1099
+
1100
+ panel.refreshDimensionsTreeData();
578
1101
  }}
579
1102
  >
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}
1103
+ <DataCubeIcon.ChevronDown className="text-lg" />
1104
+ </button>
1105
+ <div className="h-3 w-[1px] bg-neutral-200" />
1106
+ <button
1107
+ className="flex h-6 w-6 items-center justify-center text-neutral-500 disabled:text-neutral-300"
1108
+ title="Remove dimension(s)"
1109
+ disabled={selectedDimensions.length === 0}
1110
+ onClick={() => {
1111
+ if (selectedDimensions.length === 0) {
1112
+ return;
1113
+ }
1114
+
1115
+ selectedDimensions.at(0)?.setIsRenaming(true);
1116
+ }}
1117
+ >
1118
+ <DataCubeIcon.Pencil className="text-lg" />
1119
+ </button>
1120
+ <div className="h-3 w-[1px] bg-neutral-200" />
1121
+ <button
1122
+ className="flex h-6 w-6 items-center justify-center text-neutral-500 disabled:text-neutral-300"
1123
+ title="Rename dimension"
1124
+ disabled={selectedDimensions.length === 0}
1125
+ onClick={() => {
1126
+ if (selectedDimensions.length === 0) {
1127
+ return;
1128
+ }
1129
+
1130
+ panel.setDimensions(
1131
+ panel.dimensions.filter(
1132
+ (dimension) =>
1133
+ !selectedDimensions.find(
1134
+ (dim) => dim.name === dimension.name,
1135
+ ),
1136
+ ),
1137
+ );
1138
+ panel.refreshDimensionsTreeData();
1139
+ }}
1140
+ >
1141
+ <DataCubeIcon.Delete className="text-lg" />
1142
+ </button>
1143
+ </div>
1144
+ </div>
1145
+ <div className="h-[calc(100%_-_24px)]">
1146
+ <AgGridReact
1147
+ {...getColumnsSelectorBaseGridProps<DataCubeEditorDimensionsTreeNode>()}
1148
+ modules={[AllCommunityModule, TreeDataModule]}
1149
+ className="ag-theme-quartz"
1150
+ treeData={true}
1151
+ treeDataChildrenField="children"
1152
+ groupDefaultExpanded={-1} // expand all groups by default
1153
+ // Disable managed row-dragging to disallow changing the order of columns
1154
+ // and to make sure the row data and the available columns state are in sync
1155
+ onGridReady={(params) => {
1156
+ setDimensionsTreeGridApi(params.api);
1157
+
1158
+ // eslint-disable-next-line no-process-env
1159
+ if (process.env.NODE_ENV !== 'production') {
1160
+ // restore original error logging
1161
+ console.error = __INTERNAL__original_console_error; // eslint-disable-line no-console
1162
+ }
1163
+ }}
1164
+ onSelectionChanged={(
1165
+ event: SelectionChangedEvent<DataCubeEditorDimensionsTreeNode>,
1166
+ ) => {
1167
+ const selectedNodes = event.api
1168
+ .getSelectedNodes()
1169
+ .map((node) => node.data)
1170
+ .filter(isNonNullable);
1171
+ const _selectedColumns: DataCubeEditorColumnsSelectorColumnState[] =
1172
+ [];
1173
+ const _selectedDimensions: DataCubeEditorDimensionState[] =
1174
+ [];
1175
+ selectedNodes.forEach((node) => {
1176
+ if (
1177
+ node.data instanceof DataCubeEditorDimensionState
1178
+ ) {
1179
+ _selectedDimensions.push(node.data);
1180
+ } else if (
1181
+ node.data instanceof
1182
+ DataCubeEditorColumnsSelectorColumnState
1183
+ ) {
1184
+ _selectedColumns.push(node.data);
1185
+ }
1186
+ });
1187
+ setSelectedDimensions(_selectedDimensions);
1188
+ setSelectedSelectedColumns(_selectedColumns);
1189
+ }}
1190
+ rowSelection={{
1191
+ mode: 'multiRow',
1192
+ groupSelects: 'self',
1193
+ }}
1194
+ columnDefs={[]}
1195
+ // Using ag-grid quick filter is a cheap way to implement search
1196
+ quickFilterText={panel.dimensionsTreeSearchText}
1197
+ rowData={panel.dimensionsTreeData.nodes}
1198
+ rowClassRules={{
1199
+ 'border-t border-b border-1 border-transparent': () =>
1200
+ true,
1201
+ 'border-b-sky-600': (params) => {
1202
+ return (
1203
+ dimensionsTreeOnHoverIndex !== undefined &&
1204
+ params.rowIndex === dimensionsTreeOnHoverIndex
1205
+ );
1206
+ },
1207
+ 'border-t-sky-600': (params) => {
1208
+ return (
1209
+ dimensionsTreeOnHoverIndex === -1 &&
1210
+ params.rowIndex === 0
1211
+ );
1212
+ },
1213
+ }}
1214
+ autoGroupColumnDef={{
1215
+ headerName: 'Name',
1216
+ field: 'name',
1217
+ colId: 'name',
1218
+ flex: 1,
1219
+ minWidth: 100,
1220
+ filter: true,
1221
+ sortable: false,
1222
+ resizable: false,
1223
+ suppressHeaderMenuButton: true,
1224
+ suppressKeyboardEvent: (event) => {
1225
+ if (
1226
+ panel.dimensions.some(
1227
+ (dimension) => dimension.isRenaming,
1228
+ )
1229
+ ) {
1230
+ // only allow tabbing through the grid when renaming
1231
+ if (event.event.code === 'Tab') {
1232
+ return false;
1233
+ }
1234
+ return true;
1235
+ }
1236
+ return false;
1237
+ },
1238
+ getQuickFilterText: (params) => params.value,
1239
+ /**
1240
+ * Support double-click to remove all (filtered by search) columns
1241
+ */
1242
+ headerComponent: (
1243
+ params: CustomCellRendererProps,
1244
+ ) => (
1245
+ <button
1246
+ title="Double-click to remove all columns"
1247
+ className="flex h-full w-full items-center justify-between pl-0.5"
1248
+ onDoubleClick={() => {
1249
+ // The columns being moved are scoped by the current search
1250
+ const filteredNodes =
1251
+ getDataForAllFilteredNodes(params.api);
1252
+
1253
+ panel.deselectColumns(
1254
+ panel.selectedColumns.filter((column) =>
1255
+ _findCol(filteredNodes, column.name),
1256
+ ),
1257
+ );
1258
+ panel.refreshDimensionsTreeData();
1259
+ params.api.clearFocusedCell();
1260
+ }}
1261
+ >
1262
+ <div>{`[All Dimensions]`}</div>
1263
+ <DimensionsTreeSearchResultCountBadge
1264
+ panel={panel}
1265
+ gridApi={params.api}
1266
+ />
1267
+ </button>
1268
+ ),
1269
+ onCellDoubleClicked: (params) => {
1270
+ if (
1271
+ params.data?.data instanceof
1272
+ DataCubeEditorDimensionState
1273
+ ) {
1274
+ params.data.data.setColumns([]);
1275
+ panel.refreshDimensionsTreeData();
1276
+ params.api.clearFocusedCell();
1277
+ }
1278
+ },
1279
+ cellRendererParams: {
1280
+ innerRenderer: (
1281
+ params: CustomCellRendererProps<DataCubeEditorDimensionsTreeNode>,
1282
+ ) => {
1283
+ const data = params.data;
1284
+
1285
+ if (!data) {
1286
+ return null;
1287
+ }
1288
+ if (
1289
+ data.data instanceof
1290
+ DataCubeEditorDimensionState
1291
+ ) {
1292
+ return (
1293
+ <DimensionLabel
1294
+ panel={panel}
1295
+ dimension={data.data}
1296
+ gridApi={params.api}
1297
+ />
1298
+ );
1299
+ }
1300
+ return (
1301
+ <div
1302
+ className="flex h-full w-full cursor-pointer items-center"
1303
+ title={`[${data.name}]\nDouble-click to remove column`}
1304
+ onDoubleClick={() => {
1305
+ if (
1306
+ data.data instanceof
1307
+ DataCubeEditorColumnsSelectorColumnState
1308
+ ) {
1309
+ panel.deselectColumns([data.data]);
1310
+ panel.refreshDimensionsTreeData();
1311
+ }
1312
+ params.api.clearFocusedCell();
1313
+ }}
1314
+ >
1315
+ <div className="h-full flex-1 items-center overflow-hidden overflow-ellipsis whitespace-nowrap pl-1">
1316
+ {data.name}
1317
+ </div>
1318
+ </div>
1319
+ );
1320
+ },
1321
+ },
1322
+ }}
1323
+ noRowsOverlayComponent={(
1324
+ params: CustomNoRowsOverlayProps,
1325
+ ) => {
1326
+ if (params.api.getQuickFilter()) {
1327
+ return (
1328
+ <div className="flex items-center border-[1.5px] border-neutral-300 p-2 font-semibold text-neutral-400">
1329
+ <div>
1330
+ <DataCubeIcon.WarningCircle className="mr-1 text-lg" />
1331
+ </div>
1332
+ No match found
1333
+ </div>
1334
+ );
1335
+ }
1336
+ return (
1337
+ <div className="flex items-center border-[1.5px] border-neutral-200 p-2 font-semibold text-neutral-400">
1338
+ <div>
1339
+ <DataCubeIcon.Warning className="mr-1 text-lg" />
1340
+ </div>
1341
+ No dimensions specified
586
1342
  </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
- />
1343
+ );
1344
+ }}
1345
+ suppressRowDrag={!isDimensionsTreeRowDragEnabled}
1346
+ rowDragEntireRow={isDimensionsTreeRowDragEnabled}
1347
+ onRowDragEnd={onDimensionsTreeRowDragEnd}
1348
+ onRowDragMove={onDimensionsTreeRowDragMove}
1349
+ onRowDragCancel={onDimensionsTreeRowDragCancel}
1350
+ onRowDragLeave={onDimensionsTreeRowDragLeave}
1351
+ />
1352
+ </div>
1353
+ </div>
1354
+ </div>
1355
+ </div>
600
1356
  </div>
601
- </div>
1357
+ )}
602
1358
  </div>
603
1359
  </div>
604
1360
  );