@finos/legend-application-studio 28.21.40 → 28.21.42

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 (60) hide show
  1. package/lib/components/ElementIconUtils.d.ts.map +1 -1
  2. package/lib/components/ElementIconUtils.js +3 -1
  3. package/lib/components/ElementIconUtils.js.map +1 -1
  4. package/lib/components/editor/editor-group/EditorGroup.d.ts.map +1 -1
  5. package/lib/components/editor/editor-group/EditorGroup.js +5 -0
  6. package/lib/components/editor/editor-group/EditorGroup.js.map +1 -1
  7. package/lib/components/editor/editor-group/compute-editor/ComputeEditor.d.ts +19 -0
  8. package/lib/components/editor/editor-group/compute-editor/ComputeEditor.d.ts.map +1 -0
  9. package/lib/components/editor/editor-group/compute-editor/ComputeEditor.js +111 -0
  10. package/lib/components/editor/editor-group/compute-editor/ComputeEditor.js.map +1 -0
  11. package/lib/components/editor/side-bar/CreateNewElementModal.d.ts.map +1 -1
  12. package/lib/components/editor/side-bar/CreateNewElementModal.js +2 -0
  13. package/lib/components/editor/side-bar/CreateNewElementModal.js.map +1 -1
  14. package/lib/index.css +2 -2
  15. package/lib/index.css.map +1 -1
  16. package/lib/index.d.ts +1 -0
  17. package/lib/index.d.ts.map +1 -1
  18. package/lib/index.js +1 -0
  19. package/lib/index.js.map +1 -1
  20. package/lib/package.json +1 -1
  21. package/lib/stores/editor/EditorStore.d.ts.map +1 -1
  22. package/lib/stores/editor/EditorStore.js +1 -0
  23. package/lib/stores/editor/EditorStore.js.map +1 -1
  24. package/lib/stores/editor/EditorTabManagerState.d.ts.map +1 -1
  25. package/lib/stores/editor/EditorTabManagerState.js +5 -1
  26. package/lib/stores/editor/EditorTabManagerState.js.map +1 -1
  27. package/lib/stores/editor/NewElementState.d.ts +5 -1
  28. package/lib/stores/editor/NewElementState.d.ts.map +1 -1
  29. package/lib/stores/editor/NewElementState.js +21 -1
  30. package/lib/stores/editor/NewElementState.js.map +1 -1
  31. package/lib/stores/editor/editor-state/element-editor-state/compute/ComputeEditorState.d.ts +28 -0
  32. package/lib/stores/editor/editor-state/element-editor-state/compute/ComputeEditorState.d.ts.map +1 -0
  33. package/lib/stores/editor/editor-state/element-editor-state/compute/ComputeEditorState.js +63 -0
  34. package/lib/stores/editor/editor-state/element-editor-state/compute/ComputeEditorState.js.map +1 -0
  35. package/lib/stores/editor/editor-state/element-editor-state/compute/ComputeFormDescriptor.d.ts +75 -0
  36. package/lib/stores/editor/editor-state/element-editor-state/compute/ComputeFormDescriptor.d.ts.map +1 -0
  37. package/lib/stores/editor/editor-state/element-editor-state/compute/ComputeFormDescriptor.js +54 -0
  38. package/lib/stores/editor/editor-state/element-editor-state/compute/ComputeFormDescriptor.js.map +1 -0
  39. package/lib/stores/editor/editor-state/element-editor-state/compute/SnowflakeComputeFormDescriptor.d.ts +18 -0
  40. package/lib/stores/editor/editor-state/element-editor-state/compute/SnowflakeComputeFormDescriptor.d.ts.map +1 -0
  41. package/lib/stores/editor/editor-state/element-editor-state/compute/SnowflakeComputeFormDescriptor.js +211 -0
  42. package/lib/stores/editor/editor-state/element-editor-state/compute/SnowflakeComputeFormDescriptor.js.map +1 -0
  43. package/lib/stores/graph-modifier/DSL_Compute_GraphModifierHelper.d.ts +2 -3
  44. package/lib/stores/graph-modifier/DSL_Compute_GraphModifierHelper.d.ts.map +1 -1
  45. package/lib/stores/graph-modifier/DSL_Compute_GraphModifierHelper.js +3 -7
  46. package/lib/stores/graph-modifier/DSL_Compute_GraphModifierHelper.js.map +1 -1
  47. package/package.json +16 -16
  48. package/src/components/ElementIconUtils.tsx +3 -0
  49. package/src/components/editor/editor-group/EditorGroup.tsx +4 -0
  50. package/src/components/editor/editor-group/compute-editor/ComputeEditor.tsx +337 -0
  51. package/src/components/editor/side-bar/CreateNewElementModal.tsx +2 -0
  52. package/src/index.ts +1 -0
  53. package/src/stores/editor/EditorStore.ts +1 -0
  54. package/src/stores/editor/EditorTabManagerState.ts +4 -0
  55. package/src/stores/editor/NewElementState.ts +26 -0
  56. package/src/stores/editor/editor-state/element-editor-state/compute/ComputeEditorState.ts +84 -0
  57. package/src/stores/editor/editor-state/element-editor-state/compute/ComputeFormDescriptor.ts +122 -0
  58. package/src/stores/editor/editor-state/element-editor-state/compute/SnowflakeComputeFormDescriptor.ts +270 -0
  59. package/src/stores/graph-modifier/DSL_Compute_GraphModifierHelper.ts +7 -13
  60. package/tsconfig.json +4 -0
