@finos/legend-cube-builder 0.0.2 → 0.0.3

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 (197) hide show
  1. package/README.md +60 -1
  2. package/docs/adding-an-operation.md +92 -0
  3. package/docs/hosting.md +79 -0
  4. package/docs/testing.md +83 -0
  5. package/lib/__lib__/LegendCubeHelpText.d.ts.map +1 -1
  6. package/lib/__lib__/LegendCubeHelpText.js +11 -3
  7. package/lib/__lib__/LegendCubeHelpText.js.map +1 -1
  8. package/lib/__lib__/LegendCubeLabels.d.ts +19 -0
  9. package/lib/__lib__/LegendCubeLabels.d.ts.map +1 -1
  10. package/lib/__lib__/LegendCubeLabels.js +19 -0
  11. package/lib/__lib__/LegendCubeLabels.js.map +1 -1
  12. package/lib/__test-utils__/CubeEngineTestSupport.d.ts +6 -0
  13. package/lib/__test-utils__/CubeEngineTestSupport.d.ts.map +1 -1
  14. package/lib/__test-utils__/CubeEngineTestSupport.js +30 -0
  15. package/lib/__test-utils__/CubeEngineTestSupport.js.map +1 -1
  16. package/lib/__test-utils__/CubeOperationsTestUtils.d.ts +42 -0
  17. package/lib/__test-utils__/CubeOperationsTestUtils.d.ts.map +1 -0
  18. package/lib/__test-utils__/CubeOperationsTestUtils.js +92 -0
  19. package/lib/__test-utils__/CubeOperationsTestUtils.js.map +1 -0
  20. package/lib/__test-utils__/FakeCubeEngine.d.ts.map +1 -1
  21. package/lib/__test-utils__/FakeCubeEngine.js +7 -1
  22. package/lib/__test-utils__/FakeCubeEngine.js.map +1 -1
  23. package/lib/components/CubeNodeIcon.d.ts.map +1 -1
  24. package/lib/components/CubeNodeIcon.js +12 -2
  25. package/lib/components/CubeNodeIcon.js.map +1 -1
  26. package/lib/components/canvas/CubeCanvas.d.ts.map +1 -1
  27. package/lib/components/canvas/CubeCanvas.js +5 -3
  28. package/lib/components/canvas/CubeCanvas.js.map +1 -1
  29. package/lib/components/canvas/CubeCanvasElements.d.ts +2 -0
  30. package/lib/components/canvas/CubeCanvasElements.d.ts.map +1 -1
  31. package/lib/components/canvas/CubeCanvasElements.js +2 -1
  32. package/lib/components/canvas/CubeCanvasElements.js.map +1 -1
  33. package/lib/components/canvas/CubeCanvasNode.d.ts.map +1 -1
  34. package/lib/components/canvas/CubeCanvasNode.js +5 -2
  35. package/lib/components/canvas/CubeCanvasNode.js.map +1 -1
  36. package/lib/components/editors/CubeColumnPicker.d.ts +9 -2
  37. package/lib/components/editors/CubeColumnPicker.d.ts.map +1 -1
  38. package/lib/components/editors/CubeColumnPicker.js +9 -4
  39. package/lib/components/editors/CubeColumnPicker.js.map +1 -1
  40. package/lib/components/editors/CubeDistinctEditor.d.ts +23 -0
  41. package/lib/components/editors/CubeDistinctEditor.d.ts.map +1 -0
  42. package/lib/components/editors/CubeDistinctEditor.js +24 -0
  43. package/lib/components/editors/CubeDistinctEditor.js.map +1 -0
  44. package/lib/components/editors/CubeIntegerField.d.ts +29 -0
  45. package/lib/components/editors/CubeIntegerField.d.ts.map +1 -0
  46. package/lib/components/editors/CubeIntegerField.js +30 -0
  47. package/lib/components/editors/CubeIntegerField.js.map +1 -0
  48. package/lib/components/editors/CubeJoinEditor.d.ts.map +1 -1
  49. package/lib/components/editors/CubeJoinEditor.js +11 -3
  50. package/lib/components/editors/CubeJoinEditor.js.map +1 -1
  51. package/lib/components/editors/CubeNodeEditorPanel.d.ts.map +1 -1
  52. package/lib/components/editors/CubeNodeEditorPanel.js +3 -2
  53. package/lib/components/editors/CubeNodeEditorPanel.js.map +1 -1
  54. package/lib/components/editors/CubeNodeEditorRegistry.d.ts.map +1 -1
  55. package/lib/components/editors/CubeNodeEditorRegistry.js +14 -1
  56. package/lib/components/editors/CubeNodeEditorRegistry.js.map +1 -1
  57. package/lib/components/editors/CubeRenameEditor.d.ts +26 -0
  58. package/lib/components/editors/CubeRenameEditor.d.ts.map +1 -0
  59. package/lib/components/editors/CubeRenameEditor.js +58 -0
  60. package/lib/components/editors/CubeRenameEditor.js.map +1 -0
  61. package/lib/components/editors/CubeRestrictEditor.d.ts +26 -0
  62. package/lib/components/editors/CubeRestrictEditor.d.ts.map +1 -0
  63. package/lib/components/editors/CubeRestrictEditor.js +23 -0
  64. package/lib/components/editors/CubeRestrictEditor.js.map +1 -0
  65. package/lib/components/editors/CubeRowCountEditor.d.ts +25 -0
  66. package/lib/components/editors/CubeRowCountEditor.d.ts.map +1 -0
  67. package/lib/components/editors/CubeRowCountEditor.js +38 -0
  68. package/lib/components/editors/CubeRowCountEditor.js.map +1 -0
  69. package/lib/components/editors/CubeSliceEditor.d.ts +25 -0
  70. package/lib/components/editors/CubeSliceEditor.d.ts.map +1 -0
  71. package/lib/components/editors/CubeSliceEditor.js +40 -0
  72. package/lib/components/editors/CubeSliceEditor.js.map +1 -0
  73. package/lib/components/editors/CubeSortEditor.d.ts +27 -0
  74. package/lib/components/editors/CubeSortEditor.d.ts.map +1 -0
  75. package/lib/components/editors/CubeSortEditor.js +72 -0
  76. package/lib/components/editors/CubeSortEditor.js.map +1 -0
  77. package/lib/components/editors/CubeSourceEditor.d.ts.map +1 -1
  78. package/lib/components/editors/CubeSourceEditor.js +1 -2
  79. package/lib/components/editors/CubeSourceEditor.js.map +1 -1
  80. package/lib/components/editors/CubeValueEditor.d.ts.map +1 -1
  81. package/lib/components/editors/CubeValueEditor.js +2 -1
  82. package/lib/components/editors/CubeValueEditor.js.map +1 -1
  83. package/lib/components/grid/CubeGridRegion.d.ts.map +1 -1
  84. package/lib/components/grid/CubeGridRegion.js +1 -1
  85. package/lib/components/grid/CubeGridRegion.js.map +1 -1
  86. package/lib/components/grid/CubeResultGrid.d.ts +14 -1
  87. package/lib/components/grid/CubeResultGrid.d.ts.map +1 -1
  88. package/lib/components/grid/CubeResultGrid.js +41 -5
  89. package/lib/components/grid/CubeResultGrid.js.map +1 -1
  90. package/lib/graph-manager/CubeEngine.d.ts +13 -0
  91. package/lib/graph-manager/CubeEngine.d.ts.map +1 -1
  92. package/lib/graph-manager/CubeEngine.js.map +1 -1
  93. package/lib/graph-manager/CubeModelOutlineHelper.d.ts +8 -0
  94. package/lib/graph-manager/CubeModelOutlineHelper.d.ts.map +1 -1
  95. package/lib/graph-manager/CubeModelOutlineHelper.js +21 -0
  96. package/lib/graph-manager/CubeModelOutlineHelper.js.map +1 -1
  97. package/lib/graph-manager/protocol/pure/v1/V1_CubeModelOutlineBuilder.d.ts.map +1 -1
  98. package/lib/graph-manager/protocol/pure/v1/V1_CubeModelOutlineBuilder.js +56 -3
  99. package/lib/graph-manager/protocol/pure/v1/V1_CubeModelOutlineBuilder.js.map +1 -1
  100. package/lib/graph-manager/protocol/pure/v1/__test-utils__/V1_CubeEngineTestUtils.d.ts.map +1 -1
  101. package/lib/graph-manager/protocol/pure/v1/__test-utils__/V1_CubeEngineTestUtils.js +3 -3
  102. package/lib/graph-manager/protocol/pure/v1/__test-utils__/V1_CubeEngineTestUtils.js.map +1 -1
  103. package/lib/index.css +1 -1
  104. package/lib/stores/CubeEditorState.d.ts +34 -4
  105. package/lib/stores/CubeEditorState.d.ts.map +1 -1
  106. package/lib/stores/CubeEditorState.js +87 -6
  107. package/lib/stores/CubeEditorState.js.map +1 -1
  108. package/lib/stores/CubeExecutionState.d.ts.map +1 -1
  109. package/lib/stores/CubeExecutionState.js +19 -3
  110. package/lib/stores/CubeExecutionState.js.map +1 -1
  111. package/lib/stores/CubeGridQuickActions.d.ts +40 -0
  112. package/lib/stores/CubeGridQuickActions.d.ts.map +1 -0
  113. package/lib/stores/CubeGridQuickActions.js +86 -0
  114. package/lib/stores/CubeGridQuickActions.js.map +1 -0
  115. package/lib/stores/CubeNodeEditorState.d.ts +19 -1
  116. package/lib/stores/CubeNodeEditorState.d.ts.map +1 -1
  117. package/lib/stores/CubeNodeEditorState.js +55 -0
  118. package/lib/stores/CubeNodeEditorState.js.map +1 -1
  119. package/lib/stores/CubeShowPureState.d.ts +4 -1
  120. package/lib/stores/CubeShowPureState.d.ts.map +1 -1
  121. package/lib/stores/CubeShowPureState.js +22 -5
  122. package/lib/stores/CubeShowPureState.js.map +1 -1
  123. package/lib/stores/editors/CubeIntegerText.d.ts +24 -0
  124. package/lib/stores/editors/CubeIntegerText.d.ts.map +1 -0
  125. package/lib/stores/editors/CubeIntegerText.js +33 -0
  126. package/lib/stores/editors/CubeIntegerText.js.map +1 -0
  127. package/lib/stores/editors/CubeJoinDraft.d.ts +13 -5
  128. package/lib/stores/editors/CubeJoinDraft.d.ts.map +1 -1
  129. package/lib/stores/editors/CubeJoinDraft.js +21 -12
  130. package/lib/stores/editors/CubeJoinDraft.js.map +1 -1
  131. package/lib/stores/editors/CubeNodeDraftRegistry.d.ts +6 -0
  132. package/lib/stores/editors/CubeNodeDraftRegistry.d.ts.map +1 -1
  133. package/lib/stores/editors/CubeNodeDraftRegistry.js +23 -1
  134. package/lib/stores/editors/CubeNodeDraftRegistry.js.map +1 -1
  135. package/lib/stores/editors/CubeRenameDraft.d.ts +44 -0
  136. package/lib/stores/editors/CubeRenameDraft.d.ts.map +1 -0
  137. package/lib/stores/editors/CubeRenameDraft.js +79 -0
  138. package/lib/stores/editors/CubeRenameDraft.js.map +1 -0
  139. package/lib/stores/editors/CubeRestrictDraft.d.ts +37 -0
  140. package/lib/stores/editors/CubeRestrictDraft.d.ts.map +1 -0
  141. package/lib/stores/editors/CubeRestrictDraft.js +73 -0
  142. package/lib/stores/editors/CubeRestrictDraft.js.map +1 -0
  143. package/lib/stores/editors/CubeRowCountDraft.d.ts +42 -0
  144. package/lib/stores/editors/CubeRowCountDraft.d.ts.map +1 -0
  145. package/lib/stores/editors/CubeRowCountDraft.js +55 -0
  146. package/lib/stores/editors/CubeRowCountDraft.js.map +1 -0
  147. package/lib/stores/editors/CubeSliceDraft.d.ts +42 -0
  148. package/lib/stores/editors/CubeSliceDraft.d.ts.map +1 -0
  149. package/lib/stores/editors/CubeSliceDraft.js +72 -0
  150. package/lib/stores/editors/CubeSliceDraft.js.map +1 -0
  151. package/lib/stores/editors/CubeSortDraft.d.ts +49 -0
  152. package/lib/stores/editors/CubeSortDraft.d.ts.map +1 -0
  153. package/lib/stores/editors/CubeSortDraft.js +99 -0
  154. package/lib/stores/editors/CubeSortDraft.js.map +1 -0
  155. package/package.json +8 -8
  156. package/src/__lib__/LegendCubeHelpText.ts +22 -2
  157. package/src/__lib__/LegendCubeLabels.ts +30 -0
  158. package/src/__test-utils__/CubeEngineTestSupport.ts +35 -0
  159. package/src/__test-utils__/CubeOperationsTestUtils.ts +177 -0
  160. package/src/__test-utils__/FakeCubeEngine.ts +7 -1
  161. package/src/components/CubeNodeIcon.tsx +18 -1
  162. package/src/components/canvas/CubeCanvas.tsx +5 -3
  163. package/src/components/canvas/CubeCanvasElements.ts +4 -1
  164. package/src/components/canvas/CubeCanvasNode.tsx +9 -1
  165. package/src/components/editors/CubeColumnPicker.tsx +36 -12
  166. package/src/components/editors/CubeDistinctEditor.tsx +28 -0
  167. package/src/components/editors/CubeIntegerField.tsx +51 -0
  168. package/src/components/editors/CubeJoinEditor.tsx +26 -3
  169. package/src/components/editors/CubeNodeEditorPanel.tsx +12 -1
  170. package/src/components/editors/CubeNodeEditorRegistry.ts +20 -0
  171. package/src/components/editors/CubeRenameEditor.tsx +143 -0
  172. package/src/components/editors/CubeRestrictEditor.tsx +103 -0
  173. package/src/components/editors/CubeRowCountEditor.tsx +53 -0
  174. package/src/components/editors/CubeSliceEditor.tsx +69 -0
  175. package/src/components/editors/CubeSortEditor.tsx +206 -0
  176. package/src/components/editors/CubeSourceEditor.tsx +0 -10
  177. package/src/components/editors/CubeValueEditor.tsx +2 -1
  178. package/src/components/grid/CubeGridRegion.tsx +1 -0
  179. package/src/components/grid/CubeResultGrid.tsx +59 -5
  180. package/src/graph-manager/CubeEngine.ts +14 -0
  181. package/src/graph-manager/CubeModelOutlineHelper.ts +26 -0
  182. package/src/graph-manager/protocol/pure/v1/V1_CubeModelOutlineBuilder.ts +89 -3
  183. package/src/graph-manager/protocol/pure/v1/__test-utils__/V1_CubeEngineTestUtils.ts +6 -3
  184. package/src/stores/CubeEditorState.ts +110 -5
  185. package/src/stores/CubeExecutionState.ts +36 -3
  186. package/src/stores/CubeGridQuickActions.ts +129 -0
  187. package/src/stores/CubeNodeEditorState.ts +70 -1
  188. package/src/stores/CubeShowPureState.ts +27 -5
  189. package/src/stores/editors/CubeIntegerText.ts +34 -0
  190. package/src/stores/editors/CubeJoinDraft.ts +43 -16
  191. package/src/stores/editors/CubeNodeDraftRegistry.ts +35 -1
  192. package/src/stores/editors/CubeRenameDraft.ts +110 -0
  193. package/src/stores/editors/CubeRestrictDraft.ts +83 -0
  194. package/src/stores/editors/CubeRowCountDraft.ts +69 -0
  195. package/src/stores/editors/CubeSliceDraft.ts +87 -0
  196. package/src/stores/editors/CubeSortDraft.ts +135 -0
  197. package/tsconfig.json +15 -0
