@finos/legend-application-studio 28.21.51 → 28.21.52
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.
- package/lib/components/editor/Editor.d.ts.map +1 -1
- package/lib/components/editor/Editor.js +2 -1
- package/lib/components/editor/Editor.js.map +1 -1
- package/lib/components/editor/editor-group/ingest-editor/IngestDefinitionEditor.d.ts.map +1 -1
- package/lib/components/editor/editor-group/ingest-editor/IngestDefinitionEditor.js +17 -2
- package/lib/components/editor/editor-group/ingest-editor/IngestDefinitionEditor.js.map +1 -1
- package/lib/components/editor/embedded-cube/EmbeddedCube.d.ts +23 -0
- package/lib/components/editor/embedded-cube/EmbeddedCube.d.ts.map +1 -0
- package/lib/components/editor/embedded-cube/EmbeddedCube.js +38 -0
- package/lib/components/editor/embedded-cube/EmbeddedCube.js.map +1 -0
- package/lib/components/editor/embedded-cube/EmbeddedCubeEditor.d.ts +35 -0
- package/lib/components/editor/embedded-cube/EmbeddedCubeEditor.d.ts.map +1 -0
- package/lib/components/editor/embedded-cube/EmbeddedCubeEditor.js +177 -0
- package/lib/components/editor/embedded-cube/EmbeddedCubeEditor.js.map +1 -0
- package/lib/components/extensions/Core_LegendStudioApplicationPlugin.d.ts.map +1 -1
- package/lib/components/extensions/Core_LegendStudioApplicationPlugin.js +4 -0
- package/lib/components/extensions/Core_LegendStudioApplicationPlugin.js.map +1 -1
- package/lib/index.css +1 -1
- package/lib/package.json +3 -1
- package/lib/stores/editor/EditorStore.d.ts +2 -0
- package/lib/stores/editor/EditorStore.d.ts.map +1 -1
- package/lib/stores/editor/EditorStore.js +6 -1
- package/lib/stores/editor/EditorStore.js.map +1 -1
- package/lib/stores/editor/embedded-cube/EmbeddedCubeState.d.ts +81 -0
- package/lib/stores/editor/embedded-cube/EmbeddedCubeState.d.ts.map +1 -0
- package/lib/stores/editor/embedded-cube/EmbeddedCubeState.js +186 -0
- package/lib/stores/editor/embedded-cube/EmbeddedCubeState.js.map +1 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeDataProductCatalog.d.ts +51 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeDataProductCatalog.d.ts.map +1 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeDataProductCatalog.js +148 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeDataProductCatalog.js.map +1 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeHost.d.ts +39 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeHost.d.ts.map +1 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeHost.js +60 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeHost.js.map +1 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeIngestCatalog.d.ts +57 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeIngestCatalog.d.ts.map +1 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeIngestCatalog.js +148 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeIngestCatalog.js.map +1 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeWorkspace.d.ts +58 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeWorkspace.d.ts.map +1 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeWorkspace.js +94 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeWorkspace.js.map +1 -0
- package/lib/stores/graph-modifier/DSL_Ingest_GraphModifierHelper.d.ts +57 -0
- package/lib/stores/graph-modifier/DSL_Ingest_GraphModifierHelper.d.ts.map +1 -0
- package/lib/stores/graph-modifier/DSL_Ingest_GraphModifierHelper.js +151 -0
- package/lib/stores/graph-modifier/DSL_Ingest_GraphModifierHelper.js.map +1 -0
- package/package.json +13 -11
- package/src/components/editor/Editor.tsx +2 -0
- package/src/components/editor/editor-group/ingest-editor/IngestDefinitionEditor.tsx +65 -0
- package/src/components/editor/embedded-cube/EmbeddedCube.tsx +57 -0
- package/src/components/editor/embedded-cube/EmbeddedCubeEditor.tsx +448 -0
- package/src/components/extensions/Core_LegendStudioApplicationPlugin.tsx +4 -0
- package/src/stores/editor/EditorStore.ts +6 -1
- package/src/stores/editor/embedded-cube/EmbeddedCubeState.ts +251 -0
- package/src/stores/editor/embedded-cube/LegendStudioCubeDataProductCatalog.ts +242 -0
- package/src/stores/editor/embedded-cube/LegendStudioCubeHost.ts +82 -0
- package/src/stores/editor/embedded-cube/LegendStudioCubeIngestCatalog.ts +235 -0
- package/src/stores/editor/embedded-cube/LegendStudioCubeWorkspace.ts +151 -0
- package/src/stores/graph-modifier/DSL_Ingest_GraphModifierHelper.ts +214 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,448 @@
|
|
|
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
|
+
Dialog,
|
|
19
|
+
Modal,
|
|
20
|
+
ModalBody,
|
|
21
|
+
ModalFooter,
|
|
22
|
+
ModalFooterButton,
|
|
23
|
+
ModalHeader,
|
|
24
|
+
TimesIcon,
|
|
25
|
+
} from '@finos/legend-art';
|
|
26
|
+
import {
|
|
27
|
+
buildCubeRunningStepLambda,
|
|
28
|
+
CubeButton,
|
|
29
|
+
CubeEditor,
|
|
30
|
+
formatDisabledReasons,
|
|
31
|
+
type CubeEditorState,
|
|
32
|
+
} from '@finos/legend-cube-builder';
|
|
33
|
+
import type { IngestDefinition } from '@finos/legend-graph';
|
|
34
|
+
import { noop, type PlainObject } from '@finos/legend-shared';
|
|
35
|
+
import { observer } from 'mobx-react-lite';
|
|
36
|
+
import { useState } from 'react';
|
|
37
|
+
import type { EmbeddedCubeState } from '../../../stores/editor/embedded-cube/EmbeddedCubeState.js';
|
|
38
|
+
import { LegendStudioCubeHost } from '../../../stores/editor/embedded-cube/LegendStudioCubeHost.js';
|
|
39
|
+
|
|
40
|
+
export const EMBEDDED_CUBE_TEST_ID = {
|
|
41
|
+
DIALOG: 'embedded-cube__dialog',
|
|
42
|
+
ADD_MATERIALIZED_VIEW: 'embedded-cube__add-materialized-view',
|
|
43
|
+
SAVE_MATERIALIZED_VIEW: 'embedded-cube__save-materialized-view',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The classification of the view's data: one of the classifications a data
|
|
48
|
+
* product's access points offer, or typed when Studio's config gives none
|
|
49
|
+
*/
|
|
50
|
+
const MaterializedViewClassification = observer(
|
|
51
|
+
(props: {
|
|
52
|
+
embeddedCubeState: EmbeddedCubeState;
|
|
53
|
+
sensitivity: string;
|
|
54
|
+
setSensitivity: (sensitivity: string) => void;
|
|
55
|
+
}) => {
|
|
56
|
+
const { embeddedCubeState, sensitivity, setSensitivity } = props;
|
|
57
|
+
const { classifications } = embeddedCubeState;
|
|
58
|
+
return (
|
|
59
|
+
<label className="flex flex-col gap-1 text-base">
|
|
60
|
+
Data classification
|
|
61
|
+
{classifications.length ? (
|
|
62
|
+
<select
|
|
63
|
+
className="input--dark h-8 rounded-sm px-2"
|
|
64
|
+
aria-label="Data classification"
|
|
65
|
+
value={sensitivity}
|
|
66
|
+
onChange={(event) => setSensitivity(event.target.value)}
|
|
67
|
+
>
|
|
68
|
+
<option value="" disabled={true}>
|
|
69
|
+
Pick a classification
|
|
70
|
+
</option>
|
|
71
|
+
{classifications.map((classification) => (
|
|
72
|
+
<option key={classification} value={classification}>
|
|
73
|
+
{classification}
|
|
74
|
+
</option>
|
|
75
|
+
))}
|
|
76
|
+
</select>
|
|
77
|
+
) : (
|
|
78
|
+
<input
|
|
79
|
+
className="input--dark h-8 rounded-sm px-2"
|
|
80
|
+
aria-label="Data classification"
|
|
81
|
+
placeholder="Classification"
|
|
82
|
+
spellCheck={false}
|
|
83
|
+
value={sensitivity}
|
|
84
|
+
onChange={(event) => setSensitivity(event.target.value)}
|
|
85
|
+
/>
|
|
86
|
+
)}
|
|
87
|
+
</label>
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The view's primary key: none or some of the columns of the step that
|
|
94
|
+
* runs, kept in the order of the columns
|
|
95
|
+
*/
|
|
96
|
+
const MaterializedViewPrimaryKey: React.FC<{
|
|
97
|
+
columns: readonly string[];
|
|
98
|
+
primaryKey: readonly string[];
|
|
99
|
+
setPrimaryKey: (primaryKey: string[]) => void;
|
|
100
|
+
}> = ({ columns, primaryKey, setPrimaryKey }) => (
|
|
101
|
+
<fieldset className="flex min-w-0 flex-col gap-1 text-base">
|
|
102
|
+
<legend>Primary key</legend>
|
|
103
|
+
<span className="text-sm text-[var(--color-text-muted)]">
|
|
104
|
+
The columns that identify a row, if any
|
|
105
|
+
</span>
|
|
106
|
+
<ul className="max-h-40 overflow-auto" aria-label="Primary key columns">
|
|
107
|
+
{columns.map((column) => (
|
|
108
|
+
<li key={column}>
|
|
109
|
+
<label className="flex items-center gap-2">
|
|
110
|
+
<input
|
|
111
|
+
type="checkbox"
|
|
112
|
+
checked={primaryKey.includes(column)}
|
|
113
|
+
onChange={() =>
|
|
114
|
+
setPrimaryKey(
|
|
115
|
+
primaryKey.includes(column)
|
|
116
|
+
? primaryKey.filter((each) => each !== column)
|
|
117
|
+
: columns.filter(
|
|
118
|
+
(each) => each === column || primaryKey.includes(each),
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
/>
|
|
123
|
+
<span className="min-w-0 truncate">{column}</span>
|
|
124
|
+
</label>
|
|
125
|
+
</li>
|
|
126
|
+
))}
|
|
127
|
+
</ul>
|
|
128
|
+
</fieldset>
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
/** What a header button gives its dialog */
|
|
132
|
+
interface MaterializedViewDialogInput {
|
|
133
|
+
/** The step that runs, as a lambda with no runtime and no row limit */
|
|
134
|
+
readonly lambda: PlainObject;
|
|
135
|
+
/** The columns of the step that runs, the view's; none when Cube doesn't know them */
|
|
136
|
+
readonly columns: readonly string[] | undefined;
|
|
137
|
+
/** Why Save MatView asks again; none to add a new view */
|
|
138
|
+
readonly saveProblems?: readonly string[] | undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Names the materialized view, classifies its data and picks its primary
|
|
143
|
+
* key, then adds it to the ingest definition (Add MatView). For the view
|
|
144
|
+
* opened to edit, asks again for what it can't be saved with as it was, the
|
|
145
|
+
* classification of its data or a primary key whose column is gone, then
|
|
146
|
+
* saves it in place (Save MatView)
|
|
147
|
+
*/
|
|
148
|
+
const MaterializedViewDialog = observer(
|
|
149
|
+
(props: {
|
|
150
|
+
embeddedCubeState: EmbeddedCubeState;
|
|
151
|
+
input: MaterializedViewDialogInput;
|
|
152
|
+
close: () => void;
|
|
153
|
+
}) => {
|
|
154
|
+
const { embeddedCubeState, input, close } = props;
|
|
155
|
+
const { lambda, saveProblems } = input;
|
|
156
|
+
const saved = saveProblems ? embeddedCubeState.matViewDataSet : undefined;
|
|
157
|
+
// a view saved when Cube doesn't know the step's columns keeps its key
|
|
158
|
+
const columns = input.columns ?? saved?.primaryKey ?? [];
|
|
159
|
+
// as Cube's dialogs: the dialog follows the theme, and its primary button
|
|
160
|
+
// is an accent button in every theme (legend-art's light one is grey)
|
|
161
|
+
const darkMode =
|
|
162
|
+
!embeddedCubeState.editorStore.applicationStore.layoutService
|
|
163
|
+
.TEMPORARY__isLightColorThemeEnabled;
|
|
164
|
+
const [name, setName] = useState('');
|
|
165
|
+
// a view saved again starts on its own classification, if it can keep it,
|
|
166
|
+
// and on the columns of its key the step still has
|
|
167
|
+
const [sensitivity, setSensitivity] = useState(() =>
|
|
168
|
+
saved?.sensitivity !== undefined &&
|
|
169
|
+
embeddedCubeState.getMaterializedViewClassificationProblem(
|
|
170
|
+
saved.sensitivity,
|
|
171
|
+
) === undefined
|
|
172
|
+
? saved.sensitivity
|
|
173
|
+
: '',
|
|
174
|
+
);
|
|
175
|
+
const [primaryKey, setPrimaryKey] = useState(() =>
|
|
176
|
+
columns.filter((column) => saved?.primaryKey.includes(column)),
|
|
177
|
+
);
|
|
178
|
+
const nameProblem = saveProblems
|
|
179
|
+
? undefined
|
|
180
|
+
: embeddedCubeState.getMaterializedViewNameProblem(name);
|
|
181
|
+
const problem =
|
|
182
|
+
nameProblem ??
|
|
183
|
+
embeddedCubeState.getMaterializedViewClassificationProblem(sensitivity);
|
|
184
|
+
const verb = saveProblems ? 'Save' : 'Add';
|
|
185
|
+
const confirm = (): void => {
|
|
186
|
+
if (problem !== undefined) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (saveProblems) {
|
|
190
|
+
embeddedCubeState.saveMaterializedView(lambda, {
|
|
191
|
+
primaryKey,
|
|
192
|
+
sensitivity,
|
|
193
|
+
});
|
|
194
|
+
} else {
|
|
195
|
+
embeddedCubeState.addMaterializedView(name, lambda, {
|
|
196
|
+
primaryKey,
|
|
197
|
+
sensitivity,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
return (
|
|
202
|
+
<Dialog open={true} onClose={close}>
|
|
203
|
+
<Modal darkMode={darkMode} className="w-[440px] max-w-full">
|
|
204
|
+
<ModalHeader>
|
|
205
|
+
<div className="modal__title">{`${verb} MatView`}</div>
|
|
206
|
+
</ModalHeader>
|
|
207
|
+
<ModalBody>
|
|
208
|
+
<form
|
|
209
|
+
className="flex flex-col gap-2"
|
|
210
|
+
data-testid={
|
|
211
|
+
saveProblems
|
|
212
|
+
? EMBEDDED_CUBE_TEST_ID.SAVE_MATERIALIZED_VIEW
|
|
213
|
+
: EMBEDDED_CUBE_TEST_ID.ADD_MATERIALIZED_VIEW
|
|
214
|
+
}
|
|
215
|
+
onSubmit={(event) => {
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
confirm();
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
220
|
+
{saveProblems ? (
|
|
221
|
+
<>
|
|
222
|
+
<div className="text-base">
|
|
223
|
+
{`${embeddedCubeState.matView} of ${embeddedCubeState.ingest?.name ?? 'the ingest definition'} can't be saved as it was:`}
|
|
224
|
+
</div>
|
|
225
|
+
<ul
|
|
226
|
+
aria-label="Why Save MatView asks"
|
|
227
|
+
className="list-disc pl-5 text-sm text-[var(--color-status-warn)]"
|
|
228
|
+
>
|
|
229
|
+
{saveProblems.map((each) => (
|
|
230
|
+
<li key={each}>{each}</li>
|
|
231
|
+
))}
|
|
232
|
+
</ul>
|
|
233
|
+
</>
|
|
234
|
+
) : (
|
|
235
|
+
<>
|
|
236
|
+
<label className="flex flex-col gap-1 text-base">
|
|
237
|
+
{`A data set of ${embeddedCubeState.ingest?.name ?? 'the ingest definition'}, whose rows are the cube's`}
|
|
238
|
+
<input
|
|
239
|
+
className="input--dark h-8 rounded-sm px-2"
|
|
240
|
+
aria-label="Materialized view name"
|
|
241
|
+
placeholder="Name"
|
|
242
|
+
spellCheck={false}
|
|
243
|
+
autoFocus={true}
|
|
244
|
+
value={name}
|
|
245
|
+
onChange={(event) => setName(event.target.value)}
|
|
246
|
+
/>
|
|
247
|
+
</label>
|
|
248
|
+
{name.trim() && nameProblem && (
|
|
249
|
+
<div className="text-sm text-[var(--color-status-error)]">
|
|
250
|
+
{nameProblem}
|
|
251
|
+
</div>
|
|
252
|
+
)}
|
|
253
|
+
</>
|
|
254
|
+
)}
|
|
255
|
+
<MaterializedViewClassification
|
|
256
|
+
embeddedCubeState={embeddedCubeState}
|
|
257
|
+
sensitivity={sensitivity}
|
|
258
|
+
setSensitivity={setSensitivity}
|
|
259
|
+
/>
|
|
260
|
+
<MaterializedViewPrimaryKey
|
|
261
|
+
columns={columns}
|
|
262
|
+
primaryKey={primaryKey}
|
|
263
|
+
setPrimaryKey={setPrimaryKey}
|
|
264
|
+
/>
|
|
265
|
+
</form>
|
|
266
|
+
</ModalBody>
|
|
267
|
+
<ModalFooter>
|
|
268
|
+
<ModalFooterButton
|
|
269
|
+
darkMode={true}
|
|
270
|
+
formatText={false}
|
|
271
|
+
text={verb}
|
|
272
|
+
title={problem ?? `${verb} the materialized view`}
|
|
273
|
+
disabled={problem !== undefined}
|
|
274
|
+
onClick={confirm}
|
|
275
|
+
/>
|
|
276
|
+
<ModalFooterButton
|
|
277
|
+
darkMode={darkMode}
|
|
278
|
+
formatText={false}
|
|
279
|
+
type="secondary"
|
|
280
|
+
text="Cancel"
|
|
281
|
+
onClick={close}
|
|
282
|
+
/>
|
|
283
|
+
</ModalFooter>
|
|
284
|
+
</Modal>
|
|
285
|
+
</Dialog>
|
|
286
|
+
);
|
|
287
|
+
},
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Cube's header button: the step that runs, as a lambda with no runtime and
|
|
292
|
+
* no row limit, becomes a materialized view of the ingest definition (Add
|
|
293
|
+
* MatView, named, classified and keyed in a dialog), or replaces the one
|
|
294
|
+
* opened to edit (Save MatView), at once unless its classification or its
|
|
295
|
+
* key must be asked again
|
|
296
|
+
*/
|
|
297
|
+
const MaterializedViewButton = observer(
|
|
298
|
+
(props: {
|
|
299
|
+
embeddedCubeState: EmbeddedCubeState;
|
|
300
|
+
editorState: CubeEditorState;
|
|
301
|
+
}) => {
|
|
302
|
+
const { embeddedCubeState, editorState } = props;
|
|
303
|
+
const { execution } = editorState;
|
|
304
|
+
const { matView } = embeddedCubeState;
|
|
305
|
+
const [dialogInput, setDialogInput] = useState<
|
|
306
|
+
MaterializedViewDialogInput | undefined
|
|
307
|
+
>();
|
|
308
|
+
const [isBuilding, setIsBuilding] = useState(false);
|
|
309
|
+
const start = (): void => {
|
|
310
|
+
// the open step editor's edits go in first, as for Execute
|
|
311
|
+
if (!editorState.nodeEditor.finishApplied()) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
// the columns of the step that runs, read with its lambda
|
|
315
|
+
const selected = editorState.document.query.selected;
|
|
316
|
+
const columns = (
|
|
317
|
+
selected === undefined
|
|
318
|
+
? undefined
|
|
319
|
+
: editorState.analysis.schemas.get(selected)
|
|
320
|
+
)?.columns.map((column) => column.name);
|
|
321
|
+
setIsBuilding(true);
|
|
322
|
+
buildCubeRunningStepLambda(editorState)
|
|
323
|
+
.then((lambda) => {
|
|
324
|
+
if (matView === undefined) {
|
|
325
|
+
setDialogInput({ lambda, columns });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const saveProblems =
|
|
329
|
+
embeddedCubeState.getMaterializedViewSaveProblems(columns);
|
|
330
|
+
if (saveProblems.length) {
|
|
331
|
+
setDialogInput({ lambda, columns, saveProblems });
|
|
332
|
+
} else {
|
|
333
|
+
embeddedCubeState.saveMaterializedView(lambda);
|
|
334
|
+
}
|
|
335
|
+
})
|
|
336
|
+
.catch(editorState.host.applicationStore.alertUnhandledError)
|
|
337
|
+
.finally(() => setIsBuilding(false));
|
|
338
|
+
};
|
|
339
|
+
return (
|
|
340
|
+
<>
|
|
341
|
+
<CubeButton
|
|
342
|
+
primary={true}
|
|
343
|
+
title={
|
|
344
|
+
!execution.canExecute
|
|
345
|
+
? formatDisabledReasons(execution.disabledReasons)
|
|
346
|
+
: matView === undefined
|
|
347
|
+
? 'Add the step that runs to the ingest definition, as a materialized view'
|
|
348
|
+
: `Replace materialized view ${matView} with the step that runs`
|
|
349
|
+
}
|
|
350
|
+
disabled={!execution.canExecute || isBuilding}
|
|
351
|
+
onClick={start}
|
|
352
|
+
>
|
|
353
|
+
{matView === undefined ? 'Add MatView' : 'Save MatView'}
|
|
354
|
+
</CubeButton>
|
|
355
|
+
{dialogInput && (
|
|
356
|
+
<MaterializedViewDialog
|
|
357
|
+
embeddedCubeState={embeddedCubeState}
|
|
358
|
+
input={dialogInput}
|
|
359
|
+
close={() => setDialogInput(undefined)}
|
|
360
|
+
/>
|
|
361
|
+
)}
|
|
362
|
+
</>
|
|
363
|
+
);
|
|
364
|
+
},
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Legend Cube, full screen over the editor, building a materialized view of
|
|
369
|
+
* the ingest definition it was opened from (Create MatView), or
|
|
370
|
+
* editing one of its views, read back into the cube (Edit MatView). Loaded
|
|
371
|
+
* on the first opening, with Cube (`EmbeddedCube`)
|
|
372
|
+
*/
|
|
373
|
+
export const EmbeddedCubeEditor = observer(
|
|
374
|
+
(props: {
|
|
375
|
+
embeddedCubeState: EmbeddedCubeState;
|
|
376
|
+
ingest: IngestDefinition;
|
|
377
|
+
}) => {
|
|
378
|
+
const { embeddedCubeState, ingest } = props;
|
|
379
|
+
const { editorStore } = embeddedCubeState;
|
|
380
|
+
// a new host for each opening, so each starts on an empty cube
|
|
381
|
+
const [host] = useState(() => new LegendStudioCubeHost(editorStore));
|
|
382
|
+
const { matView } = embeddedCubeState;
|
|
383
|
+
// the view as it was when opened: the cube is read from it once
|
|
384
|
+
const [initialLambda] = useState(() => {
|
|
385
|
+
const lambda = embeddedCubeState.matViewLambda;
|
|
386
|
+
return lambda && matView !== undefined
|
|
387
|
+
? { lambda, label: `materialized view ${matView}` }
|
|
388
|
+
: undefined;
|
|
389
|
+
});
|
|
390
|
+
const darkMode =
|
|
391
|
+
!editorStore.applicationStore.layoutService
|
|
392
|
+
.TEMPORARY__isLightColorThemeEnabled;
|
|
393
|
+
return (
|
|
394
|
+
<Dialog
|
|
395
|
+
open={true}
|
|
396
|
+
onClose={noop} // closed by its own button only, so a cube isn't lost by Escape
|
|
397
|
+
classes={{
|
|
398
|
+
root: 'editor-modal__root-container',
|
|
399
|
+
container: 'editor-modal__container',
|
|
400
|
+
paper:
|
|
401
|
+
'editor-modal__content query-builder__dialog__container__content',
|
|
402
|
+
}}
|
|
403
|
+
>
|
|
404
|
+
<Modal
|
|
405
|
+
darkMode={darkMode}
|
|
406
|
+
className="editor-modal query-builder__dialog query-builder__dialog--expanded"
|
|
407
|
+
>
|
|
408
|
+
<div
|
|
409
|
+
className="query-builder__dialog__header"
|
|
410
|
+
data-testid={EMBEDDED_CUBE_TEST_ID.DIALOG}
|
|
411
|
+
>
|
|
412
|
+
<div className="min-w-0 flex-1 truncate px-4 text-lg font-medium text-[var(--color-state-disabled-on-accent)]">
|
|
413
|
+
{matView === undefined
|
|
414
|
+
? `Create MatView for ${ingest.path} with Cube`
|
|
415
|
+
: `Edit MatView ${matView} of ${ingest.path} with Cube`}
|
|
416
|
+
</div>
|
|
417
|
+
<div className="query-builder__dialog__header__actions">
|
|
418
|
+
<button
|
|
419
|
+
className="query-builder__dialog__header__action"
|
|
420
|
+
tabIndex={-1}
|
|
421
|
+
onClick={() => embeddedCubeState.close()}
|
|
422
|
+
title={
|
|
423
|
+
matView === undefined
|
|
424
|
+
? 'Close Cube, adding nothing'
|
|
425
|
+
: 'Close Cube, saving nothing'
|
|
426
|
+
}
|
|
427
|
+
>
|
|
428
|
+
<TimesIcon />
|
|
429
|
+
</button>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
<div className="query-builder__dialog__content">
|
|
433
|
+
<CubeEditor
|
|
434
|
+
host={host}
|
|
435
|
+
initialLambda={initialLambda}
|
|
436
|
+
headerActions={(editorState) => (
|
|
437
|
+
<MaterializedViewButton
|
|
438
|
+
embeddedCubeState={embeddedCubeState}
|
|
439
|
+
editorState={editorState}
|
|
440
|
+
/>
|
|
441
|
+
)}
|
|
442
|
+
/>
|
|
443
|
+
</div>
|
|
444
|
+
</Modal>
|
|
445
|
+
</Dialog>
|
|
446
|
+
);
|
|
447
|
+
},
|
|
448
|
+
);
|
|
@@ -40,6 +40,7 @@ import { LEGEND_STUDIO_SETTING_CONFIG } from '../../__lib__/LegendStudioSetting.
|
|
|
40
40
|
import { setupPureLanguageService } from '@finos/legend-code-editor';
|
|
41
41
|
import { configureCodeEditorComponent } from '@finos/legend-lego/code-editor';
|
|
42
42
|
import { STO_RELATIONAL_LEGEND_STUDIO_COMMAND_CONFIG } from '../../__lib__/STO_Relational_LegendStudioCommand.js';
|
|
43
|
+
import { LEGEND_CUBE_COMMAND_CONFIG } from '@finos/legend-cube-builder';
|
|
43
44
|
import { ShowcaseManager } from '../ShowcaseManager.js';
|
|
44
45
|
import { CabinetIcon } from '@finos/legend-art';
|
|
45
46
|
import { ShowcaseManagerState } from '../../stores/ShowcaseManagerState.js';
|
|
@@ -120,6 +121,9 @@ export class Core_LegendStudioApplicationPlugin extends LegendStudioApplicationP
|
|
|
120
121
|
...collectKeyedCommandConfigEntriesFromConfig(
|
|
121
122
|
STO_RELATIONAL_LEGEND_STUDIO_COMMAND_CONFIG,
|
|
122
123
|
),
|
|
124
|
+
// Legend Cube's shortcuts (F9, Ctrl/Cmd+Z), for Create MatView with
|
|
125
|
+
// Cube: Studio's own are off while the cube is open
|
|
126
|
+
...collectKeyedCommandConfigEntriesFromConfig(LEGEND_CUBE_COMMAND_CONFIG),
|
|
123
127
|
];
|
|
124
128
|
}
|
|
125
129
|
|
|
@@ -111,6 +111,7 @@ import {
|
|
|
111
111
|
import { GlobalTestRunnerState } from './sidebar-state/testable/GlobalTestRunnerState.js';
|
|
112
112
|
import type { LegendStudioApplicationStore } from '../LegendStudioBaseStore.js';
|
|
113
113
|
import { EmbeddedQueryBuilderState } from './EmbeddedQueryBuilderState.js';
|
|
114
|
+
import { EmbeddedCubeState } from './embedded-cube/EmbeddedCubeState.js';
|
|
114
115
|
import { LEGEND_STUDIO_COMMAND_KEY } from '../../__lib__/LegendStudioCommand.js';
|
|
115
116
|
import { EditorTabManagerState } from './EditorTabManagerState.js';
|
|
116
117
|
import { GraphEditFormModeState } from './GraphEditFormModeState.js';
|
|
@@ -198,6 +199,7 @@ export class EditorStore implements CommandRegistrar {
|
|
|
198
199
|
modelImporterState: ModelImporterState;
|
|
199
200
|
projectConfigurationEditorState: ProjectConfigurationEditorState;
|
|
200
201
|
embeddedQueryBuilderState: EmbeddedQueryBuilderState;
|
|
202
|
+
embeddedCubeState: EmbeddedCubeState;
|
|
201
203
|
embeddedDataCubeViewerState: QueryBuilderDataCubeViewerState | undefined;
|
|
202
204
|
newElementState: NewElementState;
|
|
203
205
|
/**
|
|
@@ -297,6 +299,7 @@ export class EditorStore implements CommandRegistrar {
|
|
|
297
299
|
this,
|
|
298
300
|
);
|
|
299
301
|
this.embeddedQueryBuilderState = new EmbeddedQueryBuilderState(this);
|
|
302
|
+
this.embeddedCubeState = new EmbeddedCubeState(this);
|
|
300
303
|
// side bar panels
|
|
301
304
|
this.explorerTreeState = new ExplorerTreeState(this);
|
|
302
305
|
this.projectOverviewState = new ProjectOverviewState(this, this.sdlcState);
|
|
@@ -445,9 +448,11 @@ export class EditorStore implements CommandRegistrar {
|
|
|
445
448
|
*/
|
|
446
449
|
createEditorCommandTrigger(additionalChecker?: () => boolean): () => boolean {
|
|
447
450
|
return (): boolean =>
|
|
448
|
-
// we don't want to leak any hotkeys when we have embedded query builder
|
|
451
|
+
// we don't want to leak any hotkeys when we have embedded query builder
|
|
452
|
+
// or Legend Cube open, e.g. Studio's F9 compiles where Cube's executes
|
|
449
453
|
// TODO?: we probably should come up with a more generic mechanism for this
|
|
450
454
|
!this.embeddedQueryBuilderState.queryBuilderState &&
|
|
455
|
+
!this.embeddedCubeState.isOpen &&
|
|
451
456
|
(!additionalChecker || additionalChecker());
|
|
452
457
|
}
|
|
453
458
|
|