@@ -0,0 +1,337 @@
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 React from 'react';
18
+ import { observer } from 'mobx-react-lite';
19
+ import { useEditorStore } from '../../EditorStoreProvider.js';
20
+ import {
21
+ type AppDirOwner,
22
+ type SnowflakeComputeSpecification,
23
+ AppDirLevel,
24
+ AppDirNode,
25
+ observe_AppDirNode,
26
+ } from '@finos/legend-graph';
27
+ import {
28
+ clsx,
29
+ CustomSelectorInput,
30
+ PanelContent,
31
+ PanelForm,
32
+ PanelFormBooleanField,
33
+ PanelFormSection,
34
+ PanelFormTextField,
35
+ PanelHeader,
36
+ } from '@finos/legend-art';
37
+ import { useApplicationStore } from '@finos/legend-application';
38
+ import {
39
+ ComputeEditorState,
40
+ applyOptionalInt,
41
+ } from '../../../../stores/editor/editor-state/element-editor-state/compute/ComputeEditorState.js';
42
+ import {
43
+ type ComputeEnumOption,
44
+ type ComputeFormDescriptor,
45
+ type ComputeFormField,
46
+ getVisibleComputeFormSections,
47
+ } from '../../../../stores/editor/editor-state/element-editor-state/compute/ComputeFormDescriptor.js';
48
+ import { SNOWFLAKE_COMPUTE_FORM_DESCRIPTOR } from '../../../../stores/editor/editor-state/element-editor-state/compute/SnowflakeComputeFormDescriptor.js';
49
+ import {
50
+ appDirNode_setAppDirId,
51
+ appDirOwner_setProdParallel,
52
+ appDirOwner_setProduction,
53
+ } from '../../../../stores/graph-modifier/DSL_Compute_GraphModifierHelper.js';
54
+
55
+ const NumberField = (props: {
56
+ name: string;
57
+ value: number | undefined;
58
+ errorMessage?: string | undefined;
59
+ isReadOnly: boolean;
60
+ onChange: (value: string) => void;
61
+ }): React.ReactElement => {
62
+ const { name, value, errorMessage, isReadOnly, onChange } = props;
63
+ return (
64
+ <div className="input-group">
65
+ <input
66
+ className={clsx(
67
+ 'input input-group__input panel__content__form__section__input input--dark input--small',
68
+ { 'input--caution': Boolean(errorMessage) },
69
+ )}
70
+ type="number"
71
+ title={name}
72
+ disabled={isReadOnly}
73
+ value={value ?? ''}
74
+ onChange={(event) => onChange(event.target.value)}
75
+ />
76
+ {errorMessage && (
77
+ <div className="input-group__error-message input--small">
78
+ {errorMessage}
79
+ </div>
80
+ )}
81
+ </div>
82
+ );
83
+ };
84
+
85
+ const OwnerSection = observer((props: { state: ComputeEditorState }) => {
86
+ const { state } = props;
87
+ const owner = state.compute.owner;
88
+ const isReadOnly = state.isReadOnly;
89
+
90
+ const buildDeploymentNode = (id: number): AppDirNode => {
91
+ const node = observe_AppDirNode(new AppDirNode());
92
+ node.appDirId = id;
93
+ node.level = AppDirLevel.DEPLOYMENT;
94
+ return node;
95
+ };
96
+
97
+ const updateNode =
98
+ (
99
+ current: AppDirNode | undefined,
100
+ setNode: (owner: AppDirOwner, node: AppDirNode | undefined) => void,
101
+ ) =>
102
+ (raw: string): void => {
103
+ applyOptionalInt(raw, (id) => {
104
+ if (id === undefined) {
105
+ setNode(owner, undefined);
106
+ } else if (current) {
107
+ appDirNode_setAppDirId(current, id);
108
+ } else {
109
+ setNode(owner, buildDeploymentNode(id));
110
+ }
111
+ });
112
+ };
113
+
114
+ return (
115
+ <>
116
+ <div className="compute-editor__section-header">
117
+ <span>Owner</span>
118
+ </div>
119
+ <PanelForm>
120
+ <div className="compute-editor__field-row">
121
+ <PanelFormSection>
122
+ <div className="panel__content__form__section__header__label">
123
+ Production AppDir ID
124
+ </div>
125
+ <NumberField
126
+ name="Production AppDir ID"
127
+ value={owner.production?.appDirId}
128
+ errorMessage={
129
+ owner.production
130
+ ? undefined
131
+ : 'A production AppDir ID is required'
132
+ }
133
+ isReadOnly={isReadOnly}
134
+ onChange={updateNode(owner.production, appDirOwner_setProduction)}
135
+ />
136
+ </PanelFormSection>
137
+ <PanelFormSection>
138
+ <div className="panel__content__form__section__header__label">
139
+ Prod-parallel AppDir ID
140
+ </div>
141
+ <NumberField
142
+ name="Prod-parallel AppDir ID"
143
+ value={owner.prodParallel?.appDirId}
144
+ isReadOnly={isReadOnly}
145
+ onChange={updateNode(
146
+ owner.prodParallel,
147
+ appDirOwner_setProdParallel,
148
+ )}
149
+ />
150
+ </PanelFormSection>
151
+ </div>
152
+ </PanelForm>
153
+ </>
154
+ );
155
+ });
156
+
157
+ interface ComputeFormFieldProps {
158
+ field: ComputeFormField;
159
+ spec: SnowflakeComputeSpecification;
160
+ isReadOnly: boolean;
161
+ applyEdit: (mutate: () => void) => void;
162
+ }
163
+
164
+ const LabelledSection = (props: {
165
+ label: string;
166
+ children: React.ReactNode;
167
+ }): React.ReactElement => (
168
+ <PanelFormSection>
169
+ <div className="panel__content__form__section__header__label">
170
+ {props.label}
171
+ </div>
172
+ {props.children}
173
+ </PanelFormSection>
174
+ );
175
+
176
+ const ComputeFormFieldEditor = observer((props: ComputeFormFieldProps) => {
177
+ const { field, spec, isReadOnly, applyEdit } = props;
178
+ const applicationStore = useApplicationStore();
179
+
180
+ switch (field.widget) {
181
+ case 'text': {
182
+ return (
183
+ <PanelFormTextField
184
+ name={field.label}
185
+ value={field.get(spec)}
186
+ isReadOnly={isReadOnly}
187
+ // an emptied field arrives as `undefined`, which is how it is unset
188
+ update={(value): void => {
189
+ applyEdit(() => field.set(spec, value));
190
+ }}
191
+ />
192
+ );
193
+ }
194
+ case 'number': {
195
+ return (
196
+ <LabelledSection label={field.label}>
197
+ <NumberField
198
+ name={field.label}
199
+ value={field.get(spec)}
200
+ isReadOnly={isReadOnly}
201
+ onChange={(raw): void => {
202
+ applyOptionalInt(raw, (next) =>
203
+ applyEdit(() => field.set(spec, next)),
204
+ );
205
+ }}
206
+ />
207
+ </LabelledSection>
208
+ );
209
+ }
210
+ case 'boolean': {
211
+ return (
212
+ <PanelFormBooleanField
213
+ prompt={field.label}
214
+ value={field.get(spec) ?? false}
215
+ isReadOnly={isReadOnly}
216
+ update={(value: boolean | undefined): void => {
217
+ applyEdit(() => field.set(spec, value ?? false));
218
+ }}
219
+ />
220
+ );
221
+ }
222
+ case 'enum': {
223
+ const current = field.get(spec);
224
+ const selected: ComputeEnumOption | null =
225
+ field.options.find((option) => option.value === current) ??
226
+ (current === undefined
227
+ ? null
228
+ : { label: String(current), value: current });
229
+ return (
230
+ <LabelledSection label={field.label}>
231
+ <CustomSelectorInput
232
+ options={field.options}
233
+ value={selected}
234
+ onChange={(option: ComputeEnumOption | null): void => {
235
+ // a field carrying a default must always hold a value
236
+ if (!option && field.defaultValue !== undefined) {
237
+ return;
238
+ }
239
+ applyEdit(() => field.set(spec, option?.value));
240
+ }}
241
+ isClearable={field.defaultValue === undefined}
242
+ darkMode={
243
+ !applicationStore.layoutService
244
+ .TEMPORARY__isLightColorThemeEnabled
245
+ }
246
+ disabled={isReadOnly}
247
+ />
248
+ </LabelledSection>
249
+ );
250
+ }
251
+ default:
252
+ return null;
253
+ }
254
+ });
255
+
256
+ const SnowflakeForm = observer(
257
+ (props: {
258
+ descriptor: ComputeFormDescriptor;
259
+ spec: SnowflakeComputeSpecification;
260
+ applyEdit: (mutate: () => void) => void;
261
+ isReadOnly: boolean;
262
+ }) => {
263
+ const { descriptor, spec, applyEdit, isReadOnly } = props;
264
+
265
+ return (
266
+ <>
267
+ <PanelForm>
268
+ <div className="compute-editor__lead-field">
269
+ <ComputeFormFieldEditor
270
+ field={descriptor.leadField}
271
+ spec={spec}
272
+ isReadOnly={isReadOnly}
273
+ applyEdit={applyEdit}
274
+ />
275
+ </div>
276
+ </PanelForm>
277
+
278
+ {getVisibleComputeFormSections(descriptor, spec).map((section) => (
279
+ <React.Fragment key={section.title}>
280
+ <div className="compute-editor__section-header">
281
+ <span>{section.title}</span>
282
+ </div>
283
+ <PanelForm>
284
+ {section.fields.map((field) => (
285
+ <ComputeFormFieldEditor
286
+ key={field.key}
287
+ field={field}
288
+ spec={spec}
289
+ isReadOnly={isReadOnly}
290
+ applyEdit={applyEdit}
291
+ />
292
+ ))}
293
+ </PanelForm>
294
+ </React.Fragment>
295
+ ))}
296
+ </>
297
+ );
298
+ },
299
+ );
300
+
301
+ export const ComputeEditor = observer(() => {
302
+ const editorStore = useEditorStore();
303
+ const applicationStore = useApplicationStore();
304
+ const state =
305
+ editorStore.tabManagerState.getCurrentEditorState(ComputeEditorState);
306
+ const isReadOnly = state.isReadOnly;
307
+ const spec = state.snowflakeSpecification;
308
+
309
+ return (
310
+ <div className="compute-editor">
311
+ <PanelHeader
312
+ title={spec ? SNOWFLAKE_COMPUTE_FORM_DESCRIPTOR.label : 'compute'}
313
+ darkMode={
314
+ !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled
315
+ }
316
+ isReadOnly={isReadOnly}
317
+ />
318
+ <PanelContent className="compute-editor__content">
319
+ <OwnerSection state={state} />
320
+ {spec ? (
321
+ <SnowflakeForm
322
+ descriptor={SNOWFLAKE_COMPUTE_FORM_DESCRIPTOR}
323
+ spec={spec}
324
+ applyEdit={state.applyFieldEdit}
325
+ isReadOnly={isReadOnly}
326
+ />
327
+ ) : (
328
+ <div className="compute-editor__unknown-message">
329
+ This Compute&apos;s specification type is not supported in form
330
+ mode. Open it in text mode to edit while preserving the original
331
+ content.
332
+ </div>
333
+ )}
334
+ </PanelContent>
335
+ </div>
336
+ );
337
+ });
@@ -97,6 +97,8 @@ export const getElementTypeLabel = (
97
97
  return 'local connection';
98
98
  case PACKAGEABLE_ELEMENT_TYPE._DATA_PRODUCT:
99
99
  return 'Lakehouse Data Product';
100
+ case PACKAGEABLE_ELEMENT_TYPE._COMPUTE:
101
+ return 'Compute';
100
102
  default: {
101
103
  if (type) {
102
104
  const extraElementTypeLabelGetters = editorStore.pluginManager
package/src/index.ts CHANGED
@@ -68,6 +68,7 @@ export * from './stores/extensions/DSL_DataProduct_LegendStudioApplicationPlugin
68
68
  export * from './stores/extensions/DSL_DataSpace_LegendStudioApplicationPlugin_Extension.js';
69
69
  export * from './stores/extensions/DSL_Data_LegendStudioApplicationPlugin_Extension.js';
70
70
  export { DataProductEditorState } from './stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.js';
71
+ export { ComputeEditorState } from './stores/editor/editor-state/element-editor-state/compute/ComputeEditorState.js';
71
72
  export { LEGEND_STUDIO_DOCUMENTATION_KEY } from './__lib__/LegendStudioDocumentation.js';
72
73
  export { LegendStudioTelemetryHelper } from './__lib__/LegendStudioTelemetryHelper.js';
73
74
 
@@ -1277,6 +1277,7 @@ export class EditorStore implements CommandRegistrar {
1277
1277
  PACKAGEABLE_ELEMENT_TYPE.MAPPING,
1278
1278
  PACKAGEABLE_ELEMENT_TYPE.SERVICE,
1279
1279
  PACKAGEABLE_ELEMENT_TYPE._DATA_PRODUCT,
1280
+ PACKAGEABLE_ELEMENT_TYPE._COMPUTE,
1280
1281
  this.applicationStore.config.options
1281
1282
  .TEMPORARY__enableLocalConnectionBuilder
1282
1283
  ? PACKAGEABLE_ELEMENT_TYPE.TEMPORARY__LOCAL_CONNECTION
@@ -36,6 +36,7 @@ import {
36
36
  SnowflakeApp,
37
37
  SnowflakeM2MUdf,
38
38
  HostedService,
39
+ Compute,
39
40
  DataProduct,
40
41
  IngestDefinition,
41
42
  MemSQLFunction,
@@ -75,6 +76,7 @@ import { ArtifactGenerationViewerState } from './editor-state/ArtifactGeneration
75
76
  import { DataProductEditorState } from './editor-state/element-editor-state/dataProduct/DataProductEditorState.js';
76
77
  import { IngestDefinitionEditorState } from './editor-state/element-editor-state/ingest/IngestDefinitionEditorState.js';
77
78
  import { AvailabilityEditorState } from './editor-state/element-editor-state/availability/AvailabilityEditorState.js';
79
+ import { ComputeEditorState } from './editor-state/element-editor-state/compute/ComputeEditorState.js';
78
80
  import type { EditorInitialConfiguration } from './editor-state/element-editor-state/ElementEditorInitialConfiguration.js';
79
81
 
80
82
  export class EditorTabManagerState extends TabManagerState {
@@ -190,6 +192,8 @@ export class EditorTabManagerState extends TabManagerState {
190
192
  return new DataProductEditorState(this.editorStore, element, config);
191
193
  } else if (element instanceof Availability) {
192
194
  return new AvailabilityEditorState(this.editorStore, element);
195
+ } else if (element instanceof Compute) {
196
+ return new ComputeEditorState(this.editorStore, element);
193
197
  } else if (element instanceof GenerationSpecification) {
194
198
  return new GenerationSpecificationEditorState(this.editorStore, element);
195
199
  } else if (element instanceof FileGenerationSpecification) {
@@ -90,6 +90,10 @@ import {
90
90
  ModelAccessPointGroup,
91
91
  stub_Mapping,
92
92
  InternalDataProductType,
93
+ AppDirOwner,
94
+ Compute,
95
+ SnowflakeComputeSpecification,
96
+ SnowflakeWarehouseType,
93
97
  } from '@finos/legend-graph';
94
98
  import type { DSL_Mapping_LegendStudioApplicationPlugin_Extension } from '../extensions/DSL_Mapping_LegendStudioApplicationPlugin_Extension.js';
95
99
  import {
@@ -564,6 +568,21 @@ export class NewLakehouseDataProductDriver extends NewElementDriver<DataProduct>
564
568
  }
565
569
  }
566
570
 
571
+ export class NewComputeDriver extends NewElementDriver<Compute> {
572
+ override get isValid(): boolean {
573
+ return true;
574
+ }
575
+
576
+ override createElement(name: string): Compute {
577
+ const compute = new Compute(name);
578
+ compute.owner = new AppDirOwner();
579
+ const specification = new SnowflakeComputeSpecification();
580
+ specification.warehouseType = SnowflakeWarehouseType.STANDARD;
581
+ compute.specification = specification;
582
+ return compute;
583
+ }
584
+ }
585
+
567
586
  export class NewServiceDriver extends NewElementDriver<Service> {
568
587
  mappingOption?: PackageableElementOption<Mapping> | undefined;
569
588
  runtimeOption: RuntimeOption;
@@ -853,6 +872,9 @@ export class NewElementState {
853
872
  case PACKAGEABLE_ELEMENT_TYPE._DATA_PRODUCT:
854
873
  driver = new NewLakehouseDataProductDriver(this.editorStore);
855
874
  break;
875
+ case PACKAGEABLE_ELEMENT_TYPE._COMPUTE:
876
+ driver = new NewComputeDriver(this.editorStore);
877
+ break;
856
878
  default: {
857
879
  const extraNewElementDriverCreators = this.editorStore.pluginManager
858
880
  .getApplicationPlugins()
@@ -1091,6 +1113,10 @@ export class NewElementState {
1091
1113
  NewLakehouseDataProductDriver,
1092
1114
  ).createElement(name);
1093
1115
  break;
1116
+ case PACKAGEABLE_ELEMENT_TYPE._COMPUTE:
1117
+ element =
1118
+ this.getNewElementDriver(NewComputeDriver).createElement(name);
1119
+ break;
1094
1120
  default: {
1095
1121
  const extraNewElementFromStateCreators = this.editorStore.pluginManager
1096
1122
  .getApplicationPlugins()
@@ -0,0 +1,84 @@
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 { action, computed, makeObservable } from 'mobx';
18
+ import { guaranteeType } from '@finos/legend-shared';
19
+ import {
20
+ Compute,
21
+ type PackageableElement,
22
+ SnowflakeComputeSpecification,
23
+ } from '@finos/legend-graph';
24
+ import { ElementEditorState } from '../ElementEditorState.js';
25
+ import type { EditorStore } from '../../../EditorStore.js';
26
+ import { reconcileComputeSpec } from './ComputeFormDescriptor.js';
27
+ import { SNOWFLAKE_COMPUTE_FORM_DESCRIPTOR } from './SnowflakeComputeFormDescriptor.js';
28
+
29
+ export const applyOptionalInt = (
30
+ raw: string,
31
+ apply: (value: number | undefined) => void,
32
+ ): void => {
33
+ // a cleared number input reports `''`, which is how the field is unset
34
+ if (raw === '') {
35
+ apply(undefined);
36
+ return;
37
+ }
38
+ const parsed = Number(raw);
39
+ if (!Number.isInteger(parsed)) {
40
+ return;
41
+ }
42
+ apply(parsed);
43
+ };
44
+
45
+ export class ComputeEditorState extends ElementEditorState {
46
+ constructor(editorStore: EditorStore, element: PackageableElement) {
47
+ super(editorStore, element);
48
+ makeObservable(this, {
49
+ compute: computed,
50
+ snowflakeSpecification: computed,
51
+ applyFieldEdit: action,
52
+ });
53
+ }
54
+
55
+ get compute(): Compute {
56
+ return guaranteeType(
57
+ this.element,
58
+ Compute,
59
+ 'Element of compute editor state must be a Compute',
60
+ );
61
+ }
62
+
63
+ /** `undefined` for spec types the form cannot render yet. */
64
+ get snowflakeSpecification(): SnowflakeComputeSpecification | undefined {
65
+ const spec = this.compute.specification;
66
+ return spec instanceof SnowflakeComputeSpecification ? spec : undefined;
67
+ }
68
+
69
+ applyFieldEdit = (mutate: () => void): void => {
70
+ const spec = this.snowflakeSpecification;
71
+ if (!spec) {
72
+ return;
73
+ }
74
+ mutate();
75
+ reconcileComputeSpec(SNOWFLAKE_COMPUTE_FORM_DESCRIPTOR, spec);
76
+ };
77
+
78
+ override reprocess(
79
+ newElement: PackageableElement,
80
+ editorStore: EditorStore,
81
+ ): ComputeEditorState {
82
+ return new ComputeEditorState(editorStore, newElement);
83
+ }
84
+ }
@@ -0,0 +1,122 @@
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
+ /**
18
+ * The renderer and the reconcile pass read the same `visible` predicates, so
19
+ * what the user sees and what the model may hold cannot drift apart. The engine
20
+ * rejects properties that are illegal for the selected warehouse type.
21
+ *
22
+ * Bound to `SnowflakeComputeSpecification` rather than generic over a vendor
23
+ * spec: Snowflake is the only vendor whose form is conditional.
24
+ *
25
+ * Numeric limits are not modelled. The engine's are tunable server-side, so a
26
+ * copy here would reject values the backend accepts.
27
+ */
28
+
29
+ import type { SnowflakeComputeSpecification } from '@finos/legend-graph';
30
+
31
+ /** Every enum option is a string except `generation`, which is numeric. */
32
+ export type ComputeEnumOptionValue = string | number;
33
+
34
+ export interface ComputeEnumOption {
35
+ label: string;
36
+ value: ComputeEnumOptionValue;
37
+ }
38
+
39
+ interface ComputeFormFieldBase<TValue> {
40
+ key: string;
41
+ label: string;
42
+ /** When false, the reconcile pass clears the field rather than hiding it. */
43
+ visible: (spec: SnowflakeComputeSpecification) => boolean;
44
+ /** Set on the fields that must always hold a value while visible. */
45
+ defaultValue?: TValue | undefined;
46
+ get: (spec: SnowflakeComputeSpecification) => TValue | undefined;
47
+ set(spec: SnowflakeComputeSpecification, value: TValue | undefined): void;
48
+ }
49
+
50
+ export type ComputeFormField =
51
+ | ({
52
+ widget: 'text';
53
+ } & ComputeFormFieldBase<string>)
54
+ | ({ widget: 'number' } & ComputeFormFieldBase<number>)
55
+ | ({ widget: 'boolean' } & ComputeFormFieldBase<boolean>)
56
+ | ({
57
+ widget: 'enum';
58
+ options: ComputeEnumOption[];
59
+ } & ComputeFormFieldBase<ComputeEnumOptionValue>);
60
+
61
+ type ComputeEnumFormField = Extract<ComputeFormField, { widget: 'enum' }>;
62
+
63
+ export interface ComputeFormSection {
64
+ title: string;
65
+ fields: ComputeFormField[];
66
+ }
67
+
68
+ export interface ComputeFormDescriptor {
69
+ label: string;
70
+ /** The field the others' `visible` predicates key off. */
71
+ leadField: ComputeEnumFormField;
72
+ sections: ComputeFormSection[];
73
+ }
74
+
75
+ export const getComputeFormFields = (
76
+ descriptor: ComputeFormDescriptor,
77
+ ): ComputeFormField[] => [
78
+ descriptor.leadField,
79
+ ...descriptor.sections.flatMap((section) => section.fields),
80
+ ];
81
+
82
+ export const getVisibleComputeFormSections = (
83
+ descriptor: ComputeFormDescriptor,
84
+ spec: SnowflakeComputeSpecification,
85
+ ): ComputeFormSection[] =>
86
+ descriptor.sections
87
+ .map((section) => ({
88
+ title: section.title,
89
+ fields: section.fields.filter((field) => field.visible(spec)),
90
+ }))
91
+ .filter((section) => section.fields.length > 0);
92
+
93
+ /**
94
+ * Bring `spec` back in line with the descriptor after a mutation. Defaults
95
+ * apply only while a field is visible, and cleared values are not restored.
96
+ */
97
+ export const reconcileComputeSpec = (
98
+ descriptor: ComputeFormDescriptor,
99
+ spec: SnowflakeComputeSpecification,
100
+ ): void => {
101
+ const fields = getComputeFormFields(descriptor);
102
+ // the bound is only to stop a descriptor whose predicates form a cycle
103
+ const maxPasses = fields.length + 1;
104
+ for (let pass = 0; pass < maxPasses; pass++) {
105
+ let changed = false;
106
+ for (const field of fields) {
107
+ const current = field.get(spec);
108
+ if (!field.visible(spec)) {
109
+ if (current !== undefined) {
110
+ field.set(spec, undefined);
111
+ changed = true;
112
+ }
113
+ } else if (current === undefined && field.defaultValue !== undefined) {
114
+ field.set(spec, field.defaultValue as never);
115
+ changed = true;
116
+ }
117
+ }
118
+ if (!changed) {
119
+ return;
120
+ }
121
+ }
122
+ };