@finos/legend-cube-builder 0.0.5 → 0.0.6

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 (245) hide show
  1. package/docs/adding-an-operation.md +4 -2
  2. package/docs/demo/README.md +1 -1
  3. package/docs/hosting.md +40 -9
  4. package/lib/__lib__/LegendCubeHelpText.d.ts.map +1 -1
  5. package/lib/__lib__/LegendCubeHelpText.js +3 -1
  6. package/lib/__lib__/LegendCubeHelpText.js.map +1 -1
  7. package/lib/__lib__/LegendCubeLabels.d.ts +34 -5
  8. package/lib/__lib__/LegendCubeLabels.d.ts.map +1 -1
  9. package/lib/__lib__/LegendCubeLabels.js +45 -8
  10. package/lib/__lib__/LegendCubeLabels.js.map +1 -1
  11. package/lib/__lib__/LegendCubeNavigation.d.ts +23 -0
  12. package/lib/__lib__/LegendCubeNavigation.d.ts.map +1 -0
  13. package/lib/__lib__/LegendCubeNavigation.js +33 -0
  14. package/lib/__lib__/LegendCubeNavigation.js.map +1 -0
  15. package/lib/__lib__/LegendCubeTesting.d.ts +3 -1
  16. package/lib/__lib__/LegendCubeTesting.d.ts.map +1 -1
  17. package/lib/__lib__/LegendCubeTesting.js +2 -0
  18. package/lib/__lib__/LegendCubeTesting.js.map +1 -1
  19. package/lib/__lib__/LegendCubeTour.d.ts +62 -0
  20. package/lib/__lib__/LegendCubeTour.d.ts.map +1 -0
  21. package/lib/__lib__/LegendCubeTour.js +149 -0
  22. package/lib/__lib__/LegendCubeTour.js.map +1 -0
  23. package/lib/__test-utils__/CubeExampleTestUtils.d.ts +34 -0
  24. package/lib/__test-utils__/CubeExampleTestUtils.d.ts.map +1 -0
  25. package/lib/__test-utils__/CubeExampleTestUtils.js +72 -0
  26. package/lib/__test-utils__/CubeExampleTestUtils.js.map +1 -0
  27. package/lib/__test-utils__/CubeVariantTestUtils.d.ts +16 -0
  28. package/lib/__test-utils__/CubeVariantTestUtils.d.ts.map +1 -0
  29. package/lib/__test-utils__/CubeVariantTestUtils.js +66 -0
  30. package/lib/__test-utils__/CubeVariantTestUtils.js.map +1 -0
  31. package/lib/components/CubeAddItems.d.ts.map +1 -1
  32. package/lib/components/CubeAddItems.js +2 -1
  33. package/lib/components/CubeAddItems.js.map +1 -1
  34. package/lib/components/CubeEditor.d.ts.map +1 -1
  35. package/lib/components/CubeEditor.js +11 -5
  36. package/lib/components/CubeEditor.js.map +1 -1
  37. package/lib/components/CubeFileMenu.d.ts.map +1 -1
  38. package/lib/components/CubeFileMenu.js +2 -1
  39. package/lib/components/CubeFileMenu.js.map +1 -1
  40. package/lib/components/CubeNodeIcon.d.ts.map +1 -1
  41. package/lib/components/CubeNodeIcon.js +2 -1
  42. package/lib/components/CubeNodeIcon.js.map +1 -1
  43. package/lib/components/canvas/CubeCanvas.d.ts.map +1 -1
  44. package/lib/components/canvas/CubeCanvas.js +3 -2
  45. package/lib/components/canvas/CubeCanvas.js.map +1 -1
  46. package/lib/components/canvas/CubeCanvasNode.d.ts.map +1 -1
  47. package/lib/components/canvas/CubeCanvasNode.js +4 -1
  48. package/lib/components/canvas/CubeCanvasNode.js.map +1 -1
  49. package/lib/components/dev-settings/CubeDevSettings.d.ts +2 -2
  50. package/lib/components/dev-settings/CubeDevSettings.d.ts.map +1 -1
  51. package/lib/components/dev-settings/CubeDevSettings.js +12 -8
  52. package/lib/components/dev-settings/CubeDevSettings.js.map +1 -1
  53. package/lib/components/editors/CubeColumnPicker.d.ts.map +1 -1
  54. package/lib/components/editors/CubeColumnPicker.js +3 -1
  55. package/lib/components/editors/CubeColumnPicker.js.map +1 -1
  56. package/lib/components/editors/CubeExplodeEditor.d.ts +26 -0
  57. package/lib/components/editors/CubeExplodeEditor.d.ts.map +1 -0
  58. package/lib/components/editors/CubeExplodeEditor.js +61 -0
  59. package/lib/components/editors/CubeExplodeEditor.js.map +1 -0
  60. package/lib/components/editors/CubeExpressionFilterEditor.d.ts.map +1 -1
  61. package/lib/components/editors/CubeExpressionFilterEditor.js +8 -5
  62. package/lib/components/editors/CubeExpressionFilterEditor.js.map +1 -1
  63. package/lib/components/editors/CubeExtendEditor.d.ts +0 -2
  64. package/lib/components/editors/CubeExtendEditor.d.ts.map +1 -1
  65. package/lib/components/editors/CubeExtendEditor.js +12 -11
  66. package/lib/components/editors/CubeExtendEditor.js.map +1 -1
  67. package/lib/components/editors/CubeJsonFieldsTree.d.ts +46 -0
  68. package/lib/components/editors/CubeJsonFieldsTree.d.ts.map +1 -0
  69. package/lib/components/editors/CubeJsonFieldsTree.js +93 -0
  70. package/lib/components/editors/CubeJsonFieldsTree.js.map +1 -0
  71. package/lib/components/editors/CubeNodeEditorRegistry.d.ts.map +1 -1
  72. package/lib/components/editors/CubeNodeEditorRegistry.js +3 -1
  73. package/lib/components/editors/CubeNodeEditorRegistry.js.map +1 -1
  74. package/lib/components/grid/CubeGridRegion.d.ts.map +1 -1
  75. package/lib/components/grid/CubeGridRegion.js +5 -4
  76. package/lib/components/grid/CubeGridRegion.js.map +1 -1
  77. package/lib/components/grid/CubeGridValues.d.ts +5 -3
  78. package/lib/components/grid/CubeGridValues.d.ts.map +1 -1
  79. package/lib/components/grid/CubeGridValues.js +8 -3
  80. package/lib/components/grid/CubeGridValues.js.map +1 -1
  81. package/lib/components/grid/CubeJsonExplorerDialog.d.ts +27 -0
  82. package/lib/components/grid/CubeJsonExplorerDialog.d.ts.map +1 -0
  83. package/lib/components/grid/CubeJsonExplorerDialog.js +102 -0
  84. package/lib/components/grid/CubeJsonExplorerDialog.js.map +1 -0
  85. package/lib/components/grid/CubeJsonViewerDialog.d.ts +27 -0
  86. package/lib/components/grid/CubeJsonViewerDialog.d.ts.map +1 -0
  87. package/lib/components/grid/CubeJsonViewerDialog.js +68 -0
  88. package/lib/components/grid/CubeJsonViewerDialog.js.map +1 -0
  89. package/lib/components/grid/CubeResultGrid.d.ts +15 -3
  90. package/lib/components/grid/CubeResultGrid.d.ts.map +1 -1
  91. package/lib/components/grid/CubeResultGrid.js +95 -3
  92. package/lib/components/grid/CubeResultGrid.js.map +1 -1
  93. package/lib/components/grid/CubeServerSideGrid.d.ts.map +1 -1
  94. package/lib/components/grid/CubeServerSideGrid.js +4 -2
  95. package/lib/components/grid/CubeServerSideGrid.js.map +1 -1
  96. package/lib/components/palette/CubePalette.d.ts.map +1 -1
  97. package/lib/components/palette/CubePalette.js +4 -1
  98. package/lib/components/palette/CubePalette.js.map +1 -1
  99. package/lib/components/source-picker/CubeIngestTab.d.ts +2 -2
  100. package/lib/components/source-picker/CubeIngestTab.d.ts.map +1 -1
  101. package/lib/components/source-picker/CubeIngestTab.js +19 -13
  102. package/lib/components/source-picker/CubeIngestTab.js.map +1 -1
  103. package/lib/components/tour/CubeTour.d.ts +36 -0
  104. package/lib/components/tour/CubeTour.d.ts.map +1 -0
  105. package/lib/components/tour/CubeTour.js +183 -0
  106. package/lib/components/tour/CubeTour.js.map +1 -0
  107. package/lib/index.css +1 -1
  108. package/lib/index.d.ts +1 -0
  109. package/lib/index.d.ts.map +1 -1
  110. package/lib/index.js +1 -0
  111. package/lib/index.js.map +1 -1
  112. package/lib/stores/CubeEditorState.d.ts +17 -1
  113. package/lib/stores/CubeEditorState.d.ts.map +1 -1
  114. package/lib/stores/CubeEditorState.js +35 -5
  115. package/lib/stores/CubeEditorState.js.map +1 -1
  116. package/lib/stores/CubeExamples.d.ts.map +1 -1
  117. package/lib/stores/CubeExamples.js +101 -4
  118. package/lib/stores/CubeExamples.js.map +1 -1
  119. package/lib/stores/CubeExamplesState.d.ts.map +1 -1
  120. package/lib/stores/CubeExamplesState.js +4 -1
  121. package/lib/stores/CubeExamplesState.js.map +1 -1
  122. package/lib/stores/CubeGridQuickActions.d.ts +7 -0
  123. package/lib/stores/CubeGridQuickActions.d.ts.map +1 -1
  124. package/lib/stores/CubeGridQuickActions.js +20 -11
  125. package/lib/stores/CubeGridQuickActions.js.map +1 -1
  126. package/lib/stores/CubePureText.d.ts +20 -0
  127. package/lib/stores/CubePureText.d.ts.map +1 -0
  128. package/lib/stores/CubePureText.js +24 -0
  129. package/lib/stores/CubePureText.js.map +1 -0
  130. package/lib/stores/CubeTourState.d.ts +49 -0
  131. package/lib/stores/CubeTourState.d.ts.map +1 -0
  132. package/lib/stores/CubeTourState.js +92 -0
  133. package/lib/stores/CubeTourState.js.map +1 -0
  134. package/lib/stores/LegendCubeHost.d.ts +88 -0
  135. package/lib/stores/LegendCubeHost.d.ts.map +1 -0
  136. package/lib/stores/LegendCubeHost.js +149 -0
  137. package/lib/stores/LegendCubeHost.js.map +1 -0
  138. package/lib/stores/editors/CubeExplodeDraft.d.ts +87 -0
  139. package/lib/stores/editors/CubeExplodeDraft.d.ts.map +1 -0
  140. package/lib/stores/editors/CubeExplodeDraft.js +221 -0
  141. package/lib/stores/editors/CubeExplodeDraft.js.map +1 -0
  142. package/lib/stores/editors/CubeExpressionFilterDraft.d.ts +3 -0
  143. package/lib/stores/editors/CubeExpressionFilterDraft.d.ts.map +1 -1
  144. package/lib/stores/editors/CubeExpressionFilterDraft.js +4 -0
  145. package/lib/stores/editors/CubeExpressionFilterDraft.js.map +1 -1
  146. package/lib/stores/editors/CubeExtendDraft.d.ts +9 -0
  147. package/lib/stores/editors/CubeExtendDraft.d.ts.map +1 -1
  148. package/lib/stores/editors/CubeExtendDraft.js +26 -0
  149. package/lib/stores/editors/CubeExtendDraft.js.map +1 -1
  150. package/lib/stores/editors/CubeNodeDraftRegistry.d.ts.map +1 -1
  151. package/lib/stores/editors/CubeNodeDraftRegistry.js +6 -1
  152. package/lib/stores/editors/CubeNodeDraftRegistry.js.map +1 -1
  153. package/lib/stores/fixtures/CubeSalesDuckDbModel.d.ts +1 -0
  154. package/lib/stores/fixtures/CubeSalesDuckDbModel.d.ts.map +1 -1
  155. package/lib/stores/fixtures/CubeSalesDuckDbModel.js +8 -2
  156. package/lib/stores/fixtures/CubeSalesDuckDbModel.js.map +1 -1
  157. package/lib/stores/source-picker/CubeIngestTabState.d.ts +48 -14
  158. package/lib/stores/source-picker/CubeIngestTabState.d.ts.map +1 -1
  159. package/lib/stores/source-picker/CubeIngestTabState.js +159 -62
  160. package/lib/stores/source-picker/CubeIngestTabState.js.map +1 -1
  161. package/lib/stores/variant/CubeJson.d.ts +48 -0
  162. package/lib/stores/variant/CubeJson.d.ts.map +1 -0
  163. package/lib/stores/variant/CubeJson.js +207 -0
  164. package/lib/stores/variant/CubeJson.js.map +1 -0
  165. package/lib/stores/variant/CubeJsonExplorerState.d.ts +88 -0
  166. package/lib/stores/variant/CubeJsonExplorerState.d.ts.map +1 -0
  167. package/lib/stores/variant/CubeJsonExplorerState.js +297 -0
  168. package/lib/stores/variant/CubeJsonExplorerState.js.map +1 -0
  169. package/lib/stores/variant/CubeJsonFieldsState.d.ts +83 -0
  170. package/lib/stores/variant/CubeJsonFieldsState.d.ts.map +1 -0
  171. package/lib/stores/variant/CubeJsonFieldsState.js +207 -0
  172. package/lib/stores/variant/CubeJsonFieldsState.js.map +1 -0
  173. package/lib/stores/variant/CubeJsonOffers.d.ts +76 -0
  174. package/lib/stores/variant/CubeJsonOffers.d.ts.map +1 -0
  175. package/lib/stores/variant/CubeJsonOffers.js +250 -0
  176. package/lib/stores/variant/CubeJsonOffers.js.map +1 -0
  177. package/lib/stores/variant/CubeJsonParse.d.ts +28 -0
  178. package/lib/stores/variant/CubeJsonParse.d.ts.map +1 -0
  179. package/lib/stores/variant/CubeJsonParse.js +53 -0
  180. package/lib/stores/variant/CubeJsonParse.js.map +1 -0
  181. package/lib/stores/variant/CubeJsonSampling.d.ts +41 -0
  182. package/lib/stores/variant/CubeJsonSampling.d.ts.map +1 -0
  183. package/lib/stores/variant/CubeJsonSampling.js +59 -0
  184. package/lib/stores/variant/CubeJsonSampling.js.map +1 -0
  185. package/lib/stores/variant/CubeJsonShape.d.ts +87 -0
  186. package/lib/stores/variant/CubeJsonShape.d.ts.map +1 -0
  187. package/lib/stores/variant/CubeJsonShape.js +209 -0
  188. package/lib/stores/variant/CubeJsonShape.js.map +1 -0
  189. package/lib/stores/variant/CubeJsonViewerState.d.ts +33 -0
  190. package/lib/stores/variant/CubeJsonViewerState.d.ts.map +1 -0
  191. package/lib/stores/variant/CubeJsonViewerState.js +53 -0
  192. package/lib/stores/variant/CubeJsonViewerState.js.map +1 -0
  193. package/package.json +2 -2
  194. package/src/__lib__/LegendCubeHelpText.ts +4 -0
  195. package/src/__lib__/LegendCubeLabels.ts +60 -8
  196. package/src/__lib__/LegendCubeNavigation.ts +48 -0
  197. package/src/__lib__/LegendCubeTesting.ts +2 -0
  198. package/src/__lib__/LegendCubeTour.ts +173 -0
  199. package/src/__test-utils__/CubeExampleTestUtils.ts +119 -0
  200. package/src/__test-utils__/CubeVariantTestUtils.ts +103 -0
  201. package/src/components/CubeAddItems.tsx +2 -0
  202. package/src/components/CubeEditor.tsx +15 -0
  203. package/src/components/CubeFileMenu.tsx +2 -0
  204. package/src/components/CubeNodeIcon.tsx +2 -0
  205. package/src/components/canvas/CubeCanvas.tsx +17 -1
  206. package/src/components/canvas/CubeCanvasNode.tsx +7 -0
  207. package/src/components/dev-settings/CubeDevSettings.tsx +17 -2
  208. package/src/components/editors/CubeColumnPicker.tsx +3 -0
  209. package/src/components/editors/CubeExplodeEditor.tsx +207 -0
  210. package/src/components/editors/CubeExpressionFilterEditor.tsx +25 -8
  211. package/src/components/editors/CubeExtendEditor.tsx +32 -19
  212. package/src/components/editors/CubeJsonFieldsTree.tsx +316 -0
  213. package/src/components/editors/CubeNodeEditorRegistry.ts +3 -0
  214. package/src/components/grid/CubeGridRegion.tsx +21 -3
  215. package/src/components/grid/CubeGridValues.ts +8 -3
  216. package/src/components/grid/CubeJsonExplorerDialog.tsx +304 -0
  217. package/src/components/grid/CubeJsonViewerDialog.tsx +190 -0
  218. package/src/components/grid/CubeResultGrid.tsx +144 -2
  219. package/src/components/grid/CubeServerSideGrid.tsx +13 -0
  220. package/src/components/palette/CubePalette.tsx +9 -0
  221. package/src/components/source-picker/CubeIngestTab.tsx +62 -30
  222. package/src/components/tour/CubeTour.tsx +317 -0
  223. package/src/index.ts +7 -0
  224. package/src/stores/CubeEditorState.ts +36 -5
  225. package/src/stores/CubeExamples.ts +120 -2
  226. package/src/stores/CubeExamplesState.ts +9 -1
  227. package/src/stores/CubeGridQuickActions.ts +23 -12
  228. package/src/stores/CubePureText.ts +28 -0
  229. package/src/stores/CubeTourState.ts +116 -0
  230. package/src/stores/LegendCubeHost.ts +243 -0
  231. package/src/stores/editors/CubeExplodeDraft.ts +272 -0
  232. package/src/stores/editors/CubeExpressionFilterDraft.ts +4 -0
  233. package/src/stores/editors/CubeExtendDraft.ts +29 -0
  234. package/src/stores/editors/CubeNodeDraftRegistry.ts +7 -0
  235. package/src/stores/fixtures/CubeSalesDuckDbModel.ts +8 -2
  236. package/src/stores/source-picker/CubeIngestTabState.ts +253 -102
  237. package/src/stores/variant/CubeJson.ts +229 -0
  238. package/src/stores/variant/CubeJsonExplorerState.ts +378 -0
  239. package/src/stores/variant/CubeJsonFieldsState.ts +252 -0
  240. package/src/stores/variant/CubeJsonOffers.ts +359 -0
  241. package/src/stores/variant/CubeJsonParse.ts +75 -0
  242. package/src/stores/variant/CubeJsonSampling.ts +90 -0
  243. package/src/stores/variant/CubeJsonShape.ts +279 -0
  244. package/src/stores/variant/CubeJsonViewerState.ts +59 -0
  245. package/tsconfig.json +22 -1