@@ -135,3 +135,38 @@ export const CUBE_ENGINE_TEST__execute = async (
135
135
  text: async () => response.data,
136
136
  };
137
137
  };
138
+
139
+ /**
140
+ * Generates the execution plan of a query, never running it, and gives the
141
+ * SQL of its nodes, outermost first; a body given as text (with lossless
142
+ * numbers) is sent as it is
143
+ */
144
+ export const CUBE_ENGINE_TEST__generatePlanSql = async (
145
+ input: object | string,
146
+ ): Promise<string[]> => {
147
+ const { data } = await axios.post<unknown, AxiosResponse<string>>(
148
+ `${PURE_API}/execution/generatePlan`,
149
+ input,
150
+ {
151
+ headers: JSON_HEADERS,
152
+ responseType: 'text',
153
+ transformResponse: (text: string) => text,
154
+ },
155
+ );
156
+ const sql: string[] = [];
157
+ const visit = (node: unknown): void => {
158
+ if (Array.isArray(node)) {
159
+ node.forEach(visit);
160
+ } else if (node && typeof node === 'object') {
161
+ Object.entries(node).forEach(([key, value]) => {
162
+ if (key === 'sqlQuery' && typeof value === 'string') {
163
+ sql.push(value);
164
+ } else {
165
+ visit(value);
166
+ }
167
+ });
168
+ }
169
+ };
170
+ visit(parseLosslessJSON(data));
171
+ return sql;
172
+ };
@@ -0,0 +1,177 @@
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 { expect } from '@jest/globals';
18
+ import {
19
+ buildSchemasAndValidity,
20
+ Connection,
21
+ createNodeRegistry,
22
+ Query,
23
+ type QueryNode,
24
+ QueryEmitter,
25
+ RelationalTableSource,
26
+ Schema,
27
+ } from '@finos/legend-cube';
28
+ import type {
29
+ CubeEngine,
30
+ CubeResult,
31
+ CubeResultValue,
32
+ } from '../graph-manager/CubeEngine.js';
33
+ import {
34
+ CUBE_NORTHWIND_DATABASE,
35
+ CUBE_NORTHWIND_MODEL,
36
+ CUBE_NORTHWIND_RUNTIME,
37
+ } from '../stores/fixtures/CubeNorthwindModel.js';
38
+
39
+ // Helpers for engine tests of operations on the Cube Northwind fixture,
40
+ // through the engine port only (no V1 classes), so any CubeEngine works
41
+
42
+ /** A table of the Cube Northwind model, not resolved yet */
43
+ export const TEST__northwindTable = (
44
+ id: string,
45
+ table: string,
46
+ schema = 'NORTHWIND',
47
+ ): RelationalTableSource =>
48
+ new RelationalTableSource(id, {
49
+ database: CUBE_NORTHWIND_DATABASE,
50
+ schema,
51
+ table,
52
+ });
53
+
54
+ /** The sources, resolved through the engine as the editor resolves them */
55
+ export const TEST__resolveSources = async (
56
+ engine: CubeEngine,
57
+ sources: readonly RelationalTableSource[],
58
+ ): Promise<RelationalTableSource[]> => {
59
+ const typed = await engine.resolveSchemas(
60
+ CUBE_NORTHWIND_MODEL,
61
+ new Map(
62
+ sources.map((source) => [
63
+ source.id,
64
+ [source.database, source.schema, source.table] as const,
65
+ ]),
66
+ ),
67
+ );
68
+ return sources.map((source) => {
69
+ const schema = typed.get(source.id);
70
+ if (!(schema instanceof Schema)) {
71
+ throw schema ?? new Error(`No schema for ${source.id}`);
72
+ }
73
+ return source.withResolution({ kind: 'resolved', schema });
74
+ });
75
+ };
76
+
77
+ /** A chain of nodes, each feeding the next on its first port, captured at the last */
78
+ export const TEST__chainOf = (nodes: readonly QueryNode[]): Query =>
79
+ new Query(
80
+ nodes,
81
+ nodes
82
+ .slice(1)
83
+ .map(
84
+ (node, index) =>
85
+ new Connection(
86
+ (nodes[index] as QueryNode).id,
87
+ node.id,
88
+ node.ports[0] as string,
89
+ ),
90
+ ),
91
+ nodes.at(-1)?.id,
92
+ );
93
+
94
+ /** Every node of the query is valid */
95
+ export const TEST__expectValidQuery = (query: Query): void => {
96
+ const { validity } = buildSchemasAndValidity(
97
+ query,
98
+ createNodeRegistry().queryRules,
99
+ );
100
+ expect([...validity].filter(([, errors]) => errors.length)).toEqual([]);
101
+ };
102
+
103
+ /**
104
+ * Runs the query up to its capture node, with this row limit; with a database
105
+ * type, written as for that database (on the fixture's H2 all the same, so a
106
+ * workaround's rows can be checked)
107
+ */
108
+ export const TEST__runQuery = async (
109
+ engine: CubeEngine,
110
+ query: Query,
111
+ rowLimit: number,
112
+ databaseType?: string,
113
+ ): Promise<CubeResult> => {
114
+ TEST__expectValidQuery(query);
115
+ return engine.execute(
116
+ CUBE_NORTHWIND_MODEL,
117
+ new QueryEmitter(query).emitExecutionLambda({
118
+ rowLimit,
119
+ runtime: CUBE_NORTHWIND_RUNTIME,
120
+ databaseType,
121
+ }),
122
+ );
123
+ };
124
+
125
+ /** The values of one column of a result */
126
+ export const TEST__columnValues = (
127
+ result: CubeResult,
128
+ column: string,
129
+ ): CubeResultValue[] => {
130
+ const index = result.columns.indexOf(column);
131
+ expect(index).toBeGreaterThanOrEqual(0);
132
+ return result.rows.map((row) => row[index] ?? null);
133
+ };
134
+
135
+ /** The schema Cube infers for a node of the query, its capture node by default */
136
+ export const TEST__inferredSchema = (
137
+ query: Query,
138
+ nodeId = query.selected ?? '',
139
+ ): Schema => {
140
+ const schema = buildSchemasAndValidity(
141
+ query,
142
+ createNodeRegistry().queryRules,
143
+ ).schemas.get(nodeId);
144
+ expect(schema).toBeDefined();
145
+ return schema as Schema;
146
+ };
147
+
148
+ /**
149
+ * The engine types the node as Cube infers it: the same names, in the same
150
+ * order, of the same types and parameters. Cube's nullability may only be
151
+ * wider, since the engine misreports it after outer joins (PLAN §4.7).
152
+ */
153
+ export const TEST__expectEngineTyping = async (
154
+ engine: CubeEngine,
155
+ query: Query,
156
+ nodeId = query.selected ?? '',
157
+ ): Promise<void> => {
158
+ const cube = TEST__inferredSchema(query, nodeId);
159
+ const typed = (
160
+ await engine.typeLambdas(
161
+ CUBE_NORTHWIND_MODEL,
162
+ new Map([[nodeId, new QueryEmitter(query).emitTypingLambda(nodeId)]]),
163
+ )
164
+ ).get(nodeId);
165
+ expect(typed).toBeInstanceOf(Schema);
166
+ const engineSchema = typed as Schema;
167
+ expect(
168
+ cube.columns.map((column) => [column.name, column.type.fullName]),
169
+ ).toEqual(
170
+ engineSchema.columns.map((column) => [column.name, column.type.fullName]),
171
+ );
172
+ cube.columns.forEach((column, index) => {
173
+ if (engineSchema.columns[index]?.nullable) {
174
+ expect([column.name, column.nullable]).toEqual([column.name, true]);
175
+ }
176
+ });
177
+ };
@@ -62,7 +62,13 @@ export const FAKE_NORTHWIND_OUTLINE: CubeModelOutline = {
62
62
  ],
