Skip to content

Commit

Permalink
Provide diagram support for mapping files (#46)
Browse files Browse the repository at this point in the history
* Provide diagram support for mapping files

- Move and rename previous diagram classes to 'System Diagram'
- Introduce new 'Mapping Diagram' contribution in GLSP (server, client)
- Support dropping entities on diagram for new source objects
- Support command palette (Ctrl+Space) to create new source objects
- Support create attribute mappings by dragging attributes
- Support create attribute mappings by specifying literals on canvas
- Support deletion of elements and mappings

- Use auto-layout for diagram (source objects left, target right)
-- Do not create dedicated diagram file with layout information
-- Auto layout elements manually until Elk can be properly used

- Adapt test cases

- Add handler for unhandledPromise rejections to avoid server crashes

- Improve styling for all diagrams
-- Provide grid movement
-- Provide proper background
-- Improve styling of tool palette
-- Color node types

Co-authored-by: Tobias Ortmayr <tortmayr@eclipsesource.com>

* Compile against Node 16

* Minor fix for literal creation palette not showing up sometimes

* Fix minor issue if string without end quote was provided, e.g., "asdfa

* Created ExampleDWH diagram.

* Updated some diagrams and entities to make example more complete.

* Ensure implicit attributes are properly re-calculated

* Updated example mapping.

---------

Co-authored-by: Tobias Ortmayr <tortmayr@eclipsesource.com>
Co-authored-by: Harmen Wessels <97173058+harmen-xb@users.noreply.github.com>
  • Loading branch information
3 people committed Feb 14, 2024
1 parent 0e252e7 commit 3d9eba8
Show file tree
Hide file tree
Showing 123 changed files with 3,748 additions and 1,159 deletions.
6 changes: 3 additions & 3 deletions .vscode/settings.json
Expand Up @@ -6,9 +6,9 @@
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true,
"source.fixAll.eslint": true,
"source.fixAll.markdownlint": true
"source.organizeImports": "explicit",
"source.fixAll.eslint": "explicit",
"source.fixAll.markdownlint": "explicit"
},
"eslint.validate": ["javascript", "typescript"],
"search.exclude": {
Expand Down
2 changes: 1 addition & 1 deletion e2e-tests/src/tests/crossmodel-explorer-view.spec.ts
Expand Up @@ -45,7 +45,7 @@ test.describe('CrossModel Explorer View', () => {
expect(await menu.isOpen()).toBe(true);
// Expect the Code and Form editor to be in the Open With menu option.
expect(await menu.menuItemByNamePath('Open With', 'Code Editor')).toBeDefined();
expect(await menu.menuItemByNamePath('Open With', 'CrossModel Diagram')).toBeDefined();
expect(await menu.menuItemByNamePath('Open With', 'System Diagram')).toBeDefined();
await menu.close();
});
});
8 changes: 4 additions & 4 deletions e2e-tests/src/tests/crossmodel-tabbar-toolbar.spec.ts
Expand Up @@ -84,7 +84,7 @@ test.describe('CrossModel TabBar Toolbar', () => {

test('create new diagram from tabbar toolbar', async () => {
// Get the new-entity toolbar item.
const tabBarToolbarNewEntity = await tabBarToolbar.toolBarItem('crossbreeze.new.diagram.toolbar');
const tabBarToolbarNewEntity = await tabBarToolbar.toolBarItem('crossbreeze.new.system-diagram.toolbar');
expect(tabBarToolbarNewEntity).toBeDefined();
if (tabBarToolbarNewEntity) {
// expect(await tabBarToolbarNewEntity.isEnabled()).toBe(true);
Expand All @@ -95,7 +95,7 @@ test.describe('CrossModel TabBar Toolbar', () => {
// Wait for the New Entity dialog to popup.
newDiagramDialog.waitForVisible();
// Check the title of the dialog.
expect(await newDiagramDialog.title()).toBe('New Diagram...');
expect(await newDiagramDialog.title()).toBe('New SystemDiagram...');
// Set the name for the new entity.
await newDiagramDialog.enterSingleInput('diagram-created-from-tabbar-toolbar');
// Wait until we can click the main button.
Expand All @@ -106,9 +106,9 @@ test.describe('CrossModel TabBar Toolbar', () => {
await newDiagramDialog.waitForClosed();

explorer = await app.openView(CrossModelExplorerView);
const file = await explorer.getFileStatNodeByLabel('diagram-created-from-tabbar-toolbar.diagram.cm');
const file = await explorer.getFileStatNodeByLabel('diagram-created-from-tabbar-toolbar.system-diagram.cm');
expect(file).toBeDefined();
expect(await file.label()).toBe('diagram-created-from-tabbar-toolbar.diagram.cm');
expect(await file.label()).toBe('diagram-created-from-tabbar-toolbar.system-diagram.cm');
}
});
});
30 changes: 20 additions & 10 deletions examples/mapping-example/ExampleCRM/diagrams/CRM.diagram.cm
@@ -1,22 +1,32 @@
diagram:
systemDiagram:
id: CRM
name: "CRM"
description: "Shows the complete CRM"
nodes:
- id: CustomerNode
entity: Customer
x: 325.5893891316664
y: 261.8195919791379
width: 122.22364807128906
height: 151
x: 363
y: 264
width: 141.7887420654297
height: 176
- id: OrderNode
entity: Order
x: 649.4416344093675
y: 274.85224527770106
width: 139.6079559326172
height: 132
x: 649
y: 275
width: 148.649658203125
height: 147
- id: AddressNode
entity: ExampleCRM.Address
x: 88
y: 297
width: 159.649658203125
height: 96
edges:
- id: CustomerToOrder
relationship: Order_Customer
sourceNode: CustomerNode
targetNode: OrderNode
targetNode: OrderNode
- id: AddressToCustomer
relationship: ExampleCRM.Address_Customer
sourceNode: AddressNode
targetNode: CustomerNode
Expand Up @@ -19,4 +19,7 @@ entity:
datatype: "Varchar"
- id: Phone
name: "Phone"
datatype: "Varchar"
datatype: "Varchar"
- id: BirthDate
name: "BirthDate"
datatype: "DateTime"
@@ -0,0 +1,5 @@
relationship:
id: Address_Customer
parent: Customer
child: ExampleCRM.Address
type: "1:1"
10 changes: 10 additions & 0 deletions examples/mapping-example/ExampleDWH/CalcAge.entity.cm
@@ -0,0 +1,10 @@
entity:
id: CalcAge
name: "CalcAge"
attributes:
- id: BirthDate
name: "BirthDate"
datatype: "Integer"
- id: Age
name: "Age"
datatype: "Integer"
Expand Up @@ -8,6 +8,9 @@ entity:
- id: Country
name: "Country"
datatype: "Varchar"
- id: Age
name: "Age"
datatype: "Integer"
- id: FixedNumber
name: "FixedNumber"
datatype: "Integer"
Expand Down
@@ -0,0 +1,28 @@
mapping:
id: CompleteCustomerMapping
sources:
- id: CustomerSourceObject
entity: ExampleCRM.Customer
join: from
- id: CalcAgeSourceObject
entity: ExampleDWH.CalcAge
join: apply
relations:
- source: CustomerSourceObject
conditions:
- join: CalcAgeSourceObject.BirthDate = CustomerSourceObject.BirthDate
target:
entity: ExampleDWH.CompleteCustomer
mappings:
- attribute: Country
source: CustomerSourceObject.Country
- attribute: FixedNumber
source: 1
- attribute: FixedString
source: "Hoppa"
- attribute: Name
source: CustomerSourceObject.FirstName
- attribute: Age
source: CalcAgeSourceObject.Age
- attribute: Age
source: CalcAgeSourceObject.Age
62 changes: 36 additions & 26 deletions examples/mapping-example/ExampleDWH/DWH.mapping.cm
@@ -1,31 +1,41 @@
mapping:
id: CompleteCustomer_Example
sources:
- id: Customer
object: ExampleCRM.Customer
join: from
- id: Address
object: ExampleCRM.Address
join: left-join
relations:
- source: Customer
conditions:
- join: Address.CustomerID = Customer.Id
- id: Country
object: ExampleMasterdata.Country
join: left-join
relations:
- source: Address
conditions:
- join: Country.Code = Address.CountryCode
- id: Customer
entity: ExampleCRM.Customer
join: from
- id: Address
entity: ExampleCRM.Address
join: left-join
relations:
- source: Customer
conditions:
- join: Address.CustomerID = Customer.Id
- id: Country
entity: ExampleMasterdata.Country
join: left-join
relations:
- source: Address
conditions:
- join: Country.Code = Address.CountryCode
- id: AddressSourceObject
entity: ExampleCRM.Address
join: from
- id: CalcAgeSourceObject
entity: CalcAge
join: from
target:
entity: CompleteCustomer
attributes:
- attribute: Name
source: Customer.LastName
- attribute: Country
source: Country.Code
- attribute: FixedNumber
source: 1337
- attribute: FixedString
source: "Fixed String"
mappings:
- attribute: Name
source: Customer.FirstName
- attribute: Name
source: Customer.LastName
- attribute: Country
source: Country.Name
- attribute: Age
source: CalcAgeSourceObject.Age
- attribute: FixedNumber
source: 1337
- attribute: FixedString
source: "Fixed String"
16 changes: 16 additions & 0 deletions examples/mapping-example/ExampleDWH/ExampleDWH.system-diagram.cm
@@ -0,0 +1,16 @@
systemDiagram:
id: ExampleDWH
name: "ExampleDWH"
nodes:
- id: CalcAgeNode
entity: ExampleDWH.CalcAge
x: 195.046875
y: 262
width: 10
height: 10
- id: CompleteCustomerNode
entity: ExampleDWH.CompleteCustomer
x: 594
y: 264
width: 148.649658203125
height: 116
Expand Up @@ -3,7 +3,7 @@
********************************************************************************/
import { JsonRecordingCommand, MaybePromise } from '@eclipse-glsp/server';
import * as jsonPatch from 'fast-json-patch';
import { CrossModelSourceModel, CrossModelState } from '../model/cross-model-state.js';
import { CrossModelSourceModel, CrossModelState } from './cross-model-state.js';

/**
* A custom recording command that tracks updates during execution through a textual semantic state.
Expand Down
@@ -0,0 +1,20 @@
/********************************************************************************
* Copyright (c) 2024 CrossBreeze.
********************************************************************************/
import { Point } from '@eclipse-glsp/server';

export class Grid {
public static GRID_X = 5.0;
public static GRID_Y = 5.0;

public static snap(originalPoint: Point | undefined): Point | undefined {
if (originalPoint) {
return {
x: Math.round(originalPoint.x / this.GRID_X) * this.GRID_X,
y: Math.round(originalPoint.y / this.GRID_Y) * this.GRID_Y
};
} else {
return undefined;
}
}
}
@@ -0,0 +1,68 @@
/********************************************************************************
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/
import { GModelIndex } from '@eclipse-glsp/server';
import { inject, injectable } from 'inversify';
import { AstNode, streamAst } from 'langium';
import * as uuid from 'uuid';
import { CrossModelLSPServices } from '../../integration.js';
import { CrossModelRoot } from '../../language-server/generated/ast.js';

/**
* Custom model index that not only indexes the GModel elements but also the semantic elements (AstNodes) they represent.
*/
@injectable()
export class CrossModelIndex extends GModelIndex {
@inject(CrossModelLSPServices) services!: CrossModelLSPServices;

protected idToSemanticNode = new Map<string, AstNode>();

findId(node: AstNode | undefined, fallback: () => string): string;
findId(node: AstNode | undefined, fallback?: () => string | undefined): string | undefined;
findId(node: AstNode | undefined, fallback: () => string | undefined = () => undefined): string | undefined {
return this.doFindId(node) ?? fallback();
}

protected doFindId(node?: AstNode): string | undefined {
return this.services.language.references.IdProvider.getLocalId(node);
}

createId(node?: AstNode): string {
return this.findId(node, () => 'fallback_' + uuid.v4());
}

assertId(node?: AstNode): string {
const id = this.findId(node);
if (!id) {
throw new Error('Could not create ID for: ' + node?.$cstNode?.text);
}
return id;
}

indexSemanticRoot(root: CrossModelRoot): void {
this.idToSemanticNode.clear();
streamAst(root).forEach(node => this.indexAstNode(node));
}

protected indexAstNode(node: AstNode): void {
const id = this.findId(node);
console.log('INDEX', node.$type, id);
if (id) {
this.indexSemanticElement(id, node);
}
}

indexSemanticElement<T extends AstNode>(id: string, element: T): void {
this.idToSemanticNode.set(id, element);
}

findSemanticElement(id: string): AstNode | undefined;
findSemanticElement<T extends AstNode>(id: string, guard: (item: unknown) => item is T): T | undefined;
findSemanticElement<T extends AstNode>(id: string, guard?: (item: unknown) => item is T): T | AstNode | undefined {
const semanticNode = this.idToSemanticNode.get(id);
if (guard) {
return guard(semanticNode) ? semanticNode : undefined;
}
return semanticNode;
}
}
Expand Up @@ -6,7 +6,7 @@ import { inject, injectable } from 'inversify';
import { URI } from 'vscode-uri';
import { CrossModelLSPServices } from '../../integration.js';
import { IdProvider } from '../../language-server/cross-model-naming.js';
import { CrossModelRoot, SystemDiagram } from '../../language-server/generated/ast.js';
import { CrossModelRoot } from '../../language-server/generated/ast.js';
import { ModelService } from '../../model-server/model-service.js';
import { Serializer } from '../../model-server/serializer.js';
import { CrossModelIndex } from './cross-model-index.js';
Expand Down Expand Up @@ -47,10 +47,6 @@ export class CrossModelState extends DefaultModelState implements JsonModelState
return this._packageId;
}

get diagramRoot(): SystemDiagram {
return this.semanticRoot.diagram!;
}

get modelService(): ModelService {
return this.services.shared.model.ModelService;
}
Expand Down

0 comments on commit 3d9eba8

Please sign in to comment.