@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,229 @@
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
+ // A Variant cell's JSON document (PLAN §11.20), as the engine sends it: its
18
+ // compact text. Read here with each number kept as written, so `1` and `1.0`
19
+ // stay apart and an id past 2^53 keeps its digits, and each object's keys in
20
+ // the order written (a JS object would put keys that look like integers first).
21
+
22
+ /** A JSON value, numbers kept as written and keys in their order */
23
+ export type CubeJsonNode =
24
+ | {
25
+ readonly kind: 'object';
26
+ readonly entries: readonly (readonly [string, CubeJsonNode])[];
27
+ }
28
+ | { readonly kind: 'array'; readonly items: readonly CubeJsonNode[] }
29
+ | { readonly kind: 'number'; readonly text: string }
30
+ | { readonly kind: 'string'; readonly value: string }
31
+ | { readonly kind: 'boolean'; readonly value: boolean }
32
+ | { readonly kind: 'null' };
33
+
34
+ const WHITESPACE = new Set([' ', '\t', '\n', '\r']);
35
+ const NUMBER = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/uy;
36
+
37
+ /** A JSON document; throws a `SyntaxError` on text that isn't one */
38
+ export const parseCubeJson = (text: string): CubeJsonNode => {
39
+ let at = 0;
40
+ const fail = (what: string): never => {
41
+ throw new SyntaxError(`${what} at ${at} in JSON`);
42
+ };
43
+ const skipWhitespace = (): void => {
44
+ while (at < text.length && WHITESPACE.has(text.charAt(at))) {
45
+ at += 1;
46
+ }
47
+ };
48
+ const readString = (): string => {
49
+ const start = at;
50
+ at += 1;
51
+ while (at < text.length && text.charAt(at) !== '"') {
52
+ at += text.charAt(at) === '\\' ? 2 : 1;
53
+ }
54
+ if (at >= text.length) {
55
+ fail('An unterminated string');
56
+ }
57
+ at += 1;
58
+ // a string loses nothing through JSON.parse: only numbers do
59
+ return JSON.parse(text.slice(start, at)) as string;
60
+ };
61
+ const readValue = (): CubeJsonNode => {
62
+ skipWhitespace();
63
+ const char = text.charAt(at);
64
+ if (char === '{') {
65
+ at += 1;
66
+ const entries: (readonly [string, CubeJsonNode])[] = [];
67
+ skipWhitespace();
68
+ if (text.charAt(at) === '}') {
69
+ at += 1;
70
+ return { kind: 'object', entries };
71
+ }
72
+ for (;;) {
73
+ skipWhitespace();
74
+ if (text.charAt(at) !== '"') {
75
+ fail('A key expected');
76
+ }
77
+ const key = readString();
78
+ skipWhitespace();
79
+ if (text.charAt(at) !== ':') {
80
+ fail('":" expected');
81
+ }
82
+ at += 1;
83
+ entries.push([key, readValue()]);
84
+ skipWhitespace();
85
+ if (text.charAt(at) === ',') {
86
+ at += 1;
87
+ } else if (text.charAt(at) === '}') {
88
+ at += 1;
89
+ return { kind: 'object', entries };
90
+ } else {
91
+ fail('"," or "}" expected');
92
+ }
93
+ }
94
+ }
95
+ if (char === '[') {
96
+ at += 1;
97
+ const items: CubeJsonNode[] = [];
98
+ skipWhitespace();
99
+ if (text.charAt(at) === ']') {
100
+ at += 1;
101
+ return { kind: 'array', items };
102
+ }
103
+ for (;;) {
104
+ items.push(readValue());
105
+ skipWhitespace();
106
+ if (text.charAt(at) === ',') {
107
+ at += 1;
108
+ } else if (text.charAt(at) === ']') {
109
+ at += 1;
110
+ return { kind: 'array', items };
111
+ } else {
112
+ fail('"," or "]" expected');
113
+ }
114
+ }
115
+ }
116
+ if (char === '"') {
117
+ return { kind: 'string', value: readString() };
118
+ }
119
+ for (const [word, node] of [
120
+ ['true', { kind: 'boolean', value: true }],
121
+ ['false', { kind: 'boolean', value: false }],
122
+ ['null', { kind: 'null' }],
123
+ ] as const) {
124
+ if (text.startsWith(word, at)) {
125
+ at += word.length;
126
+ return node;
127
+ }
128
+ }
129
+ NUMBER.lastIndex = at;
130
+ const number = NUMBER.exec(text);
131
+ if (!number) {
132
+ return fail('A value expected');
133
+ }
134
+ at += number[0].length;
135
+ return { kind: 'number', text: number[0] };
136
+ };
137
+ const value = readValue();
138
+ skipWhitespace();
139
+ if (at < text.length) {
140
+ fail('Text after the value');
141
+ }
142
+ return value;
143
+ };
144
+
145
+ /** The document, or `undefined` for text that isn't JSON */
146
+ export const readCubeJson = (text: string): CubeJsonNode | undefined => {
147
+ try {
148
+ return parseCubeJson(text);
149
+ } catch {
150
+ return undefined;
151
+ }
152
+ };
153
+
154
+ /**
155
+ * A key or a string shown bare when it can't be mistaken for anything else:
156
+ * not empty, none of the notation's punctuation or a line break, no space at
157
+ * either end, and not a number, `true`, `false` or `null`; JSON-quoted
158
+ * otherwise
159
+ */
160
+ const showText = (text: string): string =>
161
+ text !== '' &&
162
+ !/[,:{}[\]"\n\r\t]/u.test(text) &&
163
+ text === text.trim() &&
164
+ !/^(?:true|false|null|-?\d.*)$/u.test(text)
165
+ ? text
166
+ : JSON.stringify(text);
167
+
168
+ const showNode = (node: CubeJsonNode): string => {
169
+ switch (node.kind) {
170
+ case 'object':
171
+ return `{${node.entries.map(([key, value]) => `${showText(key)}: ${showNode(value)}`).join(', ')}}`;
172
+ case 'array':
173
+ return `[${node.items.map(showNode).join(', ')}]`;
174
+ case 'string':
175
+ return showText(node.value);
176
+ case 'number':
177
+ return node.text;
178
+ case 'boolean':
179
+ return String(node.value);
180
+ case 'null':
181
+ return 'null';
182
+ default:
183
+ return '';
184
+ }
185
+ };
186
+
187
+ /**
188
+ * A Variant cell on one line, for the eye (PLAN §11.20, as legend-lite's
189
+ * Data Cube shows one): `{name: ann, tier: gold}`, `[a, b]`, numbers as
190
+ * written. Text that isn't JSON is shown as it is; the value itself stays the
191
+ * JSON text, for copying and export.
192
+ */
193
+ export const formatCubeJsonLine = (text: string): string => {
194
+ const node = readCubeJson(text);
195
+ return node ? showNode(node) : text;
196
+ };
197
+
198
+ /** The document as JSON, indented two spaces, numbers as written */
199
+ export const formatCubeJsonPretty = (node: CubeJsonNode, depth = 0): string => {
200
+ const indent = ' '.repeat(depth + 1);
201
+ const closing = ' '.repeat(depth);
202
+ switch (node.kind) {
203
+ case 'object':
204
+ return node.entries.length === 0
205
+ ? '{}'
206
+ : `{\n${node.entries
207
+ .map(
208
+ ([key, value]) =>
209
+ `${indent}${JSON.stringify(key)}: ${formatCubeJsonPretty(value, depth + 1)}`,
210
+ )
211
+ .join(',\n')}\n${closing}}`;
212
+ case 'array':
213
+ return node.items.length === 0
214
+ ? '[]'
215
+ : `[\n${node.items
216
+ .map((item) => `${indent}${formatCubeJsonPretty(item, depth + 1)}`)
217
+ .join(',\n')}\n${closing}]`;
218
+ case 'string':
219
+ return JSON.stringify(node.value);
220
+ case 'number':
221
+ return node.text;
222
+ case 'boolean':
223
+ return String(node.value);
224
+ case 'null':
225
+ return 'null';
226
+ default:
227
+ return '';
228
+ }
229
+ };
@@ -0,0 +1,378 @@
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
+ type CubeDocument,
19
+ Explode,
20
+ Extend,
21
+ type ExtendColumn,
22
+ getVariantDatabaseProblem,
23
+ type QueryNode,
24
+ type SchemaColumn,
25
+ } from '@finos/legend-cube';
26
+ import { assertErrorThrown, type GeneratorFn } from '@finos/legend-shared';
27
+ import {
28
+ action,
29
+ computed,
30
+ flow,
31
+ flowResult,
32
+ makeObservable,
33
+ observable,
34
+ } from 'mobx';
35
+ import {
36
+ CubeEngineError,
37
+ type CubeParsedExpression,
38
+ } from '../../graph-manager/CubeEngine.js';
39
+ import type { CubeEditorState } from '../CubeEditorState.js';
40
+ import { getCubeGridActionBlocker } from '../CubeGridQuickActions.js';
41
+ import { CubeQueryRunAborted } from '../CubeQueryRun.js';
42
+ import {
43
+ type CubeJsonField,
44
+ type CubeJsonOffer,
45
+ getCubeJsonFields,
46
+ getFreeCubeColumnName,
47
+ } from './CubeJsonOffers.js';
48
+ import { readCubeJsonSample } from './CubeJsonSampling.js';
49
+ import type { CubeJsonSample } from './CubeJsonShape.js';
50
+
51
+ /** A field's path as one key, for ticking */
52
+ const pathKey = (path: readonly string[]): string => JSON.stringify(path);
53
+
54
+ /** A ticked offer, by its field and its key */
55
+ const offerKey = (path: readonly string[], offer: CubeJsonOffer): string =>
56
+ `${pathKey(path)} ${offer.key}`;
57
+
58
+ /**
59
+ * Explore JSON (PLAN §11.20): a Variant column's documents, sampled through
60
+ * the cube's own query path (the query up to the node that ran, the column
61
+ * alone, its non-empty values), so the sample is listed in Runs and can be
62
+ * stopped; their fields as a tree; and the offers ticked, added after the
63
+ * node that ran as one undo step: an Extend of the columns, then an Explode
64
+ * Array for one row per element of an array. The engine parses each column's
65
+ * text first; the background check types the Extend, as any typed in the
66
+ * editor.
67
+ */
68
+ export class CubeJsonExplorerState {
69
+ readonly editorState: CubeEditorState;
70
+
71
+ /** The column explored; `undefined` while closed */
72
+ column: SchemaColumn | undefined;
73
+ /** What was read: the first values, or every row */
74
+ sample: CubeJsonSample | undefined;
75
+ isReading = false;
76
+ isAdding = false;
77
+ /** Why the column couldn't be read, or the columns added */
78
+ error: string | undefined;
79
+ search = '';
80
+ /** The field whose offers show */
81
+ selectedPath: readonly string[] = [];
82
+ /** The ticked offers, by field and key */
83
+ ticked = new Map<string, CubeJsonOffer>();
84
+
85
+ /** The cube as it was when the dialog opened, and the node that ran */
86
+ private document: CubeDocument | undefined;
87
+ private afterId: string | undefined;
88
+ private controller: AbortController | undefined;
89
+
90
+ constructor(editorState: CubeEditorState) {
91
+ makeObservable<CubeJsonExplorerState, 'document' | 'afterId'>(this, {
92
+ column: observable.ref,
93
+ sample: observable.ref,
94
+ isReading: observable,
95
+ isAdding: observable,
96
+ error: observable,
97
+ search: observable,
98
+ selectedPath: observable.ref,
99
+ ticked: observable.shallow,
100
+ document: observable.ref,
101
+ afterId: observable,
102
+ isOpen: computed,
103
+ fields: computed,
104
+ selectedField: computed,
105
+ tickedOffers: computed,
106
+ open: flow,
107
+ read: flow,
108
+ add: flow,
109
+ stop: action,
110
+ close: action,
111
+ setSearch: action,
112
+ select: action,
113
+ toggle: action,
114
+ });
115
+ this.editorState = editorState;
116
+ }
117
+
118
+ get isOpen(): boolean {
119
+ return this.column !== undefined;
120
+ }
121
+
122
+ /** The column's fields, from what was read */
123
+ get fields(): CubeJsonField | undefined {
124
+ return this.column && this.sample
125
+ ? getCubeJsonFields(this.column.name, this.sample)
126
+ : undefined;
127
+ }
128
+
129
+ get selectedField(): CubeJsonField | undefined {
130
+ const key = pathKey(this.selectedPath);
131
+ const find = (field: CubeJsonField): CubeJsonField | undefined =>
132
+ pathKey(field.path) === key
133
+ ? field
134
+ : field.children.reduce<CubeJsonField | undefined>(
135
+ (found, child) => found ?? find(child),
136
+ undefined,
137
+ );
138
+ return this.fields ? find(this.fields) : undefined;
139
+ }
140
+
141
+ /** The offers ticked, in the order ticked */
142
+ get tickedOffers(): CubeJsonOffer[] {
143
+ return [...this.ticked.values()];
144
+ }
145
+
146
+ isTicked(path: readonly string[], offer: CubeJsonOffer): boolean {
147
+ return this.ticked.has(offerKey(path, offer));
148
+ }
149
+
150
+ /**
151
+ * Why Explore JSON can't be used on the column now: the grid's blockers
152
+ * (stale rows, edits, a run, a read-only cube), or a database without
153
+ * JSON; `undefined` when it can
154
+ */
155
+ getDisabledReason(): string | undefined {
156
+ const { editorState } = this;
157
+ const blocker = getCubeGridActionBlocker(editorState);
158
+ if (blocker) {
159
+ return blocker;
160
+ }
161
+ const { context, query } = editorState.document;
162
+ const nodeId = query.selected;
163
+ return context?.runtime === undefined || nodeId === undefined
164
+ ? undefined
165
+ : getVariantDatabaseProblem(
166
+ editorState.getRunDatabaseType(
167
+ query,
168
+ nodeId,
169
+ context.model,
170
+ context.runtime,
171
+ ),
172
+ );
173
+ }
174
+
175
+ /** Opens the dialog on a column of the rows shown, and reads its first values */
176
+ *open(column: SchemaColumn): GeneratorFn<void> {
177
+ const { editorState } = this;
178
+ const { document } = editorState;
179
+ this.stop();
180
+ this.column = column;
181
+ this.document = document;
182
+ this.afterId = document.query.selected;
183
+ this.sample = undefined;
184
+ this.error = undefined;
185
+ this.search = '';
186
+ this.selectedPath = [];
187
+ this.ticked.clear();
188
+ // the database the column's rows run on, which must read JSON: its
189
+ // model's outline may not have been needed by the run that showed them
190
+ const context = document.context;
191
+ const nodeId = this.afterId;
192
+ if (context?.runtime !== undefined && nodeId !== undefined) {
193
+ yield flowResult(editorState.loadModelOutline(context.model));
194
+ const problem = getVariantDatabaseProblem(
195
+ editorState.getRunDatabaseType(
196
+ document.query,
197
+ nodeId,
198
+ context.model,
199
+ context.runtime,
200
+ ),
201
+ );
202
+ if (problem) {
203
+ this.error = problem;
204
+ return;
205
+ }
206
+ }
207
+ yield flowResult(this.read(false));
208
+ }
209
+
210
+ /** Reads the column's first values, or every row: either replaces what was read */
211
+ *read(everyRow: boolean): GeneratorFn<void> {
212
+ const { editorState, column, document, afterId } = this;
213
+ if (
214
+ !column ||
215
+ !document ||
216
+ afterId === undefined ||
217
+ document.context?.runtime === undefined ||
218
+ this.isReading
219
+ ) {
220
+ return;
221
+ }
222
+ const controller = new AbortController();
223
+ this.controller = controller;
224
+ this.isReading = true;
225
+ this.error = undefined;
226
+ try {
227
+ const sample = (yield readCubeJsonSample(editorState, {
228
+ document,
229
+ afterId,
230
+ column,
231
+ everyRow,
232
+ abortController: controller,
233
+ })) as CubeJsonSample | undefined;
234
+ if (this.controller !== controller) {
235
+ return;
236
+ }
237
+ this.sample = sample;
238
+ } catch (error) {
239
+ if (
240
+ this.controller !== controller ||
241
+ error instanceof CubeQueryRunAborted
242
+ ) {
243
+ return;
244
+ }
245
+ assertErrorThrown(error);
246
+ this.error =
247
+ error instanceof CubeEngineError ? error.firstLine : error.message;
248
+ } finally {
249
+ if (this.controller === controller) {
250
+ this.controller = undefined;
251
+ this.isReading = false;
252
+ }
253
+ }
254
+ }
255
+
256
+ /** Stops a read in flight, keeping what was read before it */
257
+ stop(): void {
258
+ this.controller?.abort();
259
+ this.controller = undefined;
260
+ this.isReading = false;
261
+ }
262
+
263
+ setSearch(search: string): void {
264
+ this.search = search;
265
+ }
266
+
267
+ select(path: readonly string[]): void {
268
+ this.selectedPath = path;
269
+ }
270
+
271
+ /**
272
+ * Ticks or unticks an offer. One row per element can be ticked for one
273
+ * array at a time: two would give a row for each pair of their elements.
274
+ */
275
+ toggle(path: readonly string[], offer: CubeJsonOffer): void {
276
+ const key = offerKey(path, offer);
277
+ if (this.ticked.has(key)) {
278
+ this.ticked.delete(key);
279
+ return;
280
+ }
281
+ if (offer.explode) {
282
+ [...this.ticked].forEach(([other, ticked]) => {
283
+ if (ticked.explode) {
284
+ this.ticked.delete(other);
285
+ }
286
+ });
287
+ }
288
+ this.ticked.set(key, offer);
289
+ }
290
+
291
+ /**
292
+ * Adds the ticked offers after the node that ran, one undo step: an Extend
293
+ * of the columns, each under its default name or the next free one, then
294
+ * an Explode Array for the array ticked for one row per element, its
295
+ * element named the same way. The engine parses each column's text first,
296
+ * and the dialog stays open with the error if it can't. Nothing is added if
297
+ * the cube changed meanwhile.
298
+ */
299
+ *add(): GeneratorFn<void> {
300
+ const { editorState, document, afterId } = this;
301
+ const ticked = this.tickedOffers;
302
+ const offers = ticked.filter(
303
+ (offer): offer is CubeJsonOffer & { code: string } =>
304
+ offer.code !== undefined,
305
+ );
306
+ const exploded = ticked.find((offer) => offer.explode !== undefined);
307
+ if (
308
+ !document ||
309
+ afterId === undefined ||
310
+ (!offers.length && !exploded) ||
311
+ this.isAdding ||
312
+ editorState.document !== document
313
+ ) {
314
+ return;
315
+ }
316
+ this.isAdding = true;
317
+ this.error = undefined;
318
+ try {
319
+ const { query } = document;
320
+ const extendId = query.generateId(Extend.TYPE);
321
+ const taken = (
322
+ editorState.analysis.schemas.get(afterId)?.columns ?? []
323
+ ).map((column) => column.name);
324
+ const columns: ExtendColumn[] = [];
325
+ for (const [index, offer] of offers.entries()) {
326
+ const name = getFreeCubeColumnName(offer.name, [
327
+ ...taken,
328
+ ...columns.map((column) => column.name),
329
+ ]);
330
+ const parsed = (yield editorState.host.engine.parseExpression(
331
+ offer.code,
332
+ `${extendId}:${index}`,
333
+ )) as CubeParsedExpression;
334
+ columns.push({ name, code: offer.code, lambda: parsed.lambda });
335
+ }
336
+ // as the dialog found it: nothing changed while the engine parsed
337
+ if (editorState.document !== document) {
338
+ return;
339
+ }
340
+ const nodes: QueryNode[] = columns.length
341
+ ? [new Extend(extendId, columns)]
342
+ : [];
343
+ if (exploded?.explode) {
344
+ nodes.push(
345
+ new Explode(
346
+ query.generateId(Explode.TYPE),
347
+ exploded.explode.column,
348
+ exploded.explode.path,
349
+ getFreeCubeColumnName(exploded.name, [
350
+ ...taken,
351
+ ...columns.map((column) => column.name),
352
+ ]),
353
+ ),
354
+ );
355
+ }
356
+ editorState.addConfiguredNodes(nodes, afterId);
357
+ this.close();
358
+ } catch (error) {
359
+ assertErrorThrown(error);
360
+ this.error =
361
+ error instanceof CubeEngineError ? error.firstLine : error.message;
362
+ } finally {
363
+ this.isAdding = false;
364
+ }
365
+ }
366
+
367
+ close(): void {
368
+ this.stop();
369
+ this.column = undefined;
370
+ this.sample = undefined;
371
+ this.document = undefined;
372
+ this.afterId = undefined;
373
+ this.error = undefined;
374
+ this.search = '';
375
+ this.selectedPath = [];
376
+ this.ticked.clear();
377
+ }
378
+ }