63
63
  },
64
64
  ],
65
- runtimes: [{ path: NORTHWIND_RUNTIME, storePaths: [NORTHWIND_DATABASE] }],
65
+ runtimes: [
66
+ {
67
+ path: NORTHWIND_RUNTIME,
68
+ storePaths: [NORTHWIND_DATABASE],
69
+ connections: [{ storePath: NORTHWIND_DATABASE, databaseType: 'H2' }],
70
+ },
71
+ ],
66
72
  };
67
73
 
68
74
  /** The schemas the fake gives, keyed by `<schema>.<table>` as the outline names them */
@@ -15,10 +15,17 @@
15
15
  */
16
16
 
17
17
  import {
18
+ AlignBottomIcon,
19
+ AlignMiddleIcon,
20
+ AlignTopIcon,
18
21
  ArrowsJoinIcon,
22
+ CompressIcon,
23
+ DataCubeIcon,
19
24
  clsx,
20
25
  FilterIcon,
26
+ PencilIcon,
21
27
  QuestionSquareIcon,
28
+ SortIcon,
22
29
  TableIcon,
23
30
  } from '@finos/legend-art';
24
31
 
@@ -29,6 +36,13 @@ const NODE_ICONS: Readonly<
29
36
  table: TableIcon,
30
37
  filter: FilterIcon,