@@ -27,6 +27,10 @@ import {
27
27
  import { memo, useCallback, useEffect, useMemo, useState } from 'react';
28
28
  import { NULL_CELL_TEXT } from '../../__lib__/LegendCubeLabels.js';
29
29
  import { LEGEND_CUBE_TEST_ID } from '../../__lib__/LegendCubeTesting.js';
30
+ import {
31
+ CUBE_TOUR_ANCHOR,
32
+ cubeTourAnchor,
33
+ } from '../../__lib__/LegendCubeTour.js';
30
34
  import type { CubeEditorState } from '../../stores/CubeEditorState.js';
31
35
  import type { CubeExecutionResult } from '../../stores/CubeExecutionState.js';
32
36
  import type {
@@ -37,7 +41,9 @@ import { formatCellValue } from './CubeGridValues.js';
37
41
  import {
38
42
  buildCubeColumnDefs,
39
43
  CUBE_EXCEL_STYLES,
44
+ type CubeGridHeaderMenuParams,
40
45
  getCubeGridContextMenuItems,
46
+ getCubeGridHeaderMenuItems,
41
47
  keepCubeColumnWidths,
42
48
  } from './CubeResultGrid.js';
43
49
 
@@ -102,6 +108,11 @@ export const CubeServerSideGrid = memo(
102
108
  getCubeGridContextMenuItems(editorState, params),
103
109
  [editorState],
104
110
  );
111
+ const getMainMenuItems = useCallback(
112
+ (params: CubeGridHeaderMenuParams<CubeGridRowData>) =>
113
+ getCubeGridHeaderMenuItems(editorState, params),
114
+ [editorState],
115
+ );
105
116
  const columnDefs = useMemo(
106
117
  (): DataGridColumnDefinition<CubeGridRowData>[] =>
107
118
  buildCubeColumnDefs<CubeGridRowData>(
@@ -154,6 +165,7 @@ export const CubeServerSideGrid = memo(
154
165
  'ag-theme-balham-dark': darkMode,
155
166
  })}
156
167
  data-testid={LEGEND_CUBE_TEST_ID.RESULT_GRID}
168
+ {...cubeTourAnchor(CUBE_TOUR_ANCHOR.RESULTS)}
157
169
  >
158
170
  <DataGrid<CubeGridRowData>
159
171
  rowModelType="serverSide"
@@ -163,6 +175,7 @@ export const CubeServerSideGrid = memo(
163
175
  suppressFieldDotNotation={true}
164
176
  tooltipShowDelay={500}
165
177
  getContextMenuItems={getContextMenuItems}
178
+ getMainMenuItems={getMainMenuItems}
166
179
  serverSideSortAllLevels={true}
167
180
  excelStyles={CUBE_EXCEL_STYLES}
168
181
  onColumnResized={(event) =>
@@ -31,6 +31,10 @@ import {
31
31
  READ_ONLY_CUBE_TITLE,
32
32
  } from '../../__lib__/LegendCubeLabels.js';
33
33
  import { LEGEND_CUBE_TEST_ID } from '../../__lib__/LegendCubeTesting.js';
34
+ import {
35
+ CUBE_TOUR_ANCHOR,
36
+ cubeTourAnchor,
37
+ } from '../../__lib__/LegendCubeTour.js';
34
38
  import {
35
39
  addCubeNode,
36
40
  getCubeAddDatabaseProblem,
@@ -109,6 +113,11 @@ const CubePaletteItem = observer(
109
113
  tabIndex={disabled ? -1 : 0}
110
114
  aria-disabled={disabled}
111
115
  aria-label={label}
116
+ {...cubeTourAnchor(
117
+ definition.kind === 'source'
118
+ ? CUBE_TOUR_ANCHOR.SOURCES
119
+ : CUBE_TOUR_ANCHOR.STEPS,
120
+ )}
112
121
  className={clsx(
113
122
  'flex h-8 shrink-0 items-center gap-2 px-3 text-base',
114
123
  disabled
@@ -16,7 +16,7 @@
16
16
 
17
17
  import { clsx } from '@finos/legend-art';
18
18
  import { observer } from 'mobx-react-lite';
19
- import { useId } from 'react';
19
+ import { Fragment, useId } from 'react';
20
20
  import { CUBE_DATA_PRODUCT_ENVIRONMENT_LABELS } from '../../__lib__/LegendCubeDataProductLabels.js';
21
21
  import { getDroppedDefinitionsLabel } from '../../__lib__/LegendCubeIngestLabels.js';
22
22
  import {
@@ -62,11 +62,16 @@ const CubeIngestTabAlert: React.FC<{
62
62
  );
63
63
 
64
64
  /**
65
- * A definition's data sets, as both Ingest screens list them, each picked or
66
- * ticked to add; the one picked last previewed
65
+ * The data sets of the opened definitions, as both Ingest screens list them,
66
+ * each picked or ticked to add, under its definition's name when several are
67
+ * opened; the one picked last previewed
67
68
  */
68
69
  const CubeIngestDataSets = observer((props: { tab: CubeIngestTabState }) => {
69
70
  const { tab } = props;
71
+ const read = tab.definitions.filter(
72
+ (definition) => tab.getDataSets(definition.urn) !== undefined,
73
+ );
74
+ const grouped = tab.definitions.length > 1;
70
75
  return (
71
76
  <>
72
77
  {tab.isDescribing && (
@@ -74,34 +79,56 @@ const CubeIngestDataSets = observer((props: { tab: CubeIngestTabState }) => {
74
79
  reading the definition
75
80
  </div>
76
81
  )}
77
- {tab.dataSets && (
82
+ {read.length > 0 && (
78
83
  <ul
79
- className={`max-h-40 overflow-auto ${CUBE_PICKER_LIST_CLASS}`}
84
+ className={`max-h-48 overflow-auto ${CUBE_PICKER_LIST_CLASS}`}
80
85
  aria-label="Data sets"
81
86
  >
82
- {tab.dataSets.map((dataSet) => (
83
- <CubePickerListItem
84
- key={dataSet.name}
85
- picked={tab.isDataSetPicked(dataSet.name)}
86
- disabled={!dataSet.isPickable}
87
- title={dataSet.disabledReason}
88
- adds={true}
89
- onPick={() => tab.selectDataSet(dataSet.name)}
90
- onToggle={() => tab.toggleDataSet(dataSet.name)}
91
- toggleLabel={dataSet.name}
92
- >
93
- <span className="min-w-0 flex-1 truncate">{dataSet.name}</span>
94
- <span className="shrink-0 text-sm text-[var(--color-text-muted)]">
95
- {dataSet.isPickable
96
- ? `${dataSet.schema?.columns.length ?? 0} columns`
97
- : dataSet.disabledReason}
98
- </span>
99
- </CubePickerListItem>
87
+ {read.map((definition) => (
88
+ <Fragment key={definition.urn}>
89
+ {grouped && (
90
+ <li className="px-2 pt-1 text-sm text-[var(--color-text-muted)]">
91
+ {definition.name}
92
+ </li>
93
+ )}
94
+ {(tab.getDataSets(definition.urn) ?? []).map((dataSet) => (
95
+ <CubePickerListItem
96
+ key={dataSet.name}
97
+ picked={tab.isDataSetPicked(dataSet.name, definition.urn)}
98
+ disabled={!dataSet.isPickable}
99
+ title={dataSet.disabledReason}
100
+ adds={true}
101
+ onPick={() => tab.selectDataSet(dataSet.name, definition.urn)}
102
+ onToggle={() =>
103
+ tab.toggleDataSet(dataSet.name, definition.urn)
104
+ }
105
+ toggleLabel={
106
+ grouped
107
+ ? `${dataSet.name} of ${definition.name}`
108
+ : dataSet.name
109
+ }
110
+ >
111
+ <span className="min-w-0 flex-1 truncate">
112
+ {dataSet.name}
113
+ </span>
114
+ <span className="shrink-0 text-sm text-[var(--color-text-muted)]">
115
+ {dataSet.isPickable
116
+ ? `${dataSet.schema?.columns.length ?? 0} columns`
117
+ : dataSet.disabledReason}
118
+ </span>
119
+ </CubePickerListItem>
120
+ ))}
121
+ </Fragment>
100
122
  ))}
101
123
  </ul>
102
124
  )}
103
125
  {tab.dataSet?.schema && (
104
- <section aria-label="Data set preview">
126
+ <section aria-label="Data set preview" className="flex flex-col gap-1">
127
+ {grouped && (
128
+ <span className="text-sm text-[var(--color-text-muted)]">
129
+ {`${tab.dataSet.name} of ${tab.previewedDefinition?.name ?? ''}`}
130
+ </span>
131
+ )}
105
132
  <CubeSchemaColumnsTable schema={tab.dataSet.schema} />
106
133
  </section>
107
134
  )}
@@ -111,8 +138,9 @@ const CubeIngestDataSets = observer((props: { tab: CubeIngestTabState }) => {
111
138
 
112
139
  /**
113
140
  * The Ingest tab on a host reading one project's definitions (Studio): the
114
- * project's definitions by name, each path muted beside it, then a
115
- * definition's data sets; the runtime its runs use is named, not picked
141
+ * project's definitions by name, each path muted beside it, then the data
142
+ * sets of the one picked, or of several ticked; the runtime its runs use is
143
+ * named, not picked
116
144
  */
117
145
  const CubeProjectIngestTab = observer((props: { tab: CubeIngestTabState }) => {
118
146
  const { tab } = props;
@@ -142,9 +170,11 @@ const CubeProjectIngestTab = observer((props: { tab: CubeIngestTabState }) => {
142
170
  {candidates.map((candidate) => (
143
171
  <CubePickerListItem
144
172
  key={candidate.urn}
145
- picked={tab.definition?.urn === candidate.urn}
173
+ picked={tab.isDefinitionOpened(candidate)}
146
174
  title={candidate.definition}
147
175
  onPick={() => tab.selectDefinition(candidate)}
176
+ onToggle={() => tab.toggleDefinition(candidate)}
177
+ toggleLabel={candidate.name}
148
178
  >
149
179
  <span className="shrink-0">{candidate.name}</span>
150
180
  <span className="min-w-0 flex-1 truncate text-sm text-[var(--color-text-muted)]">
@@ -178,8 +208,8 @@ const CubeProjectIngestTab = observer((props: { tab: CubeIngestTabState }) => {
178
208
 
179
209
  /**
180
210
  * The source dialog's Ingest tab (PLAN §6.7): Mode, the viewer's
181
- * environment (read-only), a producer deployment, one of its definitions,
182
- * then a data set with its columns. The cube's first data set fixes the
211
+ * environment (read-only), a producer deployment, one of its definitions, or
212
+ * several ticked, then data sets with their columns. The cube's first data set fixes the
183
213
  * Mode, the producer deployment and the warehouse.
184
214
  */
185
215
  export const CubeIngestTab = observer((props: { tab: CubeIngestTabState }) => {
@@ -256,13 +286,15 @@ export const CubeIngestTab = observer((props: { tab: CubeIngestTabState }) => {
256
286
  aria-label="Ingest definitions"
257
287
  >
258
288
  {tab.shownDefinitions.map((candidate) => {
259
- const isPicked = tab.definition?.urn === candidate.urn;
289
+ const isPicked = tab.isDefinitionOpened(candidate);
260
290
  return (
261
291
  <CubePickerListItem
262
292
  key={candidate.urn}
263
293
  picked={isPicked}
264
294
  title={candidate.definition}
265
295
  onPick={() => tab.selectDefinition(candidate)}
296
+ onToggle={() => tab.toggleDefinition(candidate)}
297
+ toggleLabel={candidate.name}
266
298
  >
267
299
  <span className="min-w-0 flex-1 truncate">
268
300
  {candidate.name}
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Copyright (c) 2026-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 { BasePopper } from '@finos/legend-art';
18
+ import { noop } from '@finos/legend-shared';
19
+ import { observer } from 'mobx-react-lite';
20
+ import { useEffect, useId, useMemo, useRef, useState } from 'react';
21
+ import { createPortal } from 'react-dom';
22
+ import {
23
+ CUBE_TOUR_ANCHOR_ATTRIBUTE,
24
+ CUBE_TOUR_LABEL,
25
+ type CUBE_TOUR_ANCHOR,
26
+ type CubeTourStep,
27
+ } from '../../__lib__/LegendCubeTour.js';
28
+ import type { CubeEditorState } from '../../stores/CubeEditorState.js';
29
+ import { CubeButton } from '../CubeButton.js';
30
+
31
+ /**
32
+ * Above the canvas, the grid and the node editor (1250), below MUI's dialogs
33
+ * (1300); inside the dialog that hosts the page, if one does
34
+ */
35
+ export const CUBE_TOUR_Z_INDEX = 1290;
36
+
37
+ /** Room around the controls shown, and between them and the card */
38
+ const PADDING = 4;
39
+ const GAP = 12;
40
+ /** The card's width (Legend's root is 62.5%, so it is written in pixels) */
41
+ const CARD_WIDTH = 340;
42
+ const DIM = 'rgba(0, 0, 0, 0.45)';
43
+
44
+ const MODIFIERS = [
45
+ { name: 'offset', options: { offset: [0, GAP + PADDING] } },
46
+ { name: 'flip', options: { padding: GAP } },
47
+ {
48
+ name: 'preventOverflow',
49
+ options: { rootBoundary: 'viewport', padding: GAP, altAxis: true },
50
+ },
51
+ ];
52
+
53
+ type CubePopperInstance = NonNullable<
54
+ Extract<
55
+ React.ComponentProps<typeof BasePopper>['popperRef'],
56
+ React.RefObject<unknown>
57
+ >['current']
58
+ >;
59
+
60
+ interface Box {
61
+ left: number;
62
+ top: number;
63
+ width: number;
64
+ height: number;
65
+ }
66
+
67
+ /** The page's controls that carry the anchor */
68
+ const findAnchored = (root: Element, anchor: CUBE_TOUR_ANCHOR): Element[] =>
69
+ Array.from(
70
+ root.querySelectorAll(`[${CUBE_TOUR_ANCHOR_ATTRIBUTE}~="${anchor}"]`),
71
+ );
72
+
73
+ /** The box around the anchor's controls that have a size; none if none has */
74
+ const boxOf = (root: Element, anchor: CUBE_TOUR_ANCHOR): Box | undefined =>
75
+ findAnchored(root, anchor)
76
+ .map((element) => element.getBoundingClientRect())
77
+ .filter((rect) => rect.width > 0 || rect.height > 0)
78
+ .reduce<Box | undefined>((box, rect) => {
79
+ if (!box) {
80
+ return {
81
+ left: rect.left,
82
+ top: rect.top,
83
+ width: rect.width,
84
+ height: rect.height,
85
+ };
86
+ }
87
+ const left = Math.min(box.left, rect.left);
88
+ const top = Math.min(box.top, rect.top);
89
+ return {
90
+ left,
91
+ top,
92
+ width: Math.max(box.left + box.width, rect.right) - left,
93
+ height: Math.max(box.top + box.height, rect.bottom) - top,
94
+ };
95
+ }, undefined);
96
+
97
+ const isSameBox = (a: Box | undefined, b: Box | undefined): boolean =>
98
+ a === b ||
99
+ (a !== undefined &&
100
+ b !== undefined &&
101
+ a.left === b.left &&
102
+ a.top === b.top &&
103
+ a.width === b.width &&
104
+ a.height === b.height);
105
+
106
+ /**
107
+ * The editor's guided tour (PLAN §11.21): the page dimmed but for the
108
+ * current step's controls, and a card beside them, or in the middle of the
109
+ * page for a step that points at nothing. The controls are followed as the
110
+ * page moves them (a panel resized, the grid loaded). Clicks on the page
111
+ * are held while it shows; Esc ends it, the arrow keys move through it.
112
+ */
113
+ export const CubeTour = observer(
114
+ (props: {
115
+ editorState: CubeEditorState;
116
+ /** The page, whose controls the steps point at */
117
+ root: HTMLElement | null;
118
+ }) => {
119
+ const { editorState, root } = props;
120
+ const { tour } = editorState;
121
+ const { step } = tour;
122
+ const titleId = useId();
123
+ const textId = useId();
124
+ const [box, setBox] = useState<Box | undefined>(undefined);
125
+ const popperRef = useRef<CubePopperInstance>(null);
126
+ const isShown = useMemo(
127
+ () =>
128
+ (candidate: CubeTourStep): boolean =>
129
+ candidate.anchor === undefined ||
130
+ (root !== null && findAnchored(root, candidate.anchor).length > 0),
131
+ [root],
132
+ );
133
+
134
+ // the step's controls followed, frame by frame, while it shows
135
+ const anchor = step?.anchor;
136
+ useEffect(() => {
137
+ if (anchor === undefined || !root) {
138
+ setBox(undefined);
139
+ return undefined;
140
+ }
141
+ let frame = 0;
142
+ const follow = (): void => {
143
+ const next = boxOf(root, anchor);
144
+ setBox((current) => (isSameBox(current, next) ? current : next));
145
+ frame = requestAnimationFrame(follow);
146
+ };
147
+ follow();
148
+ return () => cancelAnimationFrame(frame);
149
+ }, [anchor, root]);
150
+ useEffect(() => {
151
+ popperRef.current?.update().catch(noop());
152
+ }, [box]);
153
+
154
+ // the tour's keys come first: the page's shortcuts wait while it shows
155
+ const isOpen = step !== undefined;
156
+ useEffect(() => {
157
+ if (!isOpen) {
158
+ return undefined;
159
+ }
160
+ const onKeyDown = (event: KeyboardEvent): void => {
161
+ if (event.key === 'Escape') {
162
+ tour.close();
163
+ } else if (event.key === 'ArrowRight') {
164
+ tour.move(1, isShown);
165
+ } else if (event.key === 'ArrowLeft') {
166
+ tour.move(-1, isShown);
167
+ } else {
168
+ return;
169
+ }
170
+ event.preventDefault();
171
+ event.stopPropagation();
172
+ };
173
+ window.addEventListener('keydown', onKeyDown, true);
174
+ return () => window.removeEventListener('keydown', onKeyDown, true);
175
+ }, [isOpen, tour, isShown]);
176
+
177
+ const spotlight = useMemo(
178
+ () =>
179
+ box && {
180
+ left: box.left - PADDING,
181
+ top: box.top - PADDING,
182
+ width: box.width + 2 * PADDING,
183
+ height: box.height + 2 * PADDING,
184
+ },
185
+ [box],
186
+ );
187
+ // the popper reads the spotlight wherever it places the card
188
+ const latest = useRef(spotlight);
189
+ latest.current = spotlight;
190
+ const virtualAnchor = useMemo(
191
+ () => ({
192
+ getBoundingClientRect: (): DOMRect => {
193
+ const current = latest.current ?? {
194
+ left: 0,
195
+ top: 0,
196
+ width: 0,
197
+ height: 0,
198
+ };
199
+ return DOMRect.fromRect({
200
+ x: current.left,
201
+ y: current.top,
202
+ width: current.width,
203
+ height: current.height,
204
+ });
205
+ },
206
+ }),
207
+ [],
208
+ );
209
+
210
+ if (!step || !root) {
211
+ return null;
212
+ }
213
+ // counted over the whole tour, steady while an example loads and adds
214
+ // the controls of later steps; a step skipped skips its number
215
+ const position = tour.index + 1;
216
+ const isLast = tour.steps.filter(isShown).at(-1) === step;
217
+ // a page hosted in a dialog, as Studio hosts Cube, keeps the tour in the
218
+ // dialog's layer, above the page
219
+ const container =
220
+ root.closest<HTMLElement>('.MuiDialog-root') ?? root.ownerDocument.body;
221
+ const card = (
222
+ <div
223
+ // a new card for each step, so its Next button takes the focus
224
+ key={tour.index}
225
+ className="legend-cube__tour flex flex-col gap-2 rounded-md border border-[var(--color-border-default)] bg-[var(--color-bg-elevated)] p-3 text-[var(--color-text-primary)] shadow-lg"
226
+ style={{ width: CARD_WIDTH, maxWidth: `calc(100vw - ${2 * GAP}px)` }}
227
+ role="dialog"
228
+ aria-modal="true"
229
+ aria-labelledby={titleId}
230
+ aria-describedby={textId}
231
+ >
232
+ <div className="flex items-baseline gap-2">
233
+ <h2 id={titleId} className="min-w-0 flex-1 text-lg font-medium">
234
+ {step.title}
235
+ </h2>
236
+ <span className="shrink-0 text-sm text-[var(--color-text-secondary)]">
237
+ {CUBE_TOUR_LABEL.progress(position, tour.steps.length)}
238
+ </span>
239
+ </div>
240
+ <p id={textId} className="text-base leading-snug">
241
+ {step.text}
242
+ {tour.openedExample && tour.index === 0 && (
243
+ <> {CUBE_TOUR_LABEL.EXAMPLE_OPENED}</>
244
+ )}
245
+ </p>
246
+ <div className="flex items-center gap-2">
247
+ {!isLast && (
248
+ <button
249
+ type="button"
250
+ className="text-sm text-[var(--color-text-secondary)] underline hover:text-[var(--color-text-primary)]"
251
+ onClick={() => tour.close()}
252
+ >
253
+ {CUBE_TOUR_LABEL.END}
254
+ </button>
255
+ )}
256
+ <span className="flex-1" />
257
+ {position > 1 && (
258
+ <CubeButton onClick={() => tour.move(-1, isShown)}>
259
+ {CUBE_TOUR_LABEL.BACK}
260
+ </CubeButton>
261
+ )}
262
+ <CubeButton
263
+ primary={true}
264
+ // the keyboard follows the tour: Enter moves on
265
+ autoFocus={true}
266
+ onClick={() => tour.move(1, isShown)}
267
+ >
268
+ {isLast ? CUBE_TOUR_LABEL.DONE : CUBE_TOUR_LABEL.NEXT}
269
+ </CubeButton>
270
+ </div>
271
+ </div>
272
+ );
273
+ return createPortal(
274
+ <div
275
+ className="legend-cube__tour-layer"
276
+ style={{ position: 'fixed', inset: 0, zIndex: CUBE_TOUR_Z_INDEX }}
277
+ // the page underneath takes no clicks, and shows no menu, meanwhile
278
+ onContextMenu={(event) => event.preventDefault()}
279
+ >
280
+ {spotlight ? (
281
+ <>
282
+ <div
283
+ aria-hidden={true}
284
+ style={{
285
+ position: 'fixed',
286
+ ...spotlight,
287
+ borderRadius: 6,
288
+ boxShadow: `0 0 0 2px var(--color-accent), 0 0 0 9999px ${DIM}`,
289
+ pointerEvents: 'none',
290
+ }}
291
+ />
292
+ <BasePopper
293
+ open={true}
294
+ role="presentation"
295
+ popperRef={popperRef}
296
+ anchorEl={virtualAnchor}
297
+ container={container}
298
+ placement={step.placement ?? 'bottom'}
299
+ modifiers={MODIFIERS}
300
+ style={{ zIndex: CUBE_TOUR_Z_INDEX + 1 }}
301
+ >
302
+ {card}
303
+ </BasePopper>
304
+ </>
305
+ ) : (
306
+ <div
307
+ className="flex items-center justify-center"
308
+ style={{ position: 'fixed', inset: 0, background: DIM }}
309
+ >
310
+ {card}
311
+ </div>
312
+ )}
313
+ </div>,
314
+ container,
315
+ );
316
+ },
317
+ );
package/src/index.ts CHANGED
@@ -63,6 +63,13 @@ export {
63
63
  isCubeProjectHost,
64
64
  isCubeSampleDataOffered,
65
65
  } from './stores/CubeHost.js';
66
+ export {
67
+ buildLegendCubeLakehouseServices,
68
+ buildLegendCubeProjectCatalog,
69
+ LegendCubeHost,
70
+ type LegendCubeHostConfig,
71
+ type LegendCubeHostParts,
72
+ } from './stores/LegendCubeHost.js';
66
73
  export { getCubeRememberedWarehouse } from './stores/CubeDataProductWarehouse.js';
67
74
  export {
68
75
  CUBE_LAMBDA_IMPORT_MESSAGE,
@@ -104,6 +104,7 @@ import {
104
104
  import { recheckCubeFunctionSources } from './CubeFunctionRecheck.js';
105
105
  import { CubeDataProductRuntimeState } from './CubeDataProductRuntimeState.js';
106
106
  import { CubeExamplesState } from './CubeExamplesState.js';
107
+ import { CubeTourState } from './CubeTourState.js';
107
108
  import { CubeExportState } from './CubeExportState.js';
108
109
  import { CubeGridFetcher } from './CubeGridFetcher.js';
109
110
  import { CubeGridState } from './CubeGridState.js';
@@ -123,6 +124,8 @@ import { getCubeExtendUpstream } from './CubeExtendTyping.js';
123
124
  import { type CubeHost, loadCubeModelOutline } from './CubeHost.js';
124
125
  import { CubeNodeEditorState } from './CubeNodeEditorState.js';
125
126
  import { CubeShowPureState } from './CubeShowPureState.js';
127
+ import { CubeJsonViewerState } from './variant/CubeJsonViewerState.js';
128
+ import { CubeJsonExplorerState } from './variant/CubeJsonExplorerState.js';
126
129
  import { CubeLambdaImportState } from './CubeLambdaImportState.js';
127
130
  import { CubeSourcePickerState } from './CubeSourcePickerState.js';
128
131
  import { CubeSpecTransferState } from './CubeSpecTransferState.js';
@@ -199,6 +202,12 @@ export class CubeEditorState implements CommandRegistrar {
199
202
  readonly specTransfer: CubeSpecTransferState;
200
203
  readonly examples: CubeExamplesState;
201
204
  readonly showPure: CubeShowPureState;
205
+ /** The editor's guided tour (PLAN §11.21) */
206
+ readonly tour: CubeTourState;
207
+ /** Open JSON: a Variant cell's document as a tree (PLAN §11.20) */
208
+ readonly jsonViewer = new CubeJsonViewerState();
209
+ /** Explore JSON: a Variant column's fields, and the columns they can become (PLAN §11.20) */
210
+ readonly jsonExplorer: CubeJsonExplorerState;
202
211
  /** Import Pure: the lambda the page opened on, read into the cube (PLAN §11.18) */
203
212
  readonly lambdaImport: CubeLambdaImportState;
204
213
  readonly nodeEditor: CubeNodeEditorState;
@@ -316,6 +325,7 @@ export class CubeEditorState implements CommandRegistrar {
316
325
  connect: action,
317
326
  addNode: action,
318
327
  addConfiguredNode: action,
328
+ addConfiguredNodes: action,
319
329
  dropNode: action,
320
330
  removeNode: action,
321
331
  copiedNode: observable.ref,
@@ -368,6 +378,8 @@ export class CubeEditorState implements CommandRegistrar {
368
378
  this.specTransfer = new CubeSpecTransferState(this);
369
379
  this.examples = new CubeExamplesState(this);
370
380
  this.showPure = new CubeShowPureState(this);
381
+ this.tour = new CubeTourState(this);
382
+ this.jsonExplorer = new CubeJsonExplorerState(this);
371
383
  this.lambdaImport = new CubeLambdaImportState(this);
372
384
  this.nodeEditor = new CubeNodeEditorState(this);
373
385
  this.rechecking = reaction(
@@ -762,7 +774,7 @@ export class CubeEditorState implements CommandRegistrar {
762
774
  }
763
775
  }
764
776
 
765
- /** One of Cube's dialogs is open: the source picker, Examples, the group rows' settings, Import or Export, Show Pure */
777
+ /** One of Cube's dialogs is open: the source picker, Examples, the group rows' settings, Import or Export, Show Pure, or the tour */
766
778
  get isDialogOpen(): boolean {
767
779
  return (
768
780
  this.sourcePicker.isOpen ||
@@ -770,7 +782,8 @@ export class CubeEditorState implements CommandRegistrar {
770
782
  this.grid.isSettingsShown ||
771
783
  this.grid.formatColumn !== undefined ||
772
784
  this.specTransfer.mode !== undefined ||
773
- this.showPure.isOpen
785
+ this.showPure.isOpen ||
786
+ this.tour.isOpen
774
787
  );
775
788
  }
776
789
 
@@ -1505,11 +1518,29 @@ export class CubeEditorState implements CommandRegistrar {
1505
1518
  * read-only cube, or when the query can't take it.
1506
1519
  */
1507
1520
  addConfiguredNode(node: QueryNode, afterId?: string): void {
1508
- const { query } = this.document;
1509
- if (this.readOnly || !query.canAdd(node, afterId)) {
1521
+ this.addConfiguredNodes([node], afterId);
1522
+ }
1523
+
1524
+ /**
1525
+ * Adds nodes made elsewhere one after another, the first spliced in after
1526
+ * `afterId`, as one undo step, the last then running when that one did
1527
+ * (Explore JSON's Extend then Explode Array). Does nothing in a read-only
1528
+ * cube, or when the query can't take one of them.
1529
+ */
1530
+ addConfiguredNodes(nodes: readonly QueryNode[], afterId?: string): void {
1531
+ if (this.readOnly || !nodes.length) {
1510
1532
  return;
1511
1533
  }
1512
- this.applyQuery(query.add(node, afterId));
1534
+ let query = this.document.query;
1535
+ let after = afterId;
1536
+ for (const node of nodes) {
1537
+ if (!query.canAdd(node, after)) {
1538
+ return;
1539
+ }
1540
+ query = query.add(node, after);
1541
+ after = node.id;
1542
+ }
1543
+ this.applyQuery(query);
1513
1544
  }
1514
1545
 
1515
1546
  /** Whether dropping a node on another does anything: connect it, or else move it after it */