@finos/legend-application-studio 28.19.105 → 28.19.106

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 (32) hide show
  1. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.d.ts.map +1 -1
  2. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js +30 -7
  3. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js.map +1 -1
  4. package/lib/components/editor/editor-group/mapping-editor/MappingEditor.d.ts.map +1 -1
  5. package/lib/components/editor/editor-group/mapping-editor/MappingEditor.js +6 -3
  6. package/lib/components/editor/editor-group/mapping-editor/MappingEditor.js.map +1 -1
  7. package/lib/index.css +2 -2
  8. package/lib/index.css.map +1 -1
  9. package/lib/package.json +1 -1
  10. package/lib/stores/LegendStudioBaseStore.d.ts.map +1 -1
  11. package/lib/stores/LegendStudioBaseStore.js +1 -1
  12. package/lib/stores/LegendStudioBaseStore.js.map +1 -1
  13. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.d.ts +1 -0
  14. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.d.ts.map +1 -1
  15. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.js +17 -0
  16. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.js.map +1 -1
  17. package/lib/stores/editor/editor-state/element-editor-state/mapping/MappingDiagramGenerator.d.ts +19 -0
  18. package/lib/stores/editor/editor-state/element-editor-state/mapping/MappingDiagramGenerator.d.ts.map +1 -0
  19. package/lib/stores/editor/editor-state/element-editor-state/mapping/MappingDiagramGenerator.js +51 -0
  20. package/lib/stores/editor/editor-state/element-editor-state/mapping/MappingDiagramGenerator.js.map +1 -0
  21. package/lib/stores/editor/editor-state/element-editor-state/mapping/MappingEditorState.d.ts +2 -0
  22. package/lib/stores/editor/editor-state/element-editor-state/mapping/MappingEditorState.d.ts.map +1 -1
  23. package/lib/stores/editor/editor-state/element-editor-state/mapping/MappingEditorState.js +18 -0
  24. package/lib/stores/editor/editor-state/element-editor-state/mapping/MappingEditorState.js.map +1 -1
  25. package/package.json +11 -11
  26. package/src/components/editor/editor-group/dataProduct/DataProductEditor.tsx +87 -29
  27. package/src/components/editor/editor-group/mapping-editor/MappingEditor.tsx +26 -0
  28. package/src/stores/LegendStudioBaseStore.ts +0 -1
  29. package/src/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.ts +21 -0
  30. package/src/stores/editor/editor-state/element-editor-state/mapping/MappingDiagramGenerator.ts +90 -0
  31. package/src/stores/editor/editor-state/element-editor-state/mapping/MappingEditorState.ts +34 -0
  32. package/tsconfig.json +1 -0
@@ -86,6 +86,7 @@ import {
86
86
  type ChangeEventHandler,
87
87
  useCallback,
88
88
  useEffect,
89
+ useLayoutEffect,
89
90
  useRef,
90
91
  useState,
91
92
  } from 'react';
@@ -1337,47 +1338,104 @@ export const CompatibleDiagramsEditor = observer(
1337
1338
  },
1338
1339
  );
1339
1340
 