31
38
  join: ArrowsJoinIcon,
39
+ limit: AlignTopIcon,
40
+ drop: AlignBottomIcon,
41
+ slice: AlignMiddleIcon,
42
+ distinct: CompressIcon,
43
+ restrict: DataCubeIcon.TableColumns,
44
+ rename: PencilIcon,
45
+ sort: SortIcon,
32
46
  };
33
47
 
34
48
  /** Whether a node definition's icon name maps to an icon, rather than the question mark */
@@ -41,7 +55,10 @@ export const CubeNodeIcon: React.FC<{
41
55
  className?: string;
42
56
  }> = (props) => {
43
57
  const { icon, className } = props;
58
+ // an own key only: a name such as `constructor` must not reach Object's
44
59
  const Icon =
45
- (icon === undefined ? undefined : NODE_ICONS[icon]) ?? QuestionSquareIcon;
60
+ icon !== undefined && hasCubeNodeIcon(icon)
61
+ ? (NODE_ICONS[icon] ?? QuestionSquareIcon)
62
+ : QuestionSquareIcon;
46
63
  return <Icon className={clsx(className)} />;
47
64
  };
@@ -93,14 +93,16 @@ const CubeCanvasFlow = observer((props: { editorState: CubeEditorState }) => {
93
93
  );
94
94
  const edges = useMemo(() => buildCubeCanvasEdges(query), [query]);
95
95
  // the view fits the graph again whenever the layout moves a node, or the
96
- // canvas gets wider or narrower (e.g. the node editor opens beside it),
97
- // once React Flow has measured every node: it fits only measured ones
96
+ // canvas changes size (e.g. the node editor opens beside it, or the
97
+ // splitter above the grid moves), once React Flow has measured every node:
98
+ // it fits only measured ones
98
99
  const layoutSignature = useMemo(
99
100
  () => JSON.stringify([...positions]),
100
101
  [positions],
101
102
  );
102
103
  const nodesInitialized = useNodesInitialized();
103
104
  const canvasWidth = useStore((state) => state.width);
105
+ const canvasHeight = useStore((state) => state.height);
104
106
  useEffect(() => {
105
107
  if (!nodesInitialized) {
106
108
  return undefined;
@@ -109,7 +111,7 @@ const CubeCanvasFlow = observer((props: { editorState: CubeEditorState }) => {
109
111
  fitView(FIT_VIEW_OPTIONS).catch(noop());
110
112
  }, 0);
111
113
  return () => window.clearTimeout(timer);
112
- }, [layoutSignature, canvasWidth, nodesInitialized, fitView]);
114
+ }, [layoutSignature, canvasWidth, canvasHeight, nodesInitialized, fitView]);
113
115
 
