@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
@@ -0,0 +1,304 @@
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 {
18
+ clsx,
19
+ Dialog,
20
+ Modal,
21
+ ModalBody,
22
+ ModalFooter,
23
+ ModalFooterButton,
24
+ ModalHeader,
25
+ } from '@finos/legend-art';
26
+ import { flowResult } from 'mobx';
27
+ import { observer } from 'mobx-react-lite';
28
+ import { CUBE_JSON_EXPLODE_OFFER_TITLE } from '../../__lib__/LegendCubeLabels.js';
29
+ import { LEGEND_CUBE_TEST_ID } from '../../__lib__/LegendCubeTesting.js';
30
+ import type { CubeEditorState } from '../../stores/CubeEditorState.js';
31
+ import type { CubeJsonExplorerState } from '../../stores/variant/CubeJsonExplorerState.js';
32
+ import {
33
+ type CubeJsonField,
34
+ filterCubeJsonFields,
35
+ } from '../../stores/variant/CubeJsonOffers.js';
36
+ import { CubeButton } from '../CubeButton.js';
37
+ import { CUBE_PRIMARY_BUTTON_DARK_MODE } from '../CubeDialogTheme.js';
38
+
39
+ const format = (count: number): string => count.toLocaleString('en-US');
40
+
41
+ /** A share, 0 to 1, as a whole percentage */
42
+ const percentOf = (share: number): number => Math.round(share * 100);
43
+
44
+ /** A field's name: its last key, or the column's for the column itself */
45
+ const nameOf = (
46
+ explorer: CubeJsonExplorerState,
47
+ field: CubeJsonField,
48
+ ): string => field.path.at(-1) ?? explorer.column?.name ?? '';
49
+
50
+ /** What was read, on one line: the first values or every row, and why it stopped */
51
+ const CubeJsonExplorerStatus = observer(
52
+ (props: { explorer: CubeJsonExplorerState }) => {
53
+ const { explorer } = props;
54
+ const { sample, column } = explorer;
55
+ let text: string;
56
+ if (explorer.isReading) {
57
+ text = `Reading ${column?.name ?? ''}…`;
58
+ } else if (!sample) {
59
+ text = '';
60
+ } else {
61
+ const values = sample.rows - sample.unreadable;
62
+ const unreadable = sample.unreadable
63
+ ? ` (${format(sample.unreadable)} not JSON)`
64
+ : '';
65
+ text = sample.complete
66
+ ? `All ${format(sample.rows)} values read${unreadable}: the fields, their types and shares cover the whole column.`
67
+ : `The first ${format(sample.rows)} values read${unreadable}: the types and shares are from this sample.`;
68
+ if (values === 0) {
69
+ text = `No JSON in the ${format(sample.rows)} values read.`;
70
+ }
71
+ }
72
+ return (
73
+ <div className="mb-2 flex min-h-6 items-center gap-2 text-sm text-[var(--color-text-secondary)]">
74
+ <span role="status">{text}</span>
75
+ {explorer.isReading ? (
76
+ <CubeButton onClick={() => explorer.stop()}>Stop</CubeButton>
77
+ ) : (
78
+ sample &&
79
+ !sample.complete && (
80
+ <CubeButton
81
+ title="Read every value of the column, for the shape of the whole column: listed in Runs, and it can be stopped"
82
+ onClick={() => {
83
+ // a failed read shows its message in the dialog
84
+ flowResult(explorer.read(true)).catch(() => undefined);
85
+ }}
86
+ >
87
+ Read every row
88
+ </CubeButton>
89
+ )
90
+ )}
91
+ </div>
92
+ );
93
+ },
94
+ );
95
+
96
+ /** A field of the tree, and its fields, indented */
97
+ const CubeJsonFieldRow = observer(
98
+ (props: {
99
+ explorer: CubeJsonExplorerState;
100
+ field: CubeJsonField;
101
+ depth: number;
102
+ }) => {
103
+ const { explorer, field, depth } = props;
104
+ const selected =
105
+ JSON.stringify(field.path) === JSON.stringify(explorer.selectedPath);
106
+ const share = percentOf(field.presence);
107
+ return (
108
+ <>
109
+ <button
110
+ type="button"
111
+ className={clsx(
112
+ 'flex w-full items-baseline gap-2 py-0.5 pr-2 text-left hover:bg-[var(--color-bg-hover)]',
113
+ { 'bg-[var(--color-bg-selected)]': selected },
114
+ )}
115
+ style={{ paddingLeft: `${8 + depth * 16}px` }}
116
+ aria-pressed={selected}
117
+ aria-label={`Field ${[explorer.column?.name ?? '', ...field.path].join('.')}`}
118
+ onClick={() => explorer.select(field.path)}
119
+ >
120
+ <span className="break-all font-mono text-sm text-[var(--color-text-primary)]">
121
+ {nameOf(explorer, field)}
122
+ </span>
123
+ <span className="text-xs text-[var(--color-text-secondary)]">
124
+ {field.description}
125
+ </span>
126
+ {share < 100 && (
127
+ <span
128
+ className="ml-auto text-xs text-[var(--color-text-muted)]"
129
+ title={
130
+ field.path.length
131
+ ? `In ${share}% of the objects it could be in`
132
+ : `In ${share}% of the rows read`
133
+ }
134
+ >
135
+ {share}%
136
+ </span>
137
+ )}
138
+ </button>
139
+ {field.children.map((child) => (
140
+ <CubeJsonFieldRow
141
+ key={JSON.stringify(child.path)}
142
+ explorer={explorer}
143
+ field={child}
144
+ depth={depth + 1}
145
+ />
146
+ ))}
147
+ </>
148
+ );
149
+ },
150
+ );
151
+
152
+ /** The selected field: where it is, what it held, and the columns it can become, to tick */
153
+ const CubeJsonFieldOffers = observer(
154
+ (props: { explorer: CubeJsonExplorerState }) => {
155
+ const { explorer } = props;
156
+ const field = explorer.selectedField;
157
+ if (!field) {
158
+ return null;
159
+ }
160
+ const { offers } = field;
161
+ const share = percentOf(field.presence);
162
+ return (
163
+ <div className="flex flex-col gap-2 p-2">
164
+ <div className="break-all font-mono text-sm text-[var(--color-text-primary)]">
165
+ {[explorer.column?.name ?? '', ...field.path].join(' › ')}
166
+ </div>
167
+ <div className="text-sm text-[var(--color-text-secondary)]">
168
+ {field.description}
169
+ {share < 100
170
+ ? ` · in ${share}% of ${field.path.length ? 'the objects it could be in' : 'the rows read'}`
171
+ : ''}
172
+ </div>
173
+ {field.topValues.length > 0 && (
174
+ <div className="break-all text-xs text-[var(--color-text-secondary)]">
175
+ Values seen: {field.topValues.join(', ')}
176
+ </div>
177
+ )}
178
+ {offers.length ? (
179
+ <div role="group" aria-label="Columns it can become">
180
+ {offers.map((offer) => (
181
+ <label
182
+ key={offer.key}
183
+ className="flex cursor-pointer items-baseline gap-2 py-0.5 text-sm"
184
+ title={offer.code ?? CUBE_JSON_EXPLODE_OFFER_TITLE}
185
+ >
186
+ <input
187
+ type="checkbox"
188
+ checked={explorer.isTicked(field.path, offer)}
189
+ onChange={() => explorer.toggle(field.path, offer)}
190
+ />
191
+ <span className="text-[var(--color-text-primary)]">
192
+ {offer.label}
193
+ </span>
194
+ <span className="text-xs text-[var(--color-text-muted)]">
195
+ {offer.explode ? 'Explode Array' : offer.type} · {offer.name}
196
+ </span>
197
+ </label>
198
+ ))}
199
+ </div>
200
+ ) : (
201
+ <div className="text-sm text-[var(--color-text-muted)]">
202
+ {field.children.length
203
+ ? 'Pick one of its fields.'
204
+ : 'Nothing to make of it.'}
205
+ </div>
206
+ )}
207
+ </div>
208
+ );
209
+ },
210
+ );
211
+
212
+ /**
213
+ * Explore JSON (PLAN §11.20): a Variant column's fields, read from its first
214
+ * values or every row, each with what it held and how often; the selected
215
+ * field's offers, ticked and added as one Extend after the node that ran
216
+ */
217
+ export const CubeJsonExplorerDialog = observer(
218
+ (props: { editorState: CubeEditorState }) => {
219
+ const { editorState } = props;
220
+ const explorer = editorState.jsonExplorer;
221
+ const { applicationStore } = editorState.host;
222
+ const darkMode =
223
+ !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled;
224
+ const fields = explorer.fields
225
+ ? filterCubeJsonFields(explorer.fields, explorer.search)
226
+ : undefined;
227
+ const count = explorer.tickedOffers.length;
228
+ const add = (): void => {
229
+ flowResult(explorer.add()).catch(applicationStore.alertUnhandledError);
230
+ };
231
+ return (
232
+ <Dialog open={explorer.isOpen} onClose={() => explorer.close()}>
233
+ <Modal darkMode={darkMode} className="w-[960px] max-w-full">
234
+ <ModalHeader>
235
+ <div className="modal__title">{`Explore JSON: ${explorer.column?.name ?? ''}`}</div>
236
+ </ModalHeader>
237
+ <ModalBody>
238
+ <div data-testid={LEGEND_CUBE_TEST_ID.JSON_EXPLORER}>
239
+ <CubeJsonExplorerStatus explorer={explorer} />
240
+ {explorer.error && (
241
+ <div
242
+ role="alert"
243
+ className="mb-2 break-words text-sm text-[var(--color-status-error)]"
244
+ >
245
+ {explorer.error}
246
+ </div>
247
+ )}
248
+ {explorer.fields && (
249
+ <div className="grid h-[50vh] grid-cols-2 gap-2">
250
+ <div className="flex min-h-0 flex-col rounded-sm border border-[var(--color-border-default)] bg-[var(--color-bg-panel)]">
251
+ <input
252
+ className="m-1 rounded-sm border border-[var(--color-border-default)] px-1 text-sm"
253
+ placeholder="Find a field"
254
+ aria-label="Find a field"
255
+ value={explorer.search}
256
+ onChange={(event) =>
257
+ explorer.setSearch(event.target.value)
258
+ }
259
+ />
260
+ <div
261
+ className="min-h-0 flex-1 overflow-auto"
262
+ aria-label="Fields"
263
+ >
264
+ {fields ? (
265
+ <CubeJsonFieldRow
266
+ explorer={explorer}
267
+ field={fields}
268
+ depth={0}
269
+ />
270
+ ) : (
271
+ <div className="p-2 text-sm text-[var(--color-text-muted)]">
272
+ No field matches.
273
+ </div>
274
+ )}
275
+ </div>
276
+ </div>
277
+ <div className="min-h-0 overflow-auto rounded-sm border border-[var(--color-border-default)] bg-[var(--color-bg-panel)]">
278
+ <CubeJsonFieldOffers explorer={explorer} />
279
+ </div>
280
+ </div>
281
+ )}
282
+ </div>
283
+ </ModalBody>
284
+ <ModalFooter>
285
+ <ModalFooterButton
286
+ darkMode={CUBE_PRIMARY_BUTTON_DARK_MODE}
287
+ formatText={false}
288
+ text={count === 1 ? 'Add 1 column' : `Add ${count} columns`}
289
+ disabled={!count || explorer.isAdding || explorer.isReading}
290
+ onClick={add}
291
+ />
292
+ <ModalFooterButton
293
+ darkMode={darkMode}
294
+ formatText={false}
295
+ type="secondary"
296
+ text="Cancel"
297
+ onClick={() => explorer.close()}
298
+ />
299
+ </ModalFooter>
300
+ </Modal>
301
+ </Dialog>
302
+ );
303
+ },
304
+ );
@@ -0,0 +1,190 @@
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 {
18
+ CaretDownIcon,
19
+ CaretRightIcon,
20
+ Dialog,
21
+ Modal,
22
+ ModalBody,
23
+ ModalFooter,
24
+ ModalFooterButton,
25
+ ModalHeader,
26
+ } from '@finos/legend-art';
27
+ import { observer } from 'mobx-react-lite';
28
+ import { useState } from 'react';
29
+ import { LEGEND_CUBE_TEST_ID } from '../../__lib__/LegendCubeTesting.js';
30
+ import type { CubeEditorState } from '../../stores/CubeEditorState.js';
31
+ import {
32
+ type CubeJsonNode,
33
+ formatCubeJsonPretty,
34
+ } from '../../stores/variant/CubeJson.js';
35
+ import { CUBE_PRIMARY_BUTTON_DARK_MODE } from '../CubeDialogTheme.js';
36
+
37
+ /** Levels open when the dialog opens: the document and its first fields */
38
+ const OPEN_DEPTH = 2;
39
+
40
+ /** What a closed object or array holds, e.g. `{2 keys}`, `[3 items]` */
41
+ const summarize = (node: CubeJsonNode): string =>
42
+ node.kind === 'object'
43
+ ? `{${node.entries.length} ${node.entries.length === 1 ? 'key' : 'keys'}}`
44
+ : node.kind === 'array'
45
+ ? `[${node.items.length} ${node.items.length === 1 ? 'item' : 'items'}]`
46
+ : '';
47
+
48
+ /** A scalar, coloured by its kind: strings quoted, numbers as written */
49
+ const CubeJsonScalar: React.FC<{ node: CubeJsonNode }> = ({ node }) =>
50
+ node.kind === 'string' ? (
51
+ <span className="break-all text-[var(--color-category-enumeration)]">
52
+ {JSON.stringify(node.value)}
53
+ </span>
54
+ ) : node.kind === 'number' ? (
55
+ <span className="text-[var(--color-category-primitive)]">{node.text}</span>
56
+ ) : node.kind === 'boolean' ? (
57
+ <span className="text-[var(--color-category-class)]">
58
+ {String(node.value)}
59
+ </span>
60
+ ) : (
61
+ <span className="text-[var(--color-text-muted)]">null</span>
62
+ );
63
+
64
+ /** One entry of the tree: its key or index, and its value, open or closed */
65
+ const CubeJsonTreeNode: React.FC<{
66
+ label: string | undefined;
67
+ node: CubeJsonNode;
68
+ depth: number;
69
+ }> = ({ label, node, depth }) => {
70
+ const [open, setOpen] = useState(depth < OPEN_DEPTH);
71
+ const children =
72
+ node.kind === 'object'
73
+ ? node.entries.map(([key, value]) => [key, value] as const)
74
+ : node.kind === 'array'
75
+ ? node.items.map((item, index) => [`[${index}]`, item] as const)
76
+ : undefined;
77
+ const name =
78
+ label === undefined ? undefined : (
79
+ <span className="font-medium text-[var(--color-text-primary)]">
80
+ {label}:{' '}
81
+ </span>
82
+ );
83
+ if (!children) {
84
+ return (
85
+ <div className="py-px pl-5 font-mono text-sm">
86
+ {name}
87
+ <CubeJsonScalar node={node} />
88
+ </div>
89
+ );
90
+ }
91
+ return (
92
+ <div className="font-mono text-sm">
93
+ <button
94
+ type="button"
95
+ className="flex items-center gap-1 py-px text-left hover:bg-[var(--color-bg-hover)]"
96
+ aria-expanded={open}
97
+ aria-label={`${open ? 'Close' : 'Open'} ${label ?? 'the document'}`}
98
+ onClick={() => setOpen(!open)}
99
+ >
100
+ {open ? (
101
+ <CaretDownIcon aria-hidden={true} />
102
+ ) : (
103
+ <CaretRightIcon aria-hidden={true} />
104
+ )}
105
+ {name}
106
+ <span className="text-[var(--color-text-secondary)]">
107
+ {summarize(node)}
108
+ </span>
109
+ </button>
110
+ {open && (
111
+ <div className="ml-2 border-l border-[var(--color-border-subtle)] pl-2">
112
+ {children.map(([key, value]) => (
113
+ <CubeJsonTreeNode
114
+ key={key}
115
+ label={key}
116
+ node={value}
117
+ depth={depth + 1}
118
+ />
119
+ ))}
120
+ </div>
121
+ )}
122
+ </div>
123
+ );
124
+ };
125
+
126
+ /**
127
+ * Open JSON (PLAN §11.20): a Variant cell's document as a tree, each object
128
+ * and array open to `OPEN_DEPTH` and opened or closed by a click, with Copy
129
+ * JSON (indented, numbers as written); text that isn't JSON is shown as it is
130
+ */
131
+ export const CubeJsonViewerDialog = observer(
132
+ (props: { editorState: CubeEditorState }) => {
133
+ const { editorState } = props;
134
+ const viewer = editorState.jsonViewer;
135
+ const { applicationStore } = editorState.host;
136
+ const darkMode =
137
+ !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled;
138
+ const document = viewer.document;
139
+ const copy = (): void => {
140
+ const text = document
141
+ ? formatCubeJsonPretty(document)
142
+ : (viewer.text ?? '');
143
+ applicationStore.clipboardService
144
+ .copyTextToClipboard(text)
145
+ .catch(applicationStore.alertUnhandledError);
146
+ };
147
+ return (
148
+ <Dialog open={viewer.isOpen} onClose={() => viewer.close()}>
149
+ <Modal darkMode={darkMode} className="w-[720px] max-w-full">
150
+ <ModalHeader>
151
+ <div className="modal__title">{`JSON of ${viewer.column ?? ''}`}</div>
152
+ </ModalHeader>
153
+ <ModalBody>
154
+ <div
155
+ className="max-h-[60vh] overflow-auto rounded-sm border border-[var(--color-border-default)] bg-[var(--color-bg-panel)] p-2"
156
+ aria-label="JSON document"
157
+ data-testid={LEGEND_CUBE_TEST_ID.JSON_VIEWER}
158
+ >
159
+ {document ? (
160
+ <CubeJsonTreeNode
161
+ key={viewer.text}
162
+ label={undefined}
163
+ node={document}
164
+ depth={0}
165
+ />
166
+ ) : (
167
+ <div className="break-all font-mono text-sm">{viewer.text}</div>
168
+ )}
169
+ </div>
170
+ </ModalBody>
171
+ <ModalFooter>
172
+ <ModalFooterButton
173
+ darkMode={CUBE_PRIMARY_BUTTON_DARK_MODE}
174
+ formatText={false}
175
+ text="Copy JSON"
176
+ onClick={copy}
177
+ />
178
+ <ModalFooterButton
179
+ darkMode={darkMode}
180
+ formatText={false}
181
+ type="secondary"
182
+ text="Close"
183
+ onClick={() => viewer.close()}
184
+ />
185
+ </ModalFooter>
186
+ </Modal>
187
+ </Dialog>
188
+ );
189
+ },
190
+ );
@@ -19,11 +19,12 @@ import {
19
19
  AggregationFunction,
20
20
  type CheckedDrilldown,
21
21
  type ColumnFormat,
22
+ type CubeType,
22
23
  DEFAULT_PRESENTATION,
23
24
  foldColumnName,
24
25
  getColumnFormat,
25
26
  type Presentation,
26
- type TypeFamily,
27
+ TypeFamily,
27
28
  } from '@finos/legend-cube';