1341
+ const hasMappingSet = group.mapping.value.path !== '';
1342
+ const noDiagramsInProject =
1343
+ groupState.getCompatibleDiagramOptions().length === 0 &&
1344
+ group.diagrams.length === 0;
1345
+ const [isGenerating, setIsGenerating] = useState(false);
1346
+ const [isAddingDiagram, setIsAddingDiagram] = useState(false);
1347
+
1348
+ const handleGenerateFromMapping = (): void => {
1349
+ setIsGenerating(true);
1350
+ flowResult(groupState.generateDiagramFromMapping())
1351
+ .catch(
1352
+ groupState.state.editorStore.applicationStore.alertUnhandledError,
1353
+ )
1354
+ .finally(() => setIsGenerating(false));
1355
+ };
1356
+
1340
1357
  const NewDiagramComponent = observer(
1341
1358
  (newDiagramProps: {
1342
1359
  onFinishEditing: () => void;
1343
1360
  }): React.ReactElement => {
1344
1361
  const { onFinishEditing } = newDiagramProps;
1345
1362
 
1363
+ useLayoutEffect(() => {
1364
+ setIsAddingDiagram(true);
1365
+ return () => setIsAddingDiagram(false);
1366
+ }, []);
1367
+
1346
1368
  return (
1347
- <div className="panel__content__form__section__list__new-item__input">
1348
- <CustomSelectorInput
1349
- options={groupState.getCompatibleDiagramOptions()}
1350
- onChange={(event: {
1351
- label: string;
1352
- value: PackageableElement;
1353
- }) => {
1354
- onFinishEditing();
1355
- handleAddDiagram(event);
1356
- }}
1357
- placeholder="Select a diagram to add..."
1358
- darkMode={
1359
- !groupState.state.editorStore.applicationStore.layoutService
1360
- .TEMPORARY__isLightColorThemeEnabled
1361
- }
1362
- />
1363
- </div>
1369
+ <>
1370
+ <div className="panel__content__form__section__list__new-item__input">
1371
+ <CustomSelectorInput
1372
+ options={groupState.getCompatibleDiagramOptions()}
1373
+ onChange={(event: {
1374
+ label: string;
1375
+ value: PackageableElement;
1376
+ }) => {
1377
+ onFinishEditing();
1378
+ handleAddDiagram(event);
1379
+ }}
1380
+ placeholder="Select a diagram to add..."
1381
+ darkMode={
1382
+ !groupState.state.editorStore.applicationStore.layoutService
1383
+ .TEMPORARY__isLightColorThemeEnabled
1384
+ }
1385
+ />
1386
+ </div>
1387
+ {noDiagramsInProject &&
1388
+ hasMappingSet &&
1389
+ !groupState.state.isReadOnly && (
1390
+ <button
1391
+ className="panel__content__form__section__list__new-item__cancel-btn btn btn--dark"
1392
+ disabled={isGenerating}
1393
+ onClick={() => {
1394
+ onFinishEditing();
1395
+ handleGenerateFromMapping();
1396
+ }}
1397
+ title="Auto-generate a diagram from the mapping"
1398
+ tabIndex={-1}
1399
+ >
1400
+ {isGenerating ? 'Generating...' : 'Generate from Mapping'}
1401
+ </button>
1402
+ )}
1403
+ </>
1364
1404
  );
1365
1405
  },
1366
1406
  );
1367
1407
 
1368
1408
  return (
1369
- <ListEditor
1370
- title="Diagrams"
1371
- prompt="Add diagrams to include in this Data Product. Diagrams are required to showcase and explain your curated data models"
1372
- items={group.diagrams}
1373
- keySelector={(element: DataProductDiagram) => element.diagram.name}
1374
- ItemComponent={DiagramComponent}
1375
- NewItemComponent={NewDiagramComponent}
1376
- handleRemoveItem={handleRemoveDiagram}
1377
- isReadOnly={groupState.state.isReadOnly}
1378
- emptyMessage="⚠ No Diagrams specified. Add at least one diagram to this Data Product."
1379
- emptyClassName="data-product-editor__empty-diagram"
1380
- />
1409
+ <div className="data-product-editor__diagrams-section">
1410
+ <ListEditor
1411
+ title="Diagrams"
1412
+ prompt="Add diagrams to include in this Data Product. Diagrams are required to showcase and explain your curated data models"
1413
+ items={group.diagrams}
1414
+ keySelector={(element: DataProductDiagram) => element.diagram.name}
1415
+ ItemComponent={DiagramComponent}
1416
+ NewItemComponent={NewDiagramComponent}
1417
+ handleRemoveItem={handleRemoveDiagram}
1418
+ isReadOnly={groupState.state.isReadOnly}
1419
+ emptyMessage="⚠ No Diagrams specified. Add at least one diagram to this Data Product."
1420
+ emptyClassName="data-product-editor__empty-diagram"
1421
+ />
1422
+ {noDiagramsInProject &&
1423
+ !isAddingDiagram &&
1424
+ hasMappingSet &&
1425
+ !groupState.state.isReadOnly && (
1426
+ <div className="data-product-editor__generate-diagram-btn">
1427
+ <button
1428
+ className="panel__content__form__section__list__new-item__add-btn btn btn--dark"
1429
+ disabled={isGenerating}
1430
+ onClick={handleGenerateFromMapping}
1431
+ title="Generate a Diagram from the Mapping"
1432
+ tabIndex={-1}
1433
+ >
1434
+ {isGenerating ? 'Generating...' : 'Generate from Mapping'}
1435
+ </button>
1436
+ </div>
1437
+ )}
1438
+ </div>
1381
1439
  );