114
116
  return (
115
117
  <ReactFlow<CubeCanvasFlowNode, CubeCanvasFlowEdge>
@@ -117,6 +117,8 @@ export interface CubeCanvasNodeStatus {
117
117
  readonly isResolving: boolean;
118
118
  /** The last run failed on it */
119
119
  readonly hasEngineError: boolean;
120
+ /** It has a warning, stored (a table that changed) or derived (a lost sort order) */
121
+ readonly hasWarnings: boolean;
120
122
  /** Its errors, then its warnings, its description and its id, one a line */
121
123
  readonly tooltip: string;
122
124
  }
@@ -128,7 +130,7 @@ export const getCubeCanvasNodeStatus = (
128
130
  const ownErrors = editorState.analysis.validity.get(node.id);
129
131
  // the tooltip adds the engine's error; the states keep it apart
130
132
  const errors = editorState.getNodeErrors(node.id);
131
- const warnings = editorState.warnings.get(node.key) ?? [];
133
+ const warnings = editorState.getNodeWarnings(node);
132
134
  return {
133
135
  isCapture: editorState.document.query.selected === node.id,
134
136
  isInvalid: (ownErrors ?? []).some(
@@ -139,6 +141,7 @@ export const getCubeCanvasNodeStatus = (
139
141
  ),
140
142
  isResolving: editorState.isPendingSource(node),
141
143
  hasEngineError: editorState.hostIssues.has(node.id),
144
+ hasWarnings: warnings.length > 0,
142
145
  tooltip: [
143
146
  ...(ownErrors ? [] : [MISSING_VALIDITY_TOOLTIP]),
144
147
  ...errors,
@@ -14,7 +14,7 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import { clsx, ContextMenu } from '@finos/legend-art';
17
+ import { clsx, ContextMenu, WarningIcon } from '@finos/legend-art';
18
18
  import { Handle, type NodeProps, Position } from '@xyflow/react';
19
19
  import { observer } from 'mobx-react-lite';
20
20
  import { useRef } from 'react';
@@ -113,6 +113,7 @@ export const CubeCanvasNode = observer(
113
113
  'legend-cube__node--resolving animate-pulse': status.isResolving,
114
114
  'legend-cube__node--engine-error bg-[var(--color-status-error-bg)]':
115
115
  status.hasEngineError,
116
+ 'legend-cube__node--warning': status.hasWarnings,
116
117
  'legend-cube__node--drop-target outline-dashed outline-2 outline-[var(--color-accent)]':
117
118
  isDropTarget,
118
119
  },
@@ -128,6 +129,13 @@ export const CubeCanvasNode = observer(
128
129
  <span className="line-clamp-2 min-w-0 break-words leading-tight">
129
130
  {node.describe()}
130
131
  </span>
132
+ {status.hasWarnings && (
133
+ // the warning's text is in the node's tooltip
134
+ <WarningIcon
135
+ className="ml-auto shrink-0 text-[var(--color-status-warn)]"
136
+ aria-hidden={true}
137
+ />
138
+ )}
131
139
  </div>
132
140
  <Handle
133
141
  id={CUBE_OUTPUT_HANDLE_ID}
@@ -23,7 +23,12 @@ import {
23
23
  StringTypeIcon,
24
24
  ToggleIcon,
25
25
  } from '@finos/legend-art';
26
- import { type CubeType, type Schema, TypeFamily } from '@finos/legend-cube';
26
+ import {
27
+ type CubeType,
28
+ type Schema,
29
+ type SchemaColumn,
30
+ TypeFamily,
31
+ } from '@finos/legend-cube';
27
32
  import { getColumnTypeLabel } from '../../__lib__/LegendCubeLabels.js';
28
33
 
29
34
  const FAMILY_ICONS: Readonly<
@@ -55,7 +60,9 @@ export const CubeColumnTypeIcon: React.FC<{
55
60
  * columns of the actual input). Each option names its type, e.g.
56
61
  * `CUSTOMER_ID: Varchar(5)?`; the picked column's type shows beside, with
57
62
  * its family's icon and its full type in the tooltip. A column the input
58
- * doesn't have stays shown, so the problem can be seen and fixed.
63
+ * doesn't have stays shown, so the problem can be seen and fixed. A column
64
+ * the editor can't take here is shown, with the reason `isColumnDisabled`
65
+ * gives after its type, but can't be picked.
59
66
  */
60
67
  export const CubeColumnPicker: React.FC<{
61
68
  /** What the column is for, e.g. `Left join column 1`: the picker's accessible name */
@@ -67,8 +74,21 @@ export const CubeColumnPicker: React.FC<{
67
74
  disabled?: boolean | undefined;
68
75
  /** Marks the pick as wrong, e.g. a type that doesn't match */
69
76
  invalid?: boolean | undefined;
77
+ /**
78
+ * Why a column can't be picked, shown after its type, e.g. a Sort's Variant
79
+ * column, or one another row has; `undefined` when it can be
80
+ */
81
+ isColumnDisabled?: ((column: SchemaColumn) => string | undefined) | undefined;
70
82
  }> = (props) => {
71
- const { label, schema, value, onChange, disabled, invalid } = props;
83
+ const {
84
+ label,
85
+ schema,
86
+ value,
87
+ onChange,
88
+ disabled,
89
+ invalid,
90
+ isColumnDisabled,
91
+ } = props;
72
92
  const picked = value ? schema.lookup(value) : undefined;
73
93
  return (
74
94
  <div className="flex min-w-0 items-center gap-1">
@@ -90,15 +110,19 @@ export const CubeColumnPicker: React.FC<{
90
110
  {value ? `${value} (not in the input)` : 'Pick a column'}
91
111
  </option>
92
112
  )}
93
- {schema.columns.map((column) => (
94
- <option
95
- key={column.name}
96
- value={column.name}
97
- title={column.type.fullName}
98
- >
99
- {`${column.name}: ${getColumnTypeLabel(column)}`}
100
- </option>
101
- ))}
113
+ {schema.columns.map((column) => {
114
+ const reason = isColumnDisabled?.(column);
115
+ return (
116
+ <option
117
+ key={column.name}
118
+ value={column.name}
119
+ title={column.type.fullName}
120
+ disabled={reason !== undefined}
121
+ >
122
+ {`${column.name}: ${getColumnTypeLabel(column)}${reason === undefined ? '' : ` (${reason})`}`}
123
+ </option>
124
+ );
125
+ })}
102
126
  </select>
103
127
  {picked && (
104
128
  <span
@@ -0,0 +1,28 @@
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 { observer } from 'mobx-react-lite';
18
+ import { DISTINCT_EDITOR_TEXT } from '../../__lib__/LegendCubeLabels.js';
19
+
20
+ /**
21
+ * The Distinct editor (spec §17.6: "nothing — description only"). Distinct
22
+ * has no settings, so it has no draft, and the panel shows no Apply or Cancel.
23
+ */
24
+ export const CubeDistinctEditor = observer(() => (
25
+ <div className="text-base text-[var(--color-text-secondary)]">
26
+ {DISTINCT_EDITOR_TEXT}
27
+ </div>
28
+ ));
@@ -0,0 +1,51 @@
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 { clsx } from '@finos/legend-art';
18
+
19
+ /**
20
+ * A field for a whole number, such as a size or a row index. It shows the
21
+ * text as typed and reports every keystroke, so the panel checks the edited
22
+ * node as the user types. Enter and Escape do nothing special: Apply is the
23
+ * panel's, and Ctrl+Z is the field's own undo.
24
+ */
25
+ export const CubeIntegerField: React.FC<{
26
+ label: string;
27
+ text: string;
28
+ invalid: boolean;
29
+ disabled: boolean;
30
+ onChange: (text: string) => void;
31
+ }> = (props) => {
32
+ const { label, text, invalid, disabled, onChange } = props;
33
+ return (
34
+ <input
35
+ aria-label={label}
36
+ aria-invalid={invalid}
37
+ className={clsx(
38
+ 'h-6 w-24 rounded-sm border bg-[var(--color-bg-input)] px-1 text-base',
39
+ invalid
40
+ ? 'border-[var(--color-status-error)] text-[var(--color-status-error)]'
41
+ : 'border-[var(--color-border-default)]',
42
+ )}
43
+ type="text"
44
+ inputMode="numeric"
45
+ spellCheck={false}
46
+ value={text}
47
+ disabled={disabled}
48
+ onChange={(event) => onChange(event.target.value)}
49
+ />
50
+ );
51
+ };
@@ -21,13 +21,17 @@ import {
21
21
  isJoinType,
22
22
  JOIN_TYPE_LABELS,
23
23
  JOIN_TYPES,
24
+ planJoinDuplicateFix,
24
25
  type Schema,
25
26
  } from '@finos/legend-cube';
26
27
  import { guaranteeType, noop } from '@finos/legend-shared';
27
28
  import { flowResult } from 'mobx';
28
29
  import { observer } from 'mobx-react-lite';
29
30
  import { useEffect } from 'react';
30
- import { CUBE_TABLE_FLAG_LABELS } from '../../__lib__/LegendCubeLabels.js';
31
+ import {
32
+ CUBE_TABLE_FLAG_LABELS,
33
+ READ_ONLY_CUBE_TITLE,
34
+ } from '../../__lib__/LegendCubeLabels.js';
31
35
  import { CubeTableFlag } from '../../graph-manager/CubeEngine.js';
32
36
  import {
33
37
  type CubeJoinKeyPair,
@@ -137,6 +141,10 @@ export const CubeJoinEditor = observer((props: CubeNodeEditorProps) => {
137
141
  edited.leftColumns,
138
142
  edited.rightColumns,
139
143
  );
144
+ // the names the autofix would give, to show beside each shared column
145
+ const fix = duplicates.length
146
+ ? planJoinDuplicateFix(edited, leftSchema, rightSchema)
147
+ : undefined;
140
148
  const canAddPair =
141
149
  !readOnly &&
142
150
  draft.pairs.length <
@@ -211,12 +219,27 @@ export const CubeJoinEditor = observer((props: CubeNodeEditorProps) => {
211
219
  both:
212
220
  </div>
213
221
  <ul aria-label="Columns in both inputs">
214
- {duplicates.map((name) => (
222
+ {duplicates.map((name, index) => (
215
223
  <li key={name} className="font-mono">
216
- {name}
224
+ {fix
225
+ ? `${name} → ${fix.left[index]?.to} (Left), ${fix.right[index]?.to} (Right)`
226
+ : name}
217
227
  </li>
218
228
  ))}
219
229
  </ul>
230
+ <div className="mt-1">
231
+ <CubeButton
232
+ title={
233
+ readOnly
234
+ ? READ_ONLY_CUBE_TITLE
235
+ : 'Add a Rename before each input that gives these columns new names. Applies your changes too, as one step to undo.'
236
+ }
237
+ disabled={!editorState.nodeEditor.canRenameDuplicateColumns}
238
+ onClick={() => editorState.nodeEditor.renameDuplicateColumns()}
239
+ >
240
+ Rename them
241
+ </CubeButton>
242
+ </div>
220
243
  </div>
221
244
  )}
222
245
  </div>
@@ -14,7 +14,7 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import { QuestionCircleIcon, TimesIcon } from '@finos/legend-art';
17
+ import { QuestionCircleIcon, TimesIcon, WarningIcon } from '@finos/legend-art';
18
18
  import {
19
19
  isIncompleteError,
20
20
  isSchemasError,
@@ -80,6 +80,7 @@ export const CubeNodeEditorPanel = observer(
80
80
  isEditable && !inputProblems.length
81
81
  ? validateEdited(edited, inputSchemas)
82
82
  : [];
83
+ const warnings = editorState.getNodeWarnings(node);
83
84
  return (
84
85
  <div
85
86
  className="flex h-full flex-col border-l border-[var(--color-border-default)] bg-[var(--color-bg-panel)]"
@@ -143,6 +144,16 @@ export const CubeNodeEditorPanel = observer(
143
144
  </button>
144
145
  </div>
145
146
  <div className="min-h-0 flex-1 overflow-auto p-2">
147
+ {warnings.map((warning) => (
148
+ <div
149
+ key={warning}
150
+ className="mb-2 flex gap-1 text-sm text-[var(--color-status-warn)]"
151
+ role="status"
152
+ >
153
+ <WarningIcon className="mt-0.5 shrink-0" aria-hidden={true} />
154
+ <span>{warning}</span>
155
+ </div>
156
+ ))}
146
157
  {inputProblems.length ? (
147
158
  <div
148
159
  className="text-base text-[var(--color-status-warn)]"
@@ -15,15 +15,28 @@
15
15
  */
16
16
 
17
17
  import {
18
+ Distinct,
19
+ Drop,
18
20
  Filter,
19
21
  Join,
22
+ Limit,
20
23
  RelationalTableSource,
24
+ Rename,
25
+ Restrict,
21
26
  type Schema,
27
+ Slice,
28
+ Sort,
22
29
  } from '@finos/legend-cube';
23
30
  import type { CubeEditorState } from '../../stores/CubeEditorState.js';
24
31
  import type { CubeNodeDraft } from '../../stores/editors/CubeNodeDraft.js';
32
+ import { CubeDistinctEditor } from './CubeDistinctEditor.js';
25
33
  import { CubeFilterEditor } from './CubeFilterEditor.js';
26
34
  import { CubeJoinEditor } from './CubeJoinEditor.js';
35
+ import { CubeRenameEditor } from './CubeRenameEditor.js';
36
+ import { CubeRestrictEditor } from './CubeRestrictEditor.js';
37
+ import { CubeRowCountEditor } from './CubeRowCountEditor.js';
38
+ import { CubeSliceEditor } from './CubeSliceEditor.js';
39
+ import { CubeSortEditor } from './CubeSortEditor.js';
27
40
  import { CubeSourceEditor } from './CubeSourceEditor.js';
28
41
 
29
42
  /**
@@ -56,6 +69,13 @@ export const CUBE_NODE_EDITORS: ReadonlyMap<
56
69
  React.FC<CubeNodeEditorProps>
57
70
  > = new Map<string, React.FC<CubeNodeEditorProps>>([
58
71
  [RelationalTableSource.TYPE, CubeSourceEditor],
72
+ [Sort.TYPE, CubeSortEditor],
59
73
  [Filter.TYPE, CubeFilterEditor],
74
+ [Restrict.TYPE, CubeRestrictEditor],
75
+ [Rename.TYPE, CubeRenameEditor],
76
+ [Distinct.TYPE, CubeDistinctEditor],
60
77
  [Join.TYPE, CubeJoinEditor],
78
+ [Drop.TYPE, CubeRowCountEditor],
79
+ [Limit.TYPE, CubeRowCountEditor],
80
+ [Slice.TYPE, CubeSliceEditor],
61
81
  ]);