28
29
  import {
29
30
  DataGrid,
@@ -34,17 +35,30 @@ import {
34
35
  type DataGridGetContextMenuItemsParams,
35
36
  type DataGridIAggFuncParams,
36
37
  type DataGridMenuItemDef,
38
+ type DataGridOptions,
37
39
  } from '@finos/legend-lego/data-grid';
38
40
  import { flowResult } from 'mobx';
39
41
  import { memo, useCallback, useMemo } from 'react';
40
42
  import {
41
43
  CUBE_FORMAT_NOT_A_NUMBER,
44
+ CUBE_JSON_DISABLED_REASON,
42
45
  NULL_CELL_TEXT,
43
46
  READ_ONLY_CUBE_TITLE,
44
47
  } from '../../__lib__/LegendCubeLabels.js';
45
48
  import { LEGEND_CUBE_TEST_ID } from '../../__lib__/LegendCubeTesting.js';
46
- import type { CubeResultValue } from '../../graph-manager/CubeEngine.js';
49
+ import {
50
+ CUBE_TOUR_ANCHOR,
51
+ cubeTourAnchor,
52
+ } from '../../__lib__/LegendCubeTour.js';
53
+ import {
54
+ CubeEngineError,
55
+ type CubeResultValue,
56
+ } from '../../graph-manager/CubeEngine.js';
47
57
  import type { CubeEditorState } from '../../stores/CubeEditorState.js';
58
+ import {
59
+ addCubeJsonParseColumn,
60
+ isCubeJsonDocumentText,
61
+ } from '../../stores/variant/CubeJsonParse.js';
48
62
  import type { CubeExecutionResult } from '../../stores/CubeExecutionState.js';
49
63
  import { getCubeExportFileName } from '../../stores/CubeExportState.js';
50
64
  import {
@@ -170,6 +184,126 @@ const CUBE_GRID_MENU_ITEMS: readonly DataGridDefaultMenuItem[] = [
170
184
  'copyWithHeaders',
171
185
  ];
172
186
 
187
+ /**
188
+ * Explore JSON… on a Variant column (PLAN §11.20), in its cells' menu and its
189
+ * header's: the column's fields, and the columns they can become
190
+ */
191
+ export const getCubeExploreJsonItem = <Row extends CubeAnyRow>(
192
+ editorState: CubeEditorState,
193
+ columnName: string,
194
+ ): DataGridMenuItemDef<Row> => {
195
+ const column = editorState.execution.result?.schema.columns.find(
196
+ ({ name }) => name === columnName,
197
+ );
198
+ const reason = editorState.jsonExplorer.getDisabledReason();
199
+ return {
200
+ name: 'Explore JSON…',
201
+ disabled: reason !== undefined || !column,
202
+ ...(reason === undefined ? {} : { tooltip: reason }),
203
+ action: () => {
204
+ if (column && reason === undefined) {
205
+ flowResult(editorState.jsonExplorer.open(column)).catch(
206
+ editorState.host.applicationStore.alertUnhandledError,
207
+ );
208
+ }
209
+ },
210
+ };
211
+ };
212
+
213
+ /**
214
+ * A JSON cell's items (PLAN §11.20): on a Variant cell, Open JSON shows a run
215
+ * row's document as a tree (a group row's cell, or an empty one, has none to
216
+ * show), and Explore JSON…; on a text cell holding a JSON object or array,
217
+ * Parse as JSON
218
+ */
219
+ const getCubeVariantMenuItems = <Row extends CubeAnyRow>(
220
+ editorState: CubeEditorState,
221
+ column: { readonly name: string; readonly type: CubeType },
222
+ cell: CubeResultValue,
223
+ kind: CubeGridCellKind,
224
+ ): DataGridMenuItemDef<Row>[] => {
225
+ // a text cell holding a JSON object or array: Parse as JSON
226
+ if (
227
+ column.type.family === TypeFamily.STRING &&
228
+ kind === CubeGridCellKind.VALUE &&
229
+ typeof cell === 'string' &&
230
+ isCubeJsonDocumentText(cell)
231
+ ) {
232
+ const reason = editorState.jsonExplorer.getDisabledReason();
233
+ const { applicationStore } = editorState.host;
234
+ return [
235
+ {
236
+ name: 'Parse as JSON',
237
+ disabled: reason !== undefined,
238
+ ...(reason === undefined ? {} : { tooltip: reason }),
239
+ action: () => {
240
+ if (reason === undefined) {
241
+ addCubeJsonParseColumn(editorState, column.name).catch(
242
+ (error: unknown) =>
243
+ applicationStore.notificationService.notifyError(
244
+ error instanceof CubeEngineError
245
+ ? error.firstLine
246
+ : String(error),
247
+ ),
248
+ );
249
+ }
250
+ },
251
+ },
252
+ ];
253
+ }
254
+ if (column.type.family !== TypeFamily.VARIANT) {
255
+ return [];
256
+ }
257
+ const reason =
258
+ kind !== CubeGridCellKind.VALUE
259
+ ? CUBE_JSON_DISABLED_REASON.NOT_A_ROW
260
+ : typeof cell !== 'string'
261
+ ? CUBE_JSON_DISABLED_REASON.EMPTY
262
+ : undefined;
263
+ return [
264
+ {
265
+ name: 'Open JSON',
266
+ disabled: reason !== undefined,
267
+ ...(reason === undefined ? {} : { tooltip: reason }),
268
+ action: () => {
269
+ if (typeof cell === 'string') {
270
+ editorState.jsonViewer.open(column.name, cell);
271
+ }
272
+ },
273
+ },
274
+ getCubeExploreJsonItem<Row>(editorState, column.name),
275
+ ];
276
+ };
277
+
278
+ /** What ag-grid gives a column header's menu */
279
+ export type CubeGridHeaderMenuParams<Row extends CubeAnyRow> = Parameters<
280
+ NonNullable<DataGridOptions<Row>['getMainMenuItems']>
281
+ >[0];
282
+
283
+ /**
284
+ * A column header's menu: ag-grid's own items, after Explore JSON… on a
285
+ * Variant column (PLAN §11.20)
286
+ */
287
+ export const getCubeGridHeaderMenuItems = <Row extends CubeAnyRow>(
288
+ editorState: CubeEditorState,
289
+ params: CubeGridHeaderMenuParams<Row>,
290
+ ): (DataGridDefaultMenuItem | DataGridMenuItemDef<Row>)[] => {
291
+ const defaults = params.defaultItems;
292
+ const colId = params.column?.getColId();
293
+ const column = colId?.startsWith(COLUMN_ID_PREFIX)
294
+ ? editorState.execution.result?.schema.columns[
295
+ Number(colId.slice(COLUMN_ID_PREFIX.length))
296
+ ]
297
+ : undefined;
298
+ return column?.type.family === TypeFamily.VARIANT
299
+ ? [
300
+ getCubeExploreJsonItem<Row>(editorState, column.name),
301
+ 'separator',
302
+ ...defaults,
303
+ ]
304
+ : defaults;
305
+ };
306
+
173
307
  /**
174
308
  * The grid's context menu (spec §12.4, PLAN §11.10), curated: on a cell,
175
309
  * 'Drilldown by' its column and the quick actions, then a separator; then,
@@ -224,6 +358,7 @@ export const getCubeGridContextMenuItems = <Row extends CubeAnyRow>(
224
358
  action: () => quickAction.apply(),
225
359
  };
226
360
  }),
361
+ ...getCubeVariantMenuItems<Row>(editorState, column, cell, kind),
227
362
  'separator',
228
363
  {
229
364
  name: 'Format Column…',
@@ -497,6 +632,11 @@ export const CubeResultGrid = memo(
497
632
  getCubeGridContextMenuItems(editorState, params),
498
633
  [editorState],
499
634
  );
635
+ const getMainMenuItems = useCallback(
636
+ (params: CubeGridHeaderMenuParams<CubeRow>) =>
637
+ getCubeGridHeaderMenuItems(editorState, params),
638
+ [editorState],
639
+ );
500
640
  const columnDefs = useMemo(
501
641
  () =>
502
642
  buildCubeColumnDefs<CubeRow>(
@@ -529,6 +669,7 @@ export const CubeResultGrid = memo(
529
669
  'ag-theme-balham-dark': darkMode,
530
670
  })}
531
671
  data-testid={LEGEND_CUBE_TEST_ID.RESULT_GRID}
672
+ {...cubeTourAnchor(CUBE_TOUR_ANCHOR.RESULTS)}
532
673
  >
533
674
  <DataGrid<CubeRow>
534
675
  rowData={result.rows as CubeRow[]}
@@ -536,6 +677,7 @@ export const CubeResultGrid = memo(
536
677
  suppressFieldDotNotation={true}
537
678
  tooltipShowDelay={500}
538
679
  getContextMenuItems={getContextMenuItems}
680
+ getMainMenuItems={getMainMenuItems}
539
681
  // local mode (spec §12.1): ag-grid's own grouping, pivot and
540
682
  // filters on the run's rows, with their panels and totals
541
683
  defaultColDef={LOCAL_COLUMN_DEFAULTS}