1382
1440
  },
1383
1441
  );
@@ -37,6 +37,8 @@ import {
37
37
  MenuContentItem,
38
38
  MenuContent,
39
39
  LockIcon,
40
+ PanelHeader,
41
+ ShapesIcon,
40
42
  } from '@finos/legend-art';
41
43
  import { ClassMappingEditor } from './ClassMappingEditor.js';
42
44
  import { EnumerationMappingEditor } from './EnumerationMappingEditor.js';
@@ -48,6 +50,7 @@ import {
48
50
  MAPPING_ELEMENT_TYPE,
49
51
  getMappingElementLabel,
50
52
  MAPPING_EDITOR_TAB,
53
+ onGeneratingDiagramFromMapping,
51
54
  } from '../../../../stores/editor/editor-state/element-editor-state/mapping/MappingEditorState.js';
52
55
  import { MappingElementState } from '../../../../stores/editor/editor-state/element-editor-state/mapping/MappingElementState.js';
53
56
  import { MappingExplorer } from './MappingExplorer.js';
@@ -353,6 +356,11 @@ export const MappingEditor = observer(() => {
353
356
  (tab: MAPPING_EDITOR_TAB): (() => void) =>
354
357
  (): void =>
355
358
  mappingEditorState.setSelectedTab(tab);
359
+ const generateDiagram = (): void => {
360
+ flowResult(onGeneratingDiagramFromMapping(mapping, editorStore)).catch(
361
+ editorStore.applicationStore.alertUnhandledError,
362
+ );
363
+ };
356
364
  return (
357
365
  <div
358
366
  data-testid={LEGEND_STUDIO_TEST_ID.MAPPING_EDITOR}
@@ -370,6 +378,24 @@ export const MappingEditor = observer(() => {
370
378
  <div className="panel__header__title__content">{mapping.name}</div>
371
379
  </div>
372
380
  </div>
381
+ <PanelHeader title="General" darkMode={true}>
382
+ <div className="panel__header__actions">
383
+ <div className="btn__dropdown-combo btn__dropdown-combo--primary">
384
+ <button
385
+ className="btn__dropdown-combo__label"
386
+ onClick={generateDiagram}
387
+ title="Generate Diagram"
388
+ tabIndex={-1}
389
+ style={{ width: 'auto', whiteSpace: 'nowrap' }}
390
+ >
391
+ <ShapesIcon className="btn__dropdown-combo__label__icon" />
392
+ <div className="btn__dropdown-combo__label__title">
393
+ Generate Diagram
394
+ </div>
395
+ </button>
396
+ </div>
397
+ </div>
398
+ </PanelHeader>
373
399
  <div className="panel__header service-editor__header--with-tabs">
374
400
  <div className="uml-element-editor__tabs">
375
401
  {Object.values(MAPPING_EDITOR_TAB).map((tab) => (
@@ -215,7 +215,6 @@ export class LegendStudioBaseStore {
215
215
  SDLCServerClient.authorizeCallbackUrl(
216
216
  this.applicationStore.config.sdlcServerUrl,
217
217
  this.applicationStore.navigationService.navigator.getCurrentAddress(),
218
- this.applicationStore.config.sdlcServerClient,
219
218
  ),
220
219
  );
221
220
  } else {
@@ -120,6 +120,7 @@ import type {
120
120
  import { deserialize } from 'serializr';
121
121
  import { Diagram } from '@finos/legend-extension-dsl-diagram';
122
122
  import { LegendStudioTelemetryHelper } from '../../../../../__lib__/LegendStudioTelemetryHelper.js';
123
+ import { onGeneratingDiagramFromMapping } from '../mapping/MappingEditorState.js';
123
124
 
124
125
  export enum DATA_PRODUCT_TAB {
125
126
  HOME = 'Home',
@@ -769,6 +770,10 @@ export class ModelAccessPointGroupState extends AccessPointGroupState {
769
770
  super(val, editorState);
770
771
  this.value = val;
771
772
  this.editorState = editorState;
773
+
774
+ makeObservable(this, {
775
+ generateDiagramFromMapping: flow,
776
+ });
772
777
  }
773
778
 
774
779
  override hasErrors(): boolean {
@@ -817,6 +822,22 @@ export class ModelAccessPointGroupState extends AccessPointGroupState {
817
822
  modelAccessPointGroup_removeDiagram(this.value, diagram);
818
823
  };
819
824
 
825
+ *generateDiagramFromMapping(): GeneratorFn<void> {
826
+ const mapping = this.value.mapping.value;
827
+ if (mapping.path === '') {
828
+ return;
829
+ }
830
+ const diagram = (yield flowResult(
831
+ onGeneratingDiagramFromMapping(mapping, this.state.editorStore),
832
+ )) as Diagram | undefined;
833
+ if (diagram) {
834
+ const newDiagram = observe_DataProductDiagram(new DataProductDiagram());
835
+ newDiagram.title = diagram.name;
836
+ newDiagram.diagram = diagram;
837
+ modelAccessPointGroup_addDiagram(this.value, newDiagram);
838
+ }
839
+ }
840
+
820
841
  addFeaturedElement(element: DataProductElement): void {
821
842
  const elementPointer = observe_DataProductElementScope(
822
843
  new DataProductElementScope(),
@@ -0,0 +1,90 @@
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
+ ClassView,
19
+ Diagram,
20
+ GeneralizationView,
21
+ PropertyView,
22
+ } from '@finos/legend-extension-dsl-diagram';
23
+ import {
24
+ Class,
25
+ getAllOwnClassProperties,
26
+ getMappingCompatibleClasses,
27
+ PackageableElementExplicitReference,
28
+ PropertyExplicitReference,
29
+ type Mapping,
30
+ } from '@finos/legend-graph';
31
+ import { uuid } from '@finos/legend-shared';
32
+
33
+ export const generateMappingDiagram = (
34
+ mapping: Mapping,
35
+ allClasses: Class[],
36
+ ): Diagram => {
37
+ let diagramName = mapping.name.replace(/mapping/i, 'Diagram');
38
+ if (diagramName === mapping.name) {
39
+ diagramName = `${mapping.name}GeneratedDiagram`;
40
+ }
41
+ const diagram = new Diagram(diagramName);
42
+ const classesInMapping = getMappingCompatibleClasses(mapping, allClasses);
43
+ diagram.classViews = classesInMapping.map(
44
+ (clazz) =>
45
+ new ClassView(
46
+ diagram,
47
+ uuid(),
48
+ PackageableElementExplicitReference.create(clazz),
49
+ ),
50
+ );
51
+ diagram.propertyViews = diagram.classViews.flatMap((classView) => {
52
+ return getAllOwnClassProperties(classView.class.value)
53
+ .filter(
54
+ (property) =>
55
+ property.genericType.value.rawType instanceof Class &&
56
+ classesInMapping.includes(property.genericType.value.rawType),
57
+ )
58
+ .map((property) => {
59
+ const targetClass = property.genericType.value.rawType as Class;
60
+ const targetView = diagram.classViews.find(
61
+ (view) => view.class.value === targetClass,
62
+ );
63
+ if (!targetView) {
64
+ throw new Error(
65
+ `Target class ${targetClass.name} for property ${property.name} not found in diagram`,
66
+ );
67
+ }
68
+ return new PropertyView(
69
+ diagram,
70
+ PropertyExplicitReference.create(property),
71
+ classView,
72
+ targetView,
73
+ );
74
+ });
75
+ });
76
+ diagram.classViews.forEach((classView) => {
77
+ diagram.classViews
78
+ .filter((view) =>
79
+ view.class.value.generalizations
80
+ .map((g) => g.value.rawType)
81
+ .includes(classView.class.value),
82
+ )
83
+ .forEach((childView) => {
84
+ diagram.generalizationViews.push(
85
+ new GeneralizationView(diagram, childView, classView),
86
+ );
87
+ });
88
+ });
89
+ return diagram;
90
+ };
@@ -130,6 +130,11 @@ import { MappingTestableState } from './testable/MappingTestableState.js';
130
130
  import { MappingTestMigrationState } from './legacy/MappingTestMigrationState.js';
131
131
  import { relationFunction_setRelationFunction } from '../../../../graph-modifier/STO_RelationFunction_GraphModifierHelper.js';
132
132
  import { RelationFunctionInstanceSetImplementationState } from './RelationFunctionInstanceSetImplementationState.js';
133
+ import { generateMappingDiagram } from './MappingDiagramGenerator.js';
134
+ import {
135
+ observe_Diagram,
136
+ type Diagram,
137
+ } from '@finos/legend-extension-dsl-diagram';
133
138
 
134
139
  export interface MappingExplorerTreeNodeData extends TreeNodeData {
135
140
  mappingElement: MappingElement;
@@ -615,6 +620,35 @@ const reprocessMappingElementNodes = (
615
620
  return { rootIds, nodes };
616
621
  };
617
622
 
623
+ export const onGeneratingDiagramFromMapping = flow(function* (
624
+ mapping: Mapping,
625
+ editorStore: EditorStore,
626
+ ): GeneratorFn<Diagram | undefined> {
627
+ try {
628
+ const diagram = generateMappingDiagram(
629
+ mapping,
630
+ editorStore.graphManagerState.usableClasses,
631
+ );
632
+ observe_Diagram(diagram);
633
+ editorStore.graphManagerState.graph.addElement(
634
+ diagram,
635
+ mapping.package?.path.replace(/mapping/, 'diagram'),
636
+ );
637
+ editorStore.graphEditorMode.openElement(diagram);
638
+ yield flowResult(editorStore.explorerTreeState.build());
639
+ editorStore.applicationStore.notificationService.notifySuccess(
640
+ `Successfully generated diagram ${diagram.path} from mapping ${mapping.name}`,
641
+ );
642
+ return diagram;
643
+ } catch (error) {
644
+ assertErrorThrown(error);
645
+ editorStore.applicationStore.notificationService.notifyError(
646
+ `Failed to generate diagram from mapping ${mapping.name}: ${error.message}`,
647
+ );
648
+ return undefined;
649
+ }
650
+ });
651
+
618
652
  export interface MappingElementSpec {
619
653
  showTarget: boolean;
620
654
  // whether or not to open the new mapping element tab as an adjacent tab, this behavior is similar to Chrome
package/tsconfig.json CHANGED
@@ -143,6 +143,7 @@
143
143
  "./src/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.ts",
144
144
  "./src/stores/editor/editor-state/element-editor-state/ingest/IngestDefinitionEditorState.ts",
145
145
  "./src/stores/editor/editor-state/element-editor-state/mapping/FlatDataInstanceSetImplementationState.ts",
146
+ "./src/stores/editor/editor-state/element-editor-state/mapping/MappingDiagramGenerator.ts",
146
147
  "./src/stores/editor/editor-state/element-editor-state/mapping/MappingEditorState.ts",
147
148
  "./src/stores/editor/editor-state/element-editor-state/mapping/MappingElementDecorator.ts",
148
149
  "./src/stores/editor/editor-state/element-editor-state/mapping/MappingElementState.ts",