From be2a625f750996ab51a498375aac5d256eadbecd Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Mon, 22 Apr 2024 12:56:03 +0200 Subject: [PATCH 01/54] Add basic Rete.js example for PolyAlg --- package-lock.json | 129 ++++++++++++++++-- package.json | 9 +- src/app/components/components.module.ts | 3 +- .../render-item/render-item.component.html | 4 + src/app/components/polyalg/editor.ts | 103 ++++++++++++++ .../polyalg/models/polyalg-plan.model.ts | 15 ++ .../polyalg-viewer.component.html | 3 + .../polyalg-viewer.component.scss | 3 + .../polyalg-viewer.component.ts | 34 +++++ src/app/models/information-page.model.ts | 3 + 10 files changed, 295 insertions(+), 11 deletions(-) create mode 100644 src/app/components/polyalg/editor.ts create mode 100644 src/app/components/polyalg/models/polyalg-plan.model.ts create mode 100644 src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.html create mode 100644 src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.scss create mode 100644 src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.ts diff --git a/package-lock.json b/package-lock.json index 640a0a45..9466ceec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@angular/common": "^17.2.3", "@angular/compiler": "^17.2.3", "@angular/core": "^17.2.3", + "@angular/elements": "^17.2.3", "@angular/forms": "^17.2.3", "@angular/platform-browser": "^17.2.3", "@angular/platform-browser-dynamic": "^17.2.3", @@ -55,6 +56,11 @@ "ngx-markdown": "^17.1.1", "ngx-plyr": "^4.0.1", "prismjs": "1.29.0", + "rete": "^2.0.3", + "rete-angular-plugin": "^2.1.1", + "rete-area-plugin": "^2.0.4", + "rete-auto-arrange-plugin": "^2.0.1", + "rete-connection-plugin": "^2.0.1", "rxjs": "^7.8.1", "rxjs-compat": "^6.5.5", "sass": "^1.26.3", @@ -1724,6 +1730,26 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/@angular/elements": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/@angular/elements/-/elements-17.2.3.tgz", + "integrity": "sha512-6Av61/sbH3sbFJ2QyHOX8hwPOzZ2lLwkgtKIMDqCrPkXxROxuOUH8wtBnwf7A3CYl5xzWNOUaaZH4RaAlqT60Q==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/core": "17.2.3", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/elements/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/@angular/forms": { "version": "17.2.3", "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.2.3.tgz", @@ -3679,7 +3705,6 @@ "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -8465,10 +8490,10 @@ "dev": true }, "node_modules/elkjs": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.2.tgz", - "integrity": "sha512-2Y/RaA1pdgSHpY0YG4TYuYCD2wh97CRvu22eLG3Kz0pgQ/6KbIFTxsTnDc4MH/6hFlg2L/9qXrDMG0nMjP63iw==", - "optional": true + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz", + "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==", + "peer": true }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -11459,6 +11484,12 @@ "web-worker": "^1.2.0" } }, + "node_modules/mermaid/node_modules/elkjs": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", + "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==", + "optional": true + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -14391,8 +14422,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regenerator-transform": { "version": "0.15.2", @@ -14616,6 +14646,88 @@ "node": ">=8" } }, + "node_modules/rete": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/rete/-/rete-2.0.3.tgz", + "integrity": "sha512-/xzcyEBhVXhMZVZHElnYaLKOmTEuwlnul9Wfjvxw5sdl/+6Nqn2nyqIaW4koefrFpIWZy9aitnjnP3zeCMVDuw==", + "hasInstallScript": true, + "dependencies": { + "@babel/runtime": "^7.21.0" + } + }, + "node_modules/rete-angular-plugin": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/rete-angular-plugin/-/rete-angular-plugin-2.1.1.tgz", + "integrity": "sha512-oaGJ+y6RyLTXBtrzQdxlubZf2q1Zau+3cuLq1cwgG8ZVsaS72GsE8prLhxBa/PsODye1GGXyenxzzn8R0TV2tA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">= 12 < 18", + "@angular/core": ">= 12 < 18", + "@angular/elements": ">= 12 < 18", + "rete": "^2.0.1", + "rete-area-plugin": "^2.0.0", + "rete-render-utils": "^2.0.0", + "rxjs": ">=6.6.0", + "zone.js": "~0.11.4 || ~0.12.0 || ~0.13.0 || ~0.14.0" + } + }, + "node_modules/rete-angular-plugin/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/rete-area-plugin": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/rete-area-plugin/-/rete-area-plugin-2.0.4.tgz", + "integrity": "sha512-i0yDG/NGWnEjFd/aD+zCxH0gt47htYrSDuTUOfL6jnnlHboj+3gMOSfaUIU6wfoLW4lK/2MV4w0XP2+NdpNQ7g==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "peerDependencies": { + "rete": "^2.0.0" + } + }, + "node_modules/rete-auto-arrange-plugin": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/rete-auto-arrange-plugin/-/rete-auto-arrange-plugin-2.0.1.tgz", + "integrity": "sha512-vHxsrI+l3wxZzxPxG7hcgUbacXQfEc1ZEE28r08O1kEy0kUyNkJR5OeCiSizZ4VucsDmu21WUtFVa1rl5h+e1A==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "peerDependencies": { + "elkjs": "^0.8.2", + "rete": "^2.0.1", + "rete-area-plugin": "^2.0.0", + "web-worker": "^1.2.0" + } + }, + "node_modules/rete-connection-plugin": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/rete-connection-plugin/-/rete-connection-plugin-2.0.1.tgz", + "integrity": "sha512-KE1IcjeOQtHgkByODtWS5hgRJDGhR3Z9sZyJAEd7YMgI6o+KUIflcNjbkvhJvPeIAv6WlEAh7ZkwdLhF9bkr4w==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "peerDependencies": { + "rete": "^2.0.1", + "rete-area-plugin": "^2.0.0" + } + }, + "node_modules/rete-render-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/rete-render-utils/-/rete-render-utils-2.0.2.tgz", + "integrity": "sha512-f4kj+dFL5QrebOkjCdwi8htHteDFbKyqrVdFDToEUvGuGod1sdLeKxOPBOhwyYDB4Zxd3Cq84I93vD2etrTL9g==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "peerDependencies": { + "rete": "^2.0.0", + "rete-area-plugin": "^2.0.0" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -16398,8 +16510,7 @@ "node_modules/web-worker": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz", - "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==", - "optional": true + "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==" }, "node_modules/webpack": { "version": "5.88.2", diff --git a/package.json b/package.json index 432c6856..4b886b57 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@angular/common": "^17.2.3", "@angular/compiler": "^17.2.3", "@angular/core": "^17.2.3", + "@angular/elements": "^17.2.3", "@angular/forms": "^17.2.3", "@angular/platform-browser": "^17.2.3", "@angular/platform-browser-dynamic": "^17.2.3", @@ -66,7 +67,13 @@ "ts-helpers": "^1.1.2", "ts-md5": "^1.2.7", "uuid": "^9.0.0", - "zone.js": "~0.14.4" + "zone.js": "~0.14.4", + "rete": "^2.0.3", + "rete-angular-plugin": "^2.1.1", + "rete-connection-plugin": "^2.0.1", + "rete-auto-arrange-plugin": "^2.0.1", + "rete-area-plugin": "^2.0.4", + "rete-render-utils": "^2.0.2" }, "devDependencies": { "@angular-builders/custom-webpack": "^17.0.1", diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts index 59d87818..adf824ff 100644 --- a/src/app/components/components.module.ts +++ b/src/app/components/components.module.ts @@ -99,6 +99,7 @@ import {ToastComponent as Toast} from './toast-exposer/toast/toast.component'; import {ReloadButtonComponent} from '../views/util/reload-button/reload-button.component'; import {ViewComponent} from './data-view/view/view.component'; import {DockerInstanceComponent} from './docker/dockerinstance/dockerinstance.component'; +import {PolyalgViewerComponent} from "./polyalg/polyalg-viewer/polyalg-viewer.component"; //import 'hammerjs'; @@ -168,7 +169,7 @@ import {DockerInstanceComponent} from './docker/dockerinstance/dockerinstance.co DropdownMenuDirective, DropdownItemDirective, DropdownDividerDirective, - DropdownToggleDirective, ModalTitleDirective, FormDirective, RowDirective, DropdownComponent, FormSelectDirective, TooltipDirective, ContainerComponent + DropdownToggleDirective, ModalTitleDirective, FormDirective, RowDirective, DropdownComponent, FormSelectDirective, TooltipDirective, ContainerComponent, PolyalgViewerComponent ], declarations: [ BreadcrumbComponent, diff --git a/src/app/components/information-manager/render-item/render-item.component.html b/src/app/components/information-manager/render-item/render-item.component.html index 50a35002..3affda78 100644 --- a/src/app/components/information-manager/render-item/render-item.component.html +++ b/src/app/components/information-manager/render-item/render-item.component.html @@ -63,6 +63,10 @@ + + + +
extends ClassicPreset.Connection { +} + +type Schemes = GetSchemes>; +type AreaExtra = AngularArea2D; + + +export async function createEditor(container: HTMLElement, injector: Injector, node: PlanNode) { + const socket = new ClassicPreset.Socket("socket"); + + const editor = new NodeEditor(); + const area = new AreaPlugin(container); + const connection = new ConnectionPlugin(); + const render = new AngularPlugin({injector}); + const arrange = new AutoArrangePlugin(); + + AreaExtensions.selectableNodes(area, AreaExtensions.selector(), { + accumulating: AreaExtensions.accumulateOnCtrl() + }); + + render.addPreset(Presets.classic.setup()); + + connection.addPreset(ConnectionPresets.classic.setup()); + + const applier = new ArrangeAppliers.TransitionApplier({ + duration: 250, + timingFunction: (t) => t, + async onTick() { + await AreaExtensions.zoomAt(area, editor.getNodes()); + } + }); + + arrange.addPreset(ArrangePresets.classic.setup()); + + editor.use(area); + area.use(connection); + area.use(render); + area.use(arrange); + + AreaExtensions.simpleNodesOrder(area); + + const [nodes, connections] = addNode(socket, node); + for (const n of nodes) { + await editor.addNode(n); + } + + for (const c of connections) { + await editor.addConnection(c); + } + + await arrange.layout({applier}); + + AreaExtensions.zoomAt(area, editor.getNodes()); + + + return () => area.destroy(); +} + +function addNode(socket: ClassicPreset.Socket, node: PlanNode): [Node[], Connection[]] { + const nodes = [] + const connections = [] + const algNode = new Node(node.opName, node.inputs.length); + algNode.addOutput("out", new ClassicPreset.Output(socket)); + algNode.addControl("asdf", new ClassicPreset.InputControl("text", { + readonly: true, + initial: JSON.stringify(node.arguments) + })); + + for (let i = 0; i < node.inputs.length; i++) { + const [childNodes, childConnections] = addNode(socket, node.inputs[i]); + const childNode = childNodes[childNodes.length - 1]; + nodes.push(...childNodes); + connections.push(...childConnections); + + algNode.addInput(i.toString(), new ClassicPreset.Input(socket)); + connections.push(new Connection(childNode, "out", algNode, i.toString())); + } + nodes.push(algNode); + return [nodes, connections]; +} diff --git a/src/app/components/polyalg/models/polyalg-plan.model.ts b/src/app/components/polyalg/models/polyalg-plan.model.ts new file mode 100644 index 00000000..aae19871 --- /dev/null +++ b/src/app/components/polyalg/models/polyalg-plan.model.ts @@ -0,0 +1,15 @@ + +export interface PlanNode { + opName: string; + arguments: { + [key: string]: PlanArgument + }; + inputs: PlanNode[]; + defaultValue: string; +} + +interface PlanArgument { + type: string; + value: any; +} + diff --git a/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.html b/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.html new file mode 100644 index 00000000..53ec03dc --- /dev/null +++ b/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.html @@ -0,0 +1,3 @@ +
+
+
\ No newline at end of file diff --git a/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.scss b/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.scss new file mode 100644 index 00000000..7bacefa5 --- /dev/null +++ b/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.scss @@ -0,0 +1,3 @@ +.rete { + min-height: 600px; +} \ No newline at end of file diff --git a/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.ts b/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.ts new file mode 100644 index 00000000..aef2d1c4 --- /dev/null +++ b/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.ts @@ -0,0 +1,34 @@ +import {AfterViewInit, Component, ElementRef, Injector, Input, ViewChild} from '@angular/core'; +import {createEditor} from "../editor"; +import {PlanNode} from "../models/polyalg-plan.model"; + +@Component({ + selector: 'app-polyalg-viewer', + standalone: true, + imports: [], + templateUrl: './polyalg-viewer.component.html', + styleUrl: './polyalg-viewer.component.scss' +}) +export class PolyalgViewerComponent implements AfterViewInit{ + @Input() planObject: string; + @Input() planType: "LOGICAL" | "ROUTED" | "PHYSICAL"; + @ViewChild("rete") container!: ElementRef; + + constructor(private injector: Injector) {} + + initPlan() { + console.log("hi!"); + } + + ngOnInit() { + this.initPlan(); + } + + ngAfterViewInit(): void { + const el = this.container.nativeElement; + + if (el) { + createEditor(el, this.injector, JSON.parse(this.planObject) as PlanNode); + } + } +} diff --git a/src/app/models/information-page.model.ts b/src/app/models/information-page.model.ts index 3dda1635..5814ab67 100644 --- a/src/app/models/information-page.model.ts +++ b/src/app/models/information-page.model.ts @@ -43,6 +43,9 @@ export interface InformationObject extends Duration { graphType?: string; //debugger queryPlan: string; + //polyalg + jsonPolyAlg: string; + planType: "LOGICAL" | "ROUTED" | "PHYSICAL"; //code code?: string; language?: string; From 7341494d12f0b3fc55147353bdff3cd664ceefba Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Wed, 24 Apr 2024 19:32:28 +0200 Subject: [PATCH 02/54] Add custom controls for several PolyAlgArgs --- package-lock.json | 16 ++- package.json | 3 +- src/app/components/components.module.ts | 20 ++- .../render-item/render-item.component.html | 2 +- src/app/components/polyalg/alg-editor.ts | 131 ++++++++++++++++++ .../polyalg/algnode/alg-node.component.html | 39 ++++++ .../polyalg/algnode/alg-node.component.scss | 87 ++++++++++++ .../polyalg/algnode/alg-node.component.ts | 71 ++++++++++ .../components/polyalg/algnode/background.ts | 28 ++++ .../polyalg/controls/arg-control-utils.ts | 41 ++++++ .../polyalg/controls/arg-control.ts | 12 ++ .../boolean-arg/boolean-arg.component.html | 8 ++ .../boolean-arg/boolean-arg.component.scss | 10 ++ .../boolean-arg/boolean-arg.component.ts | 27 ++++ .../entity-arg/entity-arg.component.html | 6 + .../entity-arg/entity-arg.component.scss | 10 ++ .../entity-arg/entity-arg.component.ts | 26 ++++ .../controls/list-arg/list-arg.component.html | 23 +++ .../controls/list-arg/list-arg.component.scss | 4 + .../controls/list-arg/list-arg.component.ts | 38 +++++ .../controls/rex-arg/rex-arg.component.html | 16 +++ .../controls/rex-arg/rex-arg.component.scss | 0 .../controls/rex-arg/rex-arg.component.ts | 31 +++++ .../string-arg/string-arg.component.html | 15 ++ .../string-arg/string-arg.component.scss | 0 .../string-arg/string-arg.component.ts | 28 ++++ .../custom-connection.component.scss | 16 +++ .../custom-connection.component.ts | 25 ++++ .../custom-socket.component.scss | 36 +++++ .../custom-socket/custom-socket.component.ts | 25 ++++ src/app/components/polyalg/editor.ts | 103 -------------- .../polyalg/models/polyalg-plan.model.ts | 51 ++++++- .../polyalg-viewer.component.html | 8 +- .../polyalg-viewer.component.ts | 12 +- .../default-layout.component.html | 7 + src/app/models/information-page.model.ts | 1 + .../querying/polyalg/polyalg.component.html | 3 + .../querying/polyalg/polyalg.component.scss | 7 + .../querying/polyalg/polyalg.component.ts | 33 +++++ .../views/querying/querying.component.html | 1 + src/app/views/views.module.ts | 14 +- 41 files changed, 909 insertions(+), 125 deletions(-) create mode 100644 src/app/components/polyalg/alg-editor.ts create mode 100644 src/app/components/polyalg/algnode/alg-node.component.html create mode 100644 src/app/components/polyalg/algnode/alg-node.component.scss create mode 100644 src/app/components/polyalg/algnode/alg-node.component.ts create mode 100644 src/app/components/polyalg/algnode/background.ts create mode 100644 src/app/components/polyalg/controls/arg-control-utils.ts create mode 100644 src/app/components/polyalg/controls/arg-control.ts create mode 100644 src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.html create mode 100644 src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.scss create mode 100644 src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts create mode 100644 src/app/components/polyalg/controls/entity-arg/entity-arg.component.html create mode 100644 src/app/components/polyalg/controls/entity-arg/entity-arg.component.scss create mode 100644 src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts create mode 100644 src/app/components/polyalg/controls/list-arg/list-arg.component.html create mode 100644 src/app/components/polyalg/controls/list-arg/list-arg.component.scss create mode 100644 src/app/components/polyalg/controls/list-arg/list-arg.component.ts create mode 100644 src/app/components/polyalg/controls/rex-arg/rex-arg.component.html create mode 100644 src/app/components/polyalg/controls/rex-arg/rex-arg.component.scss create mode 100644 src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts create mode 100644 src/app/components/polyalg/controls/string-arg/string-arg.component.html create mode 100644 src/app/components/polyalg/controls/string-arg/string-arg.component.scss create mode 100644 src/app/components/polyalg/controls/string-arg/string-arg.component.ts create mode 100644 src/app/components/polyalg/custom-connection/custom-connection.component.scss create mode 100644 src/app/components/polyalg/custom-connection/custom-connection.component.ts create mode 100644 src/app/components/polyalg/custom-socket/custom-socket.component.scss create mode 100644 src/app/components/polyalg/custom-socket/custom-socket.component.ts delete mode 100644 src/app/components/polyalg/editor.ts create mode 100644 src/app/views/querying/polyalg/polyalg.component.html create mode 100644 src/app/views/querying/polyalg/polyalg.component.scss create mode 100644 src/app/views/querying/polyalg/polyalg.component.ts diff --git a/package-lock.json b/package-lock.json index 9466ceec..bb2f60ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,6 +61,8 @@ "rete-area-plugin": "^2.0.4", "rete-auto-arrange-plugin": "^2.0.1", "rete-connection-plugin": "^2.0.1", + "rete-readonly-plugin": "^2.0.1", + "rete-render-utils": "^2.0.2", "rxjs": "^7.8.1", "rxjs-compat": "^6.5.5", "sass": "^1.26.3", @@ -14715,11 +14717,23 @@ "rete-area-plugin": "^2.0.0" } }, + "node_modules/rete-readonly-plugin": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/rete-readonly-plugin/-/rete-readonly-plugin-2.0.1.tgz", + "integrity": "sha512-PZqXOZy1QIW8kkPgJV5R3c7Jq5HZGtF+nzKK2kBhEbMLkvXMe4ixoCvdwaLKikSuMMCQQcyZVAyfQYb0cv+2EQ==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "peerDependencies": { + "rete": "^2.0.1", + "rete-area-plugin": "^2.0.0", + "rete-connection-plugin": "^2.0.0" + } + }, "node_modules/rete-render-utils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/rete-render-utils/-/rete-render-utils-2.0.2.tgz", "integrity": "sha512-f4kj+dFL5QrebOkjCdwi8htHteDFbKyqrVdFDToEUvGuGod1sdLeKxOPBOhwyYDB4Zxd3Cq84I93vD2etrTL9g==", - "peer": true, "dependencies": { "@babel/runtime": "^7.21.0" }, diff --git a/package.json b/package.json index 4b886b57..3550dc9a 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,8 @@ "rete-connection-plugin": "^2.0.1", "rete-auto-arrange-plugin": "^2.0.1", "rete-area-plugin": "^2.0.4", - "rete-render-utils": "^2.0.2" + "rete-render-utils": "^2.0.2", + "rete-readonly-plugin": "^2.0.1" }, "devDependencies": { "@angular-builders/custom-webpack": "^17.0.1", diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts index adf824ff..fcdc4351 100644 --- a/src/app/components/components.module.ts +++ b/src/app/components/components.module.ts @@ -100,6 +100,16 @@ import {ReloadButtonComponent} from '../views/util/reload-button/reload-button.c import {ViewComponent} from './data-view/view/view.component'; import {DockerInstanceComponent} from './docker/dockerinstance/dockerinstance.component'; import {PolyalgViewerComponent} from "./polyalg/polyalg-viewer/polyalg-viewer.component"; +import {AlgNodeComponent} from "./polyalg/algnode/alg-node.component"; +import {ReteModule} from "rete-angular-plugin/17"; +import {EntityArgComponent} from "./polyalg/controls/entity-arg/entity-arg.component"; +import {AutocompleteLibModule} from "angular-ng-autocomplete"; +import {ListArgComponent} from "./polyalg/controls/list-arg/list-arg.component"; +import {RexArgComponent} from './polyalg/controls/rex-arg/rex-arg.component'; +import {StringArgComponent} from './polyalg/controls/string-arg/string-arg.component'; +import {BooleanArgComponent} from './polyalg/controls/boolean-arg/boolean-arg.component'; +import {CustomSocketComponent} from './polyalg/custom-socket/custom-socket.component'; +import {CustomConnectionComponent} from './polyalg/custom-connection/custom-connection.component'; //import 'hammerjs'; @@ -169,7 +179,7 @@ import {PolyalgViewerComponent} from "./polyalg/polyalg-viewer/polyalg-viewer.co DropdownMenuDirective, DropdownItemDirective, DropdownDividerDirective, - DropdownToggleDirective, ModalTitleDirective, FormDirective, RowDirective, DropdownComponent, FormSelectDirective, TooltipDirective, ContainerComponent, PolyalgViewerComponent + DropdownToggleDirective, ModalTitleDirective, FormDirective, RowDirective, DropdownComponent, FormSelectDirective, TooltipDirective, ContainerComponent, PolyalgViewerComponent, ReteModule, AutocompleteLibModule ], declarations: [ BreadcrumbComponent, @@ -199,6 +209,14 @@ import {PolyalgViewerComponent} from "./polyalg/polyalg-viewer/polyalg-viewer.co ReloadButtonComponent, ViewComponent, DockerInstanceComponent, + AlgNodeComponent, + EntityArgComponent, + ListArgComponent, + RexArgComponent, + StringArgComponent, + BooleanArgComponent, + CustomSocketComponent, + CustomConnectionComponent ], exports: [ BreadcrumbComponent, diff --git a/src/app/components/information-manager/render-item/render-item.component.html b/src/app/components/information-manager/render-item/render-item.component.html index 3affda78..c797ecaf 100644 --- a/src/app/components/information-manager/render-item/render-item.component.html +++ b/src/app/components/information-manager/render-item/render-item.component.html @@ -64,7 +64,7 @@ - + diff --git a/src/app/components/polyalg/alg-editor.ts b/src/app/components/polyalg/alg-editor.ts new file mode 100644 index 00000000..3a829104 --- /dev/null +++ b/src/app/components/polyalg/alg-editor.ts @@ -0,0 +1,131 @@ +import {Injector} from "@angular/core"; +import {ClassicPreset, GetSchemes, NodeEditor} from "rete"; +import {AreaExtensions, AreaPlugin} from "rete-area-plugin"; +import {ConnectionPlugin, Presets as ConnectionPresets} from "rete-connection-plugin"; +import {AngularArea2D, AngularPlugin, Presets} from "rete-angular-plugin/17"; +import {PlanNode} from "./models/polyalg-plan.model"; +import {ArrangeAppliers, AutoArrangePlugin, Presets as ArrangePresets} from "rete-auto-arrange-plugin"; +import {AlgNode, AlgNodeComponent} from "./algnode/alg-node.component"; +import {addCustomBackground} from "./algnode/background"; +import {ArgControl} from "./controls/arg-control"; +import {getControl} from "./controls/arg-control-utils"; +import {CustomSocketComponent} from "./custom-socket/custom-socket.component"; +import {CustomConnection, CustomConnectionComponent} from "./custom-connection/custom-connection.component"; +import {ReadonlyPlugin} from "rete-readonly-plugin"; + +type Schemes = GetSchemes>; +type AreaExtra = AngularArea2D; + +export async function createEditor(container: HTMLElement, injector: Injector, node: PlanNode, readonly: boolean) { + const readonlyPlugin = new ReadonlyPlugin(); + + const socket = new ClassicPreset.Socket("socket"); + const editor = new NodeEditor(); + const area = new AreaPlugin(container); + const connection = new ConnectionPlugin(); + const render = new AngularPlugin({injector}); + const arrange = new AutoArrangePlugin(); + + AreaExtensions.selectableNodes(area, AreaExtensions.selector(), { + accumulating: AreaExtensions.accumulateOnCtrl() + }); + + render.addPreset(Presets.classic.setup({ + customize: { + node() { + return AlgNodeComponent; + }, + control(data) { + if (data.payload instanceof ArgControl) { + return data.payload.getArgComponent(); + } + return null; + }, + connection() { + return CustomConnectionComponent; + }, + socket() { + return CustomSocketComponent; + } + } + })); + + connection.addPreset(ConnectionPresets.classic.setup()); + + const applier = new ArrangeAppliers.TransitionApplier({ + duration: 250, + timingFunction: (t) => t, + async onTick() { + await AreaExtensions.zoomAt(area, editor.getNodes()); + } + }); + arrange.addPreset(ArrangePresets.classic.setup()); + + editor.use(readonlyPlugin.root); + editor.use(area); + area.use(readonlyPlugin.area); + area.use(render); + area.use(arrange); + + AreaExtensions.simpleNodesOrder(area); + addCustomBackground(area); + + const [nodes, connections] = addNode(socket, node, readonly); + for (const n of nodes) { + await editor.addNode(n); + } + + for (const c of connections) { + await editor.addConnection(c); + } + + await arrange.layout({applier, options: {'elk.direction': 'RIGHT', 'elk.alignment': 'RIGHT'}}); // https://github.com/retejs/rete/issues/697 + + AreaExtensions.zoomAt(area, editor.getNodes()); + + /*area.addPipe(context => { + if (context.type === 'nodepicked') { + const node = editor.getNode(context.data.id) + console.log(node, "was selected"); + } + return context + })*/ + + if (readonly) { + readonlyPlugin.enable(); // disable interaction with nodes (control interaction is deactivated separately) + } else { + area.use(connection); // make connections editable + } + + + return () => area.destroy(); +} + +function addNode(socket: ClassicPreset.Socket, node: PlanNode, readonly: boolean): [AlgNode[], CustomConnection[]] { + const nodes = [] + const connections = [] + const algNode = new AlgNode(node.opName, node.inputs.length); + algNode.addOutput("out", new ClassicPreset.Output(socket)); + + let heights = {}; + for (const [key, arg] of Object.entries(node.arguments)) { + const c = getControl(key, arg, readonly); + heights[key] = c.getHeight(); + algNode.addControl(key, c); + } + algNode.updateControlHeights(heights); + + for (let i = 0; i < node.inputs.length; i++) { + const [childNodes, childConnections] = addNode(socket, node.inputs[i], readonly); + const childNode = childNodes[childNodes.length - 1]; + nodes.push(...childNodes); + connections.push(...childConnections); + + algNode.addInput(i.toString(), new ClassicPreset.Input(socket)); + connections.push(new CustomConnection(childNode, "out", algNode, i.toString())); + } + nodes.push(algNode); + return [nodes, connections]; +} + + diff --git a/src/app/components/polyalg/algnode/alg-node.component.html b/src/app/components/polyalg/algnode/alg-node.component.html new file mode 100644 index 00000000..62e2111c --- /dev/null +++ b/src/app/components/polyalg/algnode/alg-node.component.html @@ -0,0 +1,39 @@ +
+

{{data.label}}

+
+
+
{{output.value?.label}}
+
+
+
+
+
+
{{input.value?.label}}
+
+
\ No newline at end of file diff --git a/src/app/components/polyalg/algnode/alg-node.component.scss b/src/app/components/polyalg/algnode/alg-node.component.scss new file mode 100644 index 00000000..77afc39d --- /dev/null +++ b/src/app/components/polyalg/algnode/alg-node.component.scss @@ -0,0 +1,87 @@ +@use "sass:math"; +@import "scss/style.scss"; + +$node-width: 350px; +$socket-margin: 6px; +$socket-size: 16px; + + +:host { + display: block; + background: white; + border: 4px solid $dark; + border-radius: 4px; + cursor: pointer; + //box-sizing: border-box; + width: $node-width !important; + height: auto !important; + padding-bottom: 6px; + position: relative; + user-select: none; + + .title-wrapper { + margin: -4px; + border-radius: 4px 4px 0 0; + } + + &:hover { + border-color: $primary; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + + .title-wrapper { + background-color: $primary !important; + } + } + + &.selected { + border-color: $primary; + + .title-wrapper { + background-color: $primary !important; + } + } + + .output { + text-align: right; + } + + .input { + text-align: left; + } + + .output-socket { + text-align: right; + margin-right: -20px; + display: inline-block; + } + + .input-socket { + text-align: left; + margin-left: -20px; + display: inline-block; + } + + .input-title, + .output-title { + vertical-align: middle; + color: white; + display: inline-block; + font-family: sans-serif; + font-size: 14px; + margin: $socket-margin; + line-height: $socket-size; + } + + .input-control { + z-index: 1; + width: calc(100% - #{$socket-size + 2*$socket-margin}); + vertical-align: middle; + display: inline-block; + } + + .control { + padding: $socket-margin math.div($socket-size, 2) + $socket-margin; + } + + +} \ No newline at end of file diff --git a/src/app/components/polyalg/algnode/alg-node.component.ts b/src/app/components/polyalg/algnode/alg-node.component.ts new file mode 100644 index 00000000..c6a0e91f --- /dev/null +++ b/src/app/components/polyalg/algnode/alg-node.component.ts @@ -0,0 +1,71 @@ +import {ChangeDetectorRef, Component, HostBinding, Input, OnChanges} from '@angular/core'; +import {ClassicPreset} from "rete"; +import {KeyValue} from "@angular/common"; + +type SortValue = (N['controls'] | N['inputs'] | N['outputs'])[string] + +@Component({ + selector: 'app-alg-node', + templateUrl: './alg-node.component.html', + styleUrl: './alg-node.component.scss' +}) +export class AlgNodeComponent implements OnChanges { + @Input() data!: AlgNode; + @Input() emit!: (data: any) => void + @Input() rendered!: () => void + + seed = 0 + + @HostBinding('class.selected') get selected() { + return this.data.selected + } + + constructor(private cdr: ChangeDetectorRef) { + this.cdr.detach() + } + + ngOnChanges(): void { + this.cdr.detectChanges() + requestAnimationFrame(() => this.rendered()) + this.seed++ // force render sockets + } + + sortByIndex>>(a: I, b: I) { + const ai = a.value?.index || 0 + const bi = b.value?.index || 0 + + return ai - bi + } +} + +const BASE_WIDTH = 350; +const BASE_HEIGHT = 110; + +export class AlgNode extends ClassicPreset.Node { + width = BASE_WIDTH; + height = BASE_HEIGHT; + controlHeights: { [key: string]: number } = {}; + numOfInputs = 0; + + constructor(props, numberOfInputs: number) { + super(props); + this.numOfInputs = numberOfInputs; + this.recomputeHeight(); + } + + updateControlHeights(heights: { [key: string]: number; }) { + this.controlHeights = heights; + this.recomputeHeight(); + } + + updateControlHeight(controlName: string, height: number) { + this.controlHeights[controlName] = height; + this.recomputeHeight(); + } + + recomputeHeight() { + const sum = Object.values(this.controlHeights).reduce((total, value) => total + value, 0); + this.height = BASE_HEIGHT + this.numOfInputs * 40 + sum; + } + +} diff --git a/src/app/components/polyalg/algnode/background.ts b/src/app/components/polyalg/algnode/background.ts new file mode 100644 index 00000000..97526864 --- /dev/null +++ b/src/app/components/polyalg/algnode/background.ts @@ -0,0 +1,28 @@ +import {BaseSchemes} from "rete"; +import {AreaPlugin} from "rete-area-plugin"; + +export function addCustomBackground( + area: AreaPlugin +) { + const background = document.createElement("div"); + + background.classList.add("background"); + background.classList.add("fill-area"); + + background.style.display = "table"; + background.style.zIndex = "-1"; + background.style.position = "absolute"; + background.style.top = "-320000px"; + background.style.left = "-320000px"; + background.style.width = "640000px"; + background.style.height = "640000px"; + + // Apply background styles + background.style.backgroundColor = "#ffffff"; + background.style.opacity = "1"; + background.style.backgroundImage = "radial-gradient(circle at 2px 2px, #ccc 2px, transparent 0)"; + background.style.backgroundSize = "50px 50px"; + + + area.area.content.add(background); +} \ No newline at end of file diff --git a/src/app/components/polyalg/controls/arg-control-utils.ts b/src/app/components/polyalg/controls/arg-control-utils.ts new file mode 100644 index 00000000..fb1161d8 --- /dev/null +++ b/src/app/components/polyalg/controls/arg-control-utils.ts @@ -0,0 +1,41 @@ +import {ArgControl} from "./arg-control"; +import {StringControl} from "./string-arg/string-arg.component"; +import {BooleanControl} from "./boolean-arg/boolean-arg.component"; +import {RexControl} from "./rex-arg/rex-arg.component"; +import {EntityControl} from "./entity-arg/entity-arg.component"; +import {ListControl} from "./list-arg/list-arg.component"; +import {BooleanArg, EntityArg, ListArg, PlanArgument, RexArg, StringArg} from "../models/polyalg-plan.model"; + +export function getControl(name: string, arg: PlanArgument, readonly: boolean): ArgControl { + switch (arg.type) { + case "ANY": + break; + case "INTEGER": + break; + case "STRING": + return new StringControl(name, arg.value as StringArg, readonly); + case "BOOLEAN": + return new BooleanControl(name, arg.value as BooleanArg, readonly); + case "REX": + return new RexControl(name, arg.value as RexArg, readonly); + case "AGGREGATE": + break; + case "LAX_AGGREGATE": + break; + case "ENTITY": + return new EntityControl(name, arg.value as EntityArg, readonly); + case "JOIN_TYPE_ENUM": + break; + case "MODIFY_OP_ENUM": + break; + case "FIELD": + break; + case "LIST": + return new ListControl(name, arg.value as ListArg, readonly) + case "COLLATION": + break; + case "CORR_ID": + break; + } + return new StringControl(name, {"arg": JSON.stringify(arg)}, readonly) +} \ No newline at end of file diff --git a/src/app/components/polyalg/controls/arg-control.ts b/src/app/components/polyalg/controls/arg-control.ts new file mode 100644 index 00000000..d3d7d36d --- /dev/null +++ b/src/app/components/polyalg/controls/arg-control.ts @@ -0,0 +1,12 @@ +import {ClassicPreset} from "rete"; +import {Type} from "@angular/core"; + +export abstract class ArgControl extends ClassicPreset.Control { + protected constructor(public name: string, public readonly: boolean) { + super(); + } + + abstract getHeight(): number; + + abstract getArgComponent(): Type; +} \ No newline at end of file diff --git a/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.html b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.html new file mode 100644 index 00000000..941fbe91 --- /dev/null +++ b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.html @@ -0,0 +1,8 @@ +
+ +
+ +
+
\ No newline at end of file diff --git a/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.scss b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.scss new file mode 100644 index 00000000..4b1df1dd --- /dev/null +++ b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.scss @@ -0,0 +1,10 @@ +.param-wrapper { + display: flex; + justify-content: space-between; + + label { + margin-bottom: 0; + margin-right: 1em; + line-height: 2em; + } +} \ No newline at end of file diff --git a/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts new file mode 100644 index 00000000..2e358314 --- /dev/null +++ b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts @@ -0,0 +1,27 @@ +import {Component, Input, Type} from '@angular/core'; +import {BooleanArg} from "../../models/polyalg-plan.model"; +import {ArgControl} from "../arg-control"; + +@Component({ + selector: 'app-boolean-arg', + templateUrl: './boolean-arg.component.html', + styleUrl: './boolean-arg.component.scss' +}) +export class BooleanArgComponent { + @Input() data: BooleanControl; + +} + +export class BooleanControl extends ArgControl { + constructor(name: string, public value: BooleanArg, readonly: boolean) { + super(name, readonly); + } + + getHeight(): number { + return 50; + } + + getArgComponent(): Type { + return BooleanArgComponent; + } +} \ No newline at end of file diff --git a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.html b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.html new file mode 100644 index 00000000..39d6f957 --- /dev/null +++ b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.html @@ -0,0 +1,6 @@ +
+ + +
\ No newline at end of file diff --git a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.scss b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.scss new file mode 100644 index 00000000..4b1df1dd --- /dev/null +++ b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.scss @@ -0,0 +1,10 @@ +.param-wrapper { + display: flex; + justify-content: space-between; + + label { + margin-bottom: 0; + margin-right: 1em; + line-height: 2em; + } +} \ No newline at end of file diff --git a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts new file mode 100644 index 00000000..5aaf5be7 --- /dev/null +++ b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts @@ -0,0 +1,26 @@ +import {Component, Input, Type} from '@angular/core'; +import {EntityArg} from "../../models/polyalg-plan.model"; +import {ArgControl} from "../arg-control"; + +@Component({ + selector: 'app-entity-arg', + templateUrl: './entity-arg.component.html', + styleUrl: './entity-arg.component.scss' +}) +export class EntityArgComponent { + @Input() data: EntityControl; +} + +export class EntityControl extends ArgControl { + constructor(name: string, public value: EntityArg, readonly: boolean) { + super(name, readonly); + } + + getHeight(): number { + return 40; + } + + getArgComponent(): Type { + return EntityArgComponent; + } +} diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.html b/src/app/components/polyalg/controls/list-arg/list-arg.component.html new file mode 100644 index 00000000..3c3e8e31 --- /dev/null +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.html @@ -0,0 +1,23 @@ + +
    +
  • + +
    + +
    + + + + + + + + + +

    {{child}}

    +
    +
  • +
  • + Add element +
  • +
\ No newline at end of file diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.scss b/src/app/components/polyalg/controls/list-arg/list-arg.component.scss new file mode 100644 index 00000000..a514e06f --- /dev/null +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.scss @@ -0,0 +1,4 @@ +.remove-element { + margin-top: -0.5rem; + +} \ No newline at end of file diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts new file mode 100644 index 00000000..3ab84316 --- /dev/null +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts @@ -0,0 +1,38 @@ +import {Component, Input, Type} from '@angular/core'; +import {ListArg} from "../../models/polyalg-plan.model"; +import {ArgControl} from "../arg-control"; +import {getControl} from "../arg-control-utils"; + +@Component({ + selector: 'app-list-arg', + templateUrl: './list-arg.component.html', + styleUrl: './list-arg.component.scss' +}) +export class ListArgComponent { + @Input() data: ListControl; + + +} + +export class ListControl extends ArgControl { + children: ArgControl[]; + + constructor(name: string, public value: ListArg, readonly: boolean) { + super(name, readonly); + this.children = value.args.map(arg => getControl("", arg, readonly)); + } + + getHeight(): number { + return this.children.reduce((total, child) => total + child.getHeight(), 0) + + Object.keys(this.children).length * 16; + } + + addElement() { + console.log('add child') + this.children.push(getControl("", {type: "REX", value: {rex: "abc", alias: ""}}, this.readonly)) + } + + getArgComponent(): Type { + return ListArgComponent; + } +} \ No newline at end of file diff --git a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.html b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.html new file mode 100644 index 00000000..34240de1 --- /dev/null +++ b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.html @@ -0,0 +1,16 @@ +
+ + + + + + AS + + + + +
\ No newline at end of file diff --git a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.scss b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts new file mode 100644 index 00000000..4cf2c641 --- /dev/null +++ b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts @@ -0,0 +1,31 @@ +import {Component, Input, Type} from '@angular/core'; +import {RexArg} from "../../models/polyalg-plan.model"; +import {ArgControl} from "../arg-control"; + +@Component({ + selector: 'app-rex-arg', + templateUrl: './rex-arg.component.html', + styleUrl: './rex-arg.component.scss' +}) +export class RexArgComponent { + @Input() data: RexControl; + +} + +export class RexControl extends ArgControl { + constructor(name: string, public value: RexArg, readonly: boolean) { + super(name, readonly); + } + + getHeight(): number { + return 100; + } + + trivialAlias(): boolean { + return this.value.alias === this.value.rex; + } + + getArgComponent(): Type { + return RexArgComponent; + } +} \ No newline at end of file diff --git a/src/app/components/polyalg/controls/string-arg/string-arg.component.html b/src/app/components/polyalg/controls/string-arg/string-arg.component.html new file mode 100644 index 00000000..b52386e6 --- /dev/null +++ b/src/app/components/polyalg/controls/string-arg/string-arg.component.html @@ -0,0 +1,15 @@ +
+ + + + + + AS + + + +
diff --git a/src/app/components/polyalg/controls/string-arg/string-arg.component.scss b/src/app/components/polyalg/controls/string-arg/string-arg.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/polyalg/controls/string-arg/string-arg.component.ts b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts new file mode 100644 index 00000000..337be059 --- /dev/null +++ b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts @@ -0,0 +1,28 @@ +import {Component, Input, Type} from '@angular/core'; +import {StringArg} from "../../models/polyalg-plan.model"; +import {ArgControl} from "../arg-control"; + +@Component({ + selector: 'app-string-arg', + templateUrl: './string-arg.component.html', + styleUrl: './string-arg.component.scss' +}) +export class StringArgComponent { + @Input() data: StringControl; + +} + +export class StringControl extends ArgControl { + constructor(name: string, public value: StringArg, readonly: boolean) { + super(name, readonly); + } + + getHeight(): number { + return 55; + } + + getArgComponent(): Type { + return StringArgComponent; + } + +} diff --git a/src/app/components/polyalg/custom-connection/custom-connection.component.scss b/src/app/components/polyalg/custom-connection/custom-connection.component.scss new file mode 100644 index 00000000..df331d21 --- /dev/null +++ b/src/app/components/polyalg/custom-connection/custom-connection.component.scss @@ -0,0 +1,16 @@ +@import "scss/style.scss"; + +svg { + overflow: visible !important; + position: absolute; + pointer-events: none; + width: 9999px; + height: 9999px; + + path { + fill: none; + stroke-width: 5px; + stroke: $primary; + pointer-events: auto; + } +} \ No newline at end of file diff --git a/src/app/components/polyalg/custom-connection/custom-connection.component.ts b/src/app/components/polyalg/custom-connection/custom-connection.component.ts new file mode 100644 index 00000000..ac5a5bc3 --- /dev/null +++ b/src/app/components/polyalg/custom-connection/custom-connection.component.ts @@ -0,0 +1,25 @@ +import {Component, Input} from '@angular/core'; +import {ClassicPreset} from "rete"; +import Popper from "popper.js"; +import {AlgNode} from "../algnode/alg-node.component"; +import Position = Popper.Position; + +@Component({ + selector: 'app-custom-connection', + template: ` + + + + `, + styleUrl: './custom-connection.component.scss' +}) +export class CustomConnectionComponent { + @Input() data!: ClassicPreset.Connection; + @Input() start: Position + @Input() end: Position + @Input() path: string + +} + +export class CustomConnection extends ClassicPreset.Connection { +} diff --git a/src/app/components/polyalg/custom-socket/custom-socket.component.scss b/src/app/components/polyalg/custom-socket/custom-socket.component.scss new file mode 100644 index 00000000..608d6f6b --- /dev/null +++ b/src/app/components/polyalg/custom-socket/custom-socket.component.scss @@ -0,0 +1,36 @@ +@import "scss/style.scss"; + +$socket-size: 24px; +$socket-margin: 6px; + +:host { + display: inline-block; + cursor: pointer; + border: 4px solid $dark; + border-radius: $socket-size / 2.0; + width: $socket-size; + height: $socket-size; + margin: $socket-margin; + vertical-align: middle; + background: white; + z-index: 2; + box-sizing: border-box; + + &:hover { + border-width: 4px; + border-color: $primary; + background: $primary; + } + + &.multiple { + border-color: $success; + } + + &.output { + margin-right: - $socket-size / 2; + } + + &.input { + margin-left: - $socket-size / 2; + } +} \ No newline at end of file diff --git a/src/app/components/polyalg/custom-socket/custom-socket.component.ts b/src/app/components/polyalg/custom-socket/custom-socket.component.ts new file mode 100644 index 00000000..ab853aeb --- /dev/null +++ b/src/app/components/polyalg/custom-socket/custom-socket.component.ts @@ -0,0 +1,25 @@ +import {ChangeDetectorRef, Component, HostBinding, Input, OnChanges} from '@angular/core'; + +@Component({ + selector: 'app-custom-socket', + template: ``, + styleUrl: './custom-socket.component.scss' +}) +export class CustomSocketComponent implements OnChanges { + @Input() data!: any; + @Input() rendered!: any; + + @HostBinding("title") get title() { + return this.data.name; + } + + constructor(private cdr: ChangeDetectorRef) { + this.cdr.detach(); + } + + ngOnChanges(): void { + this.cdr.detectChanges(); + requestAnimationFrame(() => this.rendered()); + } + +} diff --git a/src/app/components/polyalg/editor.ts b/src/app/components/polyalg/editor.ts deleted file mode 100644 index f9e0ff16..00000000 --- a/src/app/components/polyalg/editor.ts +++ /dev/null @@ -1,103 +0,0 @@ -import {Injector} from "@angular/core"; -import {NodeEditor, GetSchemes, ClassicPreset} from "rete"; -import {AreaPlugin, AreaExtensions} from "rete-area-plugin"; -import { - ConnectionPlugin, - Presets as ConnectionPresets -} from "rete-connection-plugin"; -import {AngularPlugin, Presets, AngularArea2D} from "rete-angular-plugin/17"; -import {PlanNode} from "./models/polyalg-plan.model"; -import {ArrangeAppliers, AutoArrangePlugin, Presets as ArrangePresets} from "rete-auto-arrange-plugin"; - -class Node extends ClassicPreset.Node { - width = 180; - height = 120; - - constructor(props, numberOfInputs: number) { - super(props); - this.height += Math.max(numberOfInputs - 1, 0) * 50; - - } - -} - -class Connection extends ClassicPreset.Connection { -} - -type Schemes = GetSchemes>; -type AreaExtra = AngularArea2D; - - -export async function createEditor(container: HTMLElement, injector: Injector, node: PlanNode) { - const socket = new ClassicPreset.Socket("socket"); - - const editor = new NodeEditor(); - const area = new AreaPlugin(container); - const connection = new ConnectionPlugin(); - const render = new AngularPlugin({injector}); - const arrange = new AutoArrangePlugin(); - - AreaExtensions.selectableNodes(area, AreaExtensions.selector(), { - accumulating: AreaExtensions.accumulateOnCtrl() - }); - - render.addPreset(Presets.classic.setup()); - - connection.addPreset(ConnectionPresets.classic.setup()); - - const applier = new ArrangeAppliers.TransitionApplier({ - duration: 250, - timingFunction: (t) => t, - async onTick() { - await AreaExtensions.zoomAt(area, editor.getNodes()); - } - }); - - arrange.addPreset(ArrangePresets.classic.setup()); - - editor.use(area); - area.use(connection); - area.use(render); - area.use(arrange); - - AreaExtensions.simpleNodesOrder(area); - - const [nodes, connections] = addNode(socket, node); - for (const n of nodes) { - await editor.addNode(n); - } - - for (const c of connections) { - await editor.addConnection(c); - } - - await arrange.layout({applier}); - - AreaExtensions.zoomAt(area, editor.getNodes()); - - - return () => area.destroy(); -} - -function addNode(socket: ClassicPreset.Socket, node: PlanNode): [Node[], Connection[]] { - const nodes = [] - const connections = [] - const algNode = new Node(node.opName, node.inputs.length); - algNode.addOutput("out", new ClassicPreset.Output(socket)); - algNode.addControl("asdf", new ClassicPreset.InputControl("text", { - readonly: true, - initial: JSON.stringify(node.arguments) - })); - - for (let i = 0; i < node.inputs.length; i++) { - const [childNodes, childConnections] = addNode(socket, node.inputs[i]); - const childNode = childNodes[childNodes.length - 1]; - nodes.push(...childNodes); - connections.push(...childConnections); - - algNode.addInput(i.toString(), new ClassicPreset.Input(socket)); - connections.push(new Connection(childNode, "out", algNode, i.toString())); - } - nodes.push(algNode); - return [nodes, connections]; -} diff --git a/src/app/components/polyalg/models/polyalg-plan.model.ts b/src/app/components/polyalg/models/polyalg-plan.model.ts index aae19871..c665fdf5 100644 --- a/src/app/components/polyalg/models/polyalg-plan.model.ts +++ b/src/app/components/polyalg/models/polyalg-plan.model.ts @@ -1,4 +1,3 @@ - export interface PlanNode { opName: string; arguments: { @@ -8,8 +7,52 @@ export interface PlanNode { defaultValue: string; } -interface PlanArgument { - type: string; - value: any; +export interface PlanArgument { + type: ParamType; + value: ArgType; +} + +export interface EntityArg { + arg: string, + namespaceId: number, + entityId: number +} + +export interface RexArg { + rex: string, + alias?: string +} + +export interface BooleanArg { + arg: boolean +} + +export interface ListArg { + innerType: ParamType, + args: PlanArgument[] } +export interface StringArg { + arg: string, + alias?: string +} + +export type ArgType = EntityArg | RexArg | ListArg | StringArg | BooleanArg; + +export type ParamType = + | "ANY" + | "INTEGER" + | "STRING" + | "BOOLEAN" + | "REX" + | "AGGREGATE" + | "LAX_AGGREGATE" + | "ENTITY" + | "JOIN_TYPE_ENUM" // TODO: handle enums dynamically based on the PolyAlgRegistry + | "SEMI_JOIN_TYPE_ENUM" + | "MODIFY_OP_ENUM" + | "DISTRIBUTION_TYPE_ENUM" + | "FIELD" + | "LIST" + | "COLLATION" + | "CORR_ID"; diff --git a/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.html b/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.html index 53ec03dc..d44999de 100644 --- a/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.html +++ b/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.html @@ -1,3 +1,9 @@ +
-
\ No newline at end of file +
+ + +
{{polyAlg}}
+
+
\ No newline at end of file diff --git a/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.ts b/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.ts index aef2d1c4..3838e337 100644 --- a/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.ts +++ b/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.ts @@ -1,15 +1,20 @@ import {AfterViewInit, Component, ElementRef, Injector, Input, ViewChild} from '@angular/core'; -import {createEditor} from "../editor"; +import {createEditor} from "../alg-editor"; import {PlanNode} from "../models/polyalg-plan.model"; +import {CardBodyComponent, CardComponent} from "@coreui/angular"; @Component({ selector: 'app-polyalg-viewer', standalone: true, - imports: [], + imports: [ + CardComponent, + CardBodyComponent + ], templateUrl: './polyalg-viewer.component.html', styleUrl: './polyalg-viewer.component.scss' }) export class PolyalgViewerComponent implements AfterViewInit{ + @Input() polyAlg: string; @Input() planObject: string; @Input() planType: "LOGICAL" | "ROUTED" | "PHYSICAL"; @ViewChild("rete") container!: ElementRef; @@ -17,7 +22,6 @@ export class PolyalgViewerComponent implements AfterViewInit{ constructor(private injector: Injector) {} initPlan() { - console.log("hi!"); } ngOnInit() { @@ -28,7 +32,7 @@ export class PolyalgViewerComponent implements AfterViewInit{ const el = this.container.nativeElement; if (el) { - createEditor(el, this.injector, JSON.parse(this.planObject) as PlanNode); + createEditor(el, this.injector, JSON.parse(this.planObject) as PlanNode, true); } } } diff --git a/src/app/containers/default-layout/default-layout.component.html b/src/app/containers/default-layout/default-layout.component.html index e0bd30c0..96f7bee5 100644 --- a/src/app/containers/default-layout/default-layout.component.html +++ b/src/app/containers/default-layout/default-layout.component.html @@ -58,6 +58,13 @@ Plan Builder +
  • + + + PolyAlgebra Editor + +
  • diff --git a/src/app/models/information-page.model.ts b/src/app/models/information-page.model.ts index 5814ab67..c1f2bf75 100644 --- a/src/app/models/information-page.model.ts +++ b/src/app/models/information-page.model.ts @@ -44,6 +44,7 @@ export interface InformationObject extends Duration { //debugger queryPlan: string; //polyalg + polyAlg: string; jsonPolyAlg: string; planType: "LOGICAL" | "ROUTED" | "PHYSICAL"; //code diff --git a/src/app/views/querying/polyalg/polyalg.component.html b/src/app/views/querying/polyalg/polyalg.component.html new file mode 100644 index 00000000..c843b783 --- /dev/null +++ b/src/app/views/querying/polyalg/polyalg.component.html @@ -0,0 +1,3 @@ +
    +
    +
    diff --git a/src/app/views/querying/polyalg/polyalg.component.scss b/src/app/views/querying/polyalg/polyalg.component.scss new file mode 100644 index 00000000..b6c65575 --- /dev/null +++ b/src/app/views/querying/polyalg/polyalg.component.scss @@ -0,0 +1,7 @@ +.rete { + min-height: 800px; + border: 2px solid lightblue; +} + +.rete-wrapper { +} \ No newline at end of file diff --git a/src/app/views/querying/polyalg/polyalg.component.ts b/src/app/views/querying/polyalg/polyalg.component.ts new file mode 100644 index 00000000..8e355d2d --- /dev/null +++ b/src/app/views/querying/polyalg/polyalg.component.ts @@ -0,0 +1,33 @@ +import {Component, ElementRef, Injector, ViewChild} from '@angular/core'; +import {createEditor} from "../../../components/polyalg/alg-editor"; +import {PlanNode} from "../../../components/polyalg/models/polyalg-plan.model"; + +@Component({ + selector: 'app-polyalg', + templateUrl: './polyalg.component.html', + styleUrl: './polyalg.component.scss' +}) +export class PolyalgComponent { + samplePlan = "{\"opName\":\"PROJECT\",\"arguments\":{\"projects\":{\"type\":\"LIST\",\"value\":{\"innerType\":\"REX\",\"args\":[{\"type\":\"REX\",\"value\":{\"rex\":\"employeeno\",\"alias\":\"employeeno\"}},{\"type\":\"REX\",\"value\":{\"rex\":\"relationshipjoy\",\"alias\":\"happiness\"}}]}}},\"inputs\":[{\"opName\":\"FILTER\",\"arguments\":{\"condition\":{\"type\":\"REX\",\"value\":{\"rex\":\"<(age0, 30)\"}},\"variables\":{\"type\":\"LIST\",\"value\":{\"innerType\":\"LIST\",\"args\":[]}}},\"inputs\":[{\"opName\":\"JOIN\",\"arguments\":{\"type\":{\"type\":\"JOIN_TYPE_ENUM\",\"value\":{\"arg\":\"INNER\",\"enum\":\"JoinAlgType\"},\"isEnum\":true},\"semiJoinDone\":{\"type\":\"BOOLEAN\",\"value\":{\"arg\":false}},\"variables\":{\"type\":\"LIST\",\"value\":{\"innerType\":\"LIST\",\"args\":[]}},\"condition\":{\"type\":\"REX\",\"value\":{\"rex\":\"=(employeeno, employeeno0)\"}}},\"inputs\":[{\"opName\":\"SCAN\",\"arguments\":{\"entity\":{\"type\":\"ENTITY\",\"value\":{\"arg\":\"public.emp\",\"namespaceId\":0,\"id\":3}}},\"inputs\":[]},{\"opName\":\"PROJECT#\",\"arguments\":{\"projects\":{\"type\":\"LIST\",\"value\":{\"innerType\":\"STRING\",\"args\":[{\"type\":\"STRING\",\"value\":{\"arg\":\"employeeno\",\"alias\":\"employeeno0\"}},{\"type\":\"STRING\",\"value\":{\"arg\":\"age\",\"alias\":\"age0\"}}]}}},\"inputs\":[{\"opName\":\"PROJECT\",\"arguments\":{\"projects\":{\"type\":\"LIST\",\"value\":{\"innerType\":\"REX\",\"args\":[{\"type\":\"REX\",\"value\":{\"rex\":\"employeeno\",\"alias\":\"employeeno\"}},{\"type\":\"REX\",\"value\":{\"rex\":\"age\",\"alias\":\"age\"}}]}}},\"inputs\":[{\"opName\":\"SCAN\",\"arguments\":{\"entity\":{\"type\":\"ENTITY\",\"value\":{\"arg\":\"public.emp\",\"namespaceId\":0,\"id\":3}}},\"inputs\":[]}]}]}]}]}]}" + + @ViewChild("rete") container!: ElementRef; + + constructor(private injector: Injector) { + } + + initPlan() { + } + + ngOnInit() { + this.initPlan(); + } + + ngAfterViewInit(): void { + const el = this.container.nativeElement; + + if (el) { + createEditor(el, this.injector, JSON.parse(this.samplePlan) as PlanNode, false); + } + } + +} diff --git a/src/app/views/querying/querying.component.html b/src/app/views/querying/querying.component.html index a84c68f2..f03ab123 100644 --- a/src/app/views/querying/querying.component.html +++ b/src/app/views/querying/querying.component.html @@ -1,6 +1,7 @@ + diff --git a/src/app/views/views.module.ts b/src/app/views/views.module.ts index f7830668..6b83e6b9 100644 --- a/src/app/views/views.module.ts +++ b/src/app/views/views.module.ts @@ -20,9 +20,7 @@ import {QueryingComponent} from './querying/querying.component'; import {NodeComponent} from './querying/algebra/node/node.component'; import {AutocompleteLibModule} from 'angular-ng-autocomplete'; import {AdaptersComponent} from './adapters/adapters.component'; -import { - RefinementOptionsComponent -} from './querying/graphical-querying/refinement-options/refinement-options.component'; +import {RefinementOptionsComponent} from './querying/graphical-querying/refinement-options/refinement-options.component'; import {AboutComponent} from './about/about.component'; import {ButtonsModule} from 'ngx-bootstrap/buttons'; import {CollapseModule} from 'ngx-bootstrap/collapse'; @@ -35,12 +33,8 @@ import {PopoverModule} from 'ngx-bootstrap/popover'; import {QueryInterfacesComponent} from './query-interfaces/query-interfaces.component'; import {EditSourceColumnsComponent} from './schema-editing/edit-source-columns/edit-source-columns.component'; import {SearchFilterPipe, ValuePipe} from '../pipes/pipes'; -import { - DocumentEditCollectionsComponent -} from './schema-editing/document-edit-collections/document-edit-collections.component'; -import { - DocumentEditCollectionComponent -} from './schema-editing/document-edit-collection/document-edit-collection.component'; +import {DocumentEditCollectionsComponent} from './schema-editing/document-edit-collections/document-edit-collections.component'; +import {DocumentEditCollectionComponent} from './schema-editing/document-edit-collection/document-edit-collection.component'; import {StatisticsColumnComponent} from './schema-editing/statistics-column/statistics-column.component'; import {GraphEditGraphComponent} from './schema-editing/graph-edit-graph/graph-edit-graph.component'; import {FileUploaderComponent} from './forms/form-generator/file-uploader/file-uploader.component'; @@ -95,6 +89,7 @@ import { } from '@coreui/angular'; import {EditEntityComponent} from './schema-editing/edit-entity/edit-entity.component'; import {TreeModule} from '@ali-hm/angular-tree-component'; +import {PolyalgComponent} from "./querying/polyalg/polyalg.component"; @NgModule({ @@ -193,6 +188,7 @@ import {TreeModule} from '@ali-hm/angular-tree-component'; FileUploaderComponent, DockerconfigComponent, EditEntityComponent, + PolyalgComponent ], exports: [] }) From 7f1f3a5c918d222059ca50db9cceb49159cf92ca Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Thu, 25 Apr 2024 11:21:55 +0200 Subject: [PATCH 03/54] Change double quotes to single quotes --- src/app/components/polyalg/alg-editor.ts | 40 +++++++------- .../polyalg/controls/arg-control-utils.ts | 48 ++++++++--------- .../polyalg/controls/arg-control.ts | 6 +-- .../boolean-arg/boolean-arg.component.ts | 6 +-- .../entity-arg/entity-arg.component.ts | 4 +- .../controls/list-arg/list-arg.component.ts | 14 ++--- .../controls/rex-arg/rex-arg.component.ts | 6 +-- .../string-arg/string-arg.component.ts | 4 +- .../custom-connection.component.ts | 12 ++--- .../polyalg/models/polyalg-plan.model.ts | 52 +++++++++---------- .../polyalg-viewer.component.ts | 10 ++-- .../querying/polyalg/polyalg.component.ts | 8 +-- 12 files changed, 105 insertions(+), 105 deletions(-) diff --git a/src/app/components/polyalg/alg-editor.ts b/src/app/components/polyalg/alg-editor.ts index 3a829104..db1a81b2 100644 --- a/src/app/components/polyalg/alg-editor.ts +++ b/src/app/components/polyalg/alg-editor.ts @@ -1,17 +1,17 @@ -import {Injector} from "@angular/core"; -import {ClassicPreset, GetSchemes, NodeEditor} from "rete"; -import {AreaExtensions, AreaPlugin} from "rete-area-plugin"; -import {ConnectionPlugin, Presets as ConnectionPresets} from "rete-connection-plugin"; -import {AngularArea2D, AngularPlugin, Presets} from "rete-angular-plugin/17"; -import {PlanNode} from "./models/polyalg-plan.model"; -import {ArrangeAppliers, AutoArrangePlugin, Presets as ArrangePresets} from "rete-auto-arrange-plugin"; -import {AlgNode, AlgNodeComponent} from "./algnode/alg-node.component"; -import {addCustomBackground} from "./algnode/background"; -import {ArgControl} from "./controls/arg-control"; -import {getControl} from "./controls/arg-control-utils"; -import {CustomSocketComponent} from "./custom-socket/custom-socket.component"; -import {CustomConnection, CustomConnectionComponent} from "./custom-connection/custom-connection.component"; -import {ReadonlyPlugin} from "rete-readonly-plugin"; +import {Injector} from '@angular/core'; +import {ClassicPreset, GetSchemes, NodeEditor} from 'rete'; +import {AreaExtensions, AreaPlugin} from 'rete-area-plugin'; +import {ConnectionPlugin, Presets as ConnectionPresets} from 'rete-connection-plugin'; +import {AngularArea2D, AngularPlugin, Presets} from 'rete-angular-plugin/17'; +import {PlanNode} from './models/polyalg-plan.model'; +import {ArrangeAppliers, AutoArrangePlugin, Presets as ArrangePresets} from 'rete-auto-arrange-plugin'; +import {AlgNode, AlgNodeComponent} from './algnode/alg-node.component'; +import {addCustomBackground} from './algnode/background'; +import {ArgControl} from './controls/arg-control'; +import {getControl} from './controls/arg-control-utils'; +import {CustomSocketComponent} from './custom-socket/custom-socket.component'; +import {CustomConnection, CustomConnectionComponent} from './custom-connection/custom-connection.component'; +import {ReadonlyPlugin} from 'rete-readonly-plugin'; type Schemes = GetSchemes>; type AreaExtra = AngularArea2D; @@ -19,7 +19,7 @@ type AreaExtra = AngularArea2D; export async function createEditor(container: HTMLElement, injector: Injector, node: PlanNode, readonly: boolean) { const readonlyPlugin = new ReadonlyPlugin(); - const socket = new ClassicPreset.Socket("socket"); + const socket = new ClassicPreset.Socket('socket'); const editor = new NodeEditor(); const area = new AreaPlugin(container); const connection = new ConnectionPlugin(); @@ -102,12 +102,12 @@ export async function createEditor(container: HTMLElement, injector: Injector, n } function addNode(socket: ClassicPreset.Socket, node: PlanNode, readonly: boolean): [AlgNode[], CustomConnection[]] { - const nodes = [] - const connections = [] + const nodes = []; + const connections = []; const algNode = new AlgNode(node.opName, node.inputs.length); - algNode.addOutput("out", new ClassicPreset.Output(socket)); + algNode.addOutput('out', new ClassicPreset.Output(socket)); - let heights = {}; + const heights = {}; for (const [key, arg] of Object.entries(node.arguments)) { const c = getControl(key, arg, readonly); heights[key] = c.getHeight(); @@ -122,7 +122,7 @@ function addNode(socket: ClassicPreset.Socket, node: PlanNode, readonly: boolean connections.push(...childConnections); algNode.addInput(i.toString(), new ClassicPreset.Input(socket)); - connections.push(new CustomConnection(childNode, "out", algNode, i.toString())); + connections.push(new CustomConnection(childNode, 'out', algNode, i.toString())); } nodes.push(algNode); return [nodes, connections]; diff --git a/src/app/components/polyalg/controls/arg-control-utils.ts b/src/app/components/polyalg/controls/arg-control-utils.ts index fb1161d8..baadc43c 100644 --- a/src/app/components/polyalg/controls/arg-control-utils.ts +++ b/src/app/components/polyalg/controls/arg-control-utils.ts @@ -1,41 +1,41 @@ -import {ArgControl} from "./arg-control"; -import {StringControl} from "./string-arg/string-arg.component"; -import {BooleanControl} from "./boolean-arg/boolean-arg.component"; -import {RexControl} from "./rex-arg/rex-arg.component"; -import {EntityControl} from "./entity-arg/entity-arg.component"; -import {ListControl} from "./list-arg/list-arg.component"; -import {BooleanArg, EntityArg, ListArg, PlanArgument, RexArg, StringArg} from "../models/polyalg-plan.model"; +import {ArgControl} from './arg-control'; +import {StringControl} from './string-arg/string-arg.component'; +import {BooleanControl} from './boolean-arg/boolean-arg.component'; +import {RexControl} from './rex-arg/rex-arg.component'; +import {EntityControl} from './entity-arg/entity-arg.component'; +import {ListControl} from './list-arg/list-arg.component'; +import {BooleanArg, EntityArg, ListArg, PlanArgument, RexArg, StringArg} from '../models/polyalg-plan.model'; export function getControl(name: string, arg: PlanArgument, readonly: boolean): ArgControl { switch (arg.type) { - case "ANY": + case 'ANY': break; - case "INTEGER": + case 'INTEGER': break; - case "STRING": + case 'STRING': return new StringControl(name, arg.value as StringArg, readonly); - case "BOOLEAN": + case 'BOOLEAN': return new BooleanControl(name, arg.value as BooleanArg, readonly); - case "REX": + case 'REX': return new RexControl(name, arg.value as RexArg, readonly); - case "AGGREGATE": + case 'AGGREGATE': break; - case "LAX_AGGREGATE": + case 'LAX_AGGREGATE': break; - case "ENTITY": + case 'ENTITY': return new EntityControl(name, arg.value as EntityArg, readonly); - case "JOIN_TYPE_ENUM": + case 'JOIN_TYPE_ENUM': break; - case "MODIFY_OP_ENUM": + case 'MODIFY_OP_ENUM': break; - case "FIELD": + case 'FIELD': break; - case "LIST": - return new ListControl(name, arg.value as ListArg, readonly) - case "COLLATION": + case 'LIST': + return new ListControl(name, arg.value as ListArg, readonly); + case 'COLLATION': break; - case "CORR_ID": + case 'CORR_ID': break; } - return new StringControl(name, {"arg": JSON.stringify(arg)}, readonly) -} \ No newline at end of file + return new StringControl(name, {'arg': JSON.stringify(arg)}, readonly); +} diff --git a/src/app/components/polyalg/controls/arg-control.ts b/src/app/components/polyalg/controls/arg-control.ts index d3d7d36d..b3b974aa 100644 --- a/src/app/components/polyalg/controls/arg-control.ts +++ b/src/app/components/polyalg/controls/arg-control.ts @@ -1,5 +1,5 @@ -import {ClassicPreset} from "rete"; -import {Type} from "@angular/core"; +import {ClassicPreset} from 'rete'; +import {Type} from '@angular/core'; export abstract class ArgControl extends ClassicPreset.Control { protected constructor(public name: string, public readonly: boolean) { @@ -9,4 +9,4 @@ export abstract class ArgControl extends ClassicPreset.Control { abstract getHeight(): number; abstract getArgComponent(): Type; -} \ No newline at end of file +} diff --git a/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts index 2e358314..27f8c33d 100644 --- a/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts +++ b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts @@ -1,6 +1,6 @@ import {Component, Input, Type} from '@angular/core'; -import {BooleanArg} from "../../models/polyalg-plan.model"; -import {ArgControl} from "../arg-control"; +import {BooleanArg} from '../../models/polyalg-plan.model'; +import {ArgControl} from '../arg-control'; @Component({ selector: 'app-boolean-arg', @@ -24,4 +24,4 @@ export class BooleanControl extends ArgControl { getArgComponent(): Type { return BooleanArgComponent; } -} \ No newline at end of file +} diff --git a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts index 5aaf5be7..1cc7a4cb 100644 --- a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts +++ b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts @@ -1,6 +1,6 @@ import {Component, Input, Type} from '@angular/core'; -import {EntityArg} from "../../models/polyalg-plan.model"; -import {ArgControl} from "../arg-control"; +import {EntityArg} from '../../models/polyalg-plan.model'; +import {ArgControl} from '../arg-control'; @Component({ selector: 'app-entity-arg', diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts index 3ab84316..0563c178 100644 --- a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts @@ -1,7 +1,7 @@ import {Component, Input, Type} from '@angular/core'; -import {ListArg} from "../../models/polyalg-plan.model"; -import {ArgControl} from "../arg-control"; -import {getControl} from "../arg-control-utils"; +import {ListArg} from '../../models/polyalg-plan.model'; +import {ArgControl} from '../arg-control'; +import {getControl} from '../arg-control-utils'; @Component({ selector: 'app-list-arg', @@ -19,7 +19,7 @@ export class ListControl extends ArgControl { constructor(name: string, public value: ListArg, readonly: boolean) { super(name, readonly); - this.children = value.args.map(arg => getControl("", arg, readonly)); + this.children = value.args.map(arg => getControl('', arg, readonly)); } getHeight(): number { @@ -28,11 +28,11 @@ export class ListControl extends ArgControl { } addElement() { - console.log('add child') - this.children.push(getControl("", {type: "REX", value: {rex: "abc", alias: ""}}, this.readonly)) + console.log('add child'); + this.children.push(getControl('', {type: 'REX', value: {rex: 'abc', alias: ''}}, this.readonly)); } getArgComponent(): Type { return ListArgComponent; } -} \ No newline at end of file +} diff --git a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts index 4cf2c641..1cf66192 100644 --- a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts +++ b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts @@ -1,6 +1,6 @@ import {Component, Input, Type} from '@angular/core'; -import {RexArg} from "../../models/polyalg-plan.model"; -import {ArgControl} from "../arg-control"; +import {RexArg} from '../../models/polyalg-plan.model'; +import {ArgControl} from '../arg-control'; @Component({ selector: 'app-rex-arg', @@ -28,4 +28,4 @@ export class RexControl extends ArgControl { getArgComponent(): Type { return RexArgComponent; } -} \ No newline at end of file +} diff --git a/src/app/components/polyalg/controls/string-arg/string-arg.component.ts b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts index 337be059..357073a2 100644 --- a/src/app/components/polyalg/controls/string-arg/string-arg.component.ts +++ b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts @@ -1,6 +1,6 @@ import {Component, Input, Type} from '@angular/core'; -import {StringArg} from "../../models/polyalg-plan.model"; -import {ArgControl} from "../arg-control"; +import {StringArg} from '../../models/polyalg-plan.model'; +import {ArgControl} from '../arg-control'; @Component({ selector: 'app-string-arg', diff --git a/src/app/components/polyalg/custom-connection/custom-connection.component.ts b/src/app/components/polyalg/custom-connection/custom-connection.component.ts index ac5a5bc3..7f5a2177 100644 --- a/src/app/components/polyalg/custom-connection/custom-connection.component.ts +++ b/src/app/components/polyalg/custom-connection/custom-connection.component.ts @@ -1,7 +1,7 @@ import {Component, Input} from '@angular/core'; -import {ClassicPreset} from "rete"; -import Popper from "popper.js"; -import {AlgNode} from "../algnode/alg-node.component"; +import {ClassicPreset} from 'rete'; +import Popper from 'popper.js'; +import {AlgNode} from '../algnode/alg-node.component'; import Position = Popper.Position; @Component({ @@ -15,9 +15,9 @@ import Position = Popper.Position; }) export class CustomConnectionComponent { @Input() data!: ClassicPreset.Connection; - @Input() start: Position - @Input() end: Position - @Input() path: string + @Input() start: Position; + @Input() end: Position; + @Input() path: string; } diff --git a/src/app/components/polyalg/models/polyalg-plan.model.ts b/src/app/components/polyalg/models/polyalg-plan.model.ts index c665fdf5..855817ea 100644 --- a/src/app/components/polyalg/models/polyalg-plan.model.ts +++ b/src/app/components/polyalg/models/polyalg-plan.model.ts @@ -13,46 +13,46 @@ export interface PlanArgument { } export interface EntityArg { - arg: string, - namespaceId: number, - entityId: number + arg: string; + namespaceId: number; + entityId: number; } export interface RexArg { - rex: string, - alias?: string + rex: string; + alias?: string; } export interface BooleanArg { - arg: boolean + arg: boolean; } export interface ListArg { - innerType: ParamType, - args: PlanArgument[] + innerType: ParamType; + args: PlanArgument[]; } export interface StringArg { - arg: string, - alias?: string + arg: string; + alias?: string; } export type ArgType = EntityArg | RexArg | ListArg | StringArg | BooleanArg; export type ParamType = - | "ANY" - | "INTEGER" - | "STRING" - | "BOOLEAN" - | "REX" - | "AGGREGATE" - | "LAX_AGGREGATE" - | "ENTITY" - | "JOIN_TYPE_ENUM" // TODO: handle enums dynamically based on the PolyAlgRegistry - | "SEMI_JOIN_TYPE_ENUM" - | "MODIFY_OP_ENUM" - | "DISTRIBUTION_TYPE_ENUM" - | "FIELD" - | "LIST" - | "COLLATION" - | "CORR_ID"; + | 'ANY' + | 'INTEGER' + | 'STRING' + | 'BOOLEAN' + | 'REX' + | 'AGGREGATE' + | 'LAX_AGGREGATE' + | 'ENTITY' + | 'JOIN_TYPE_ENUM' // TODO: handle enums dynamically based on the PolyAlgRegistry + | 'SEMI_JOIN_TYPE_ENUM' + | 'MODIFY_OP_ENUM' + | 'DISTRIBUTION_TYPE_ENUM' + | 'FIELD' + | 'LIST' + | 'COLLATION' + | 'CORR_ID'; diff --git a/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.ts b/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.ts index 3838e337..1ac6e195 100644 --- a/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.ts +++ b/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.ts @@ -1,7 +1,7 @@ import {AfterViewInit, Component, ElementRef, Injector, Input, ViewChild} from '@angular/core'; -import {createEditor} from "../alg-editor"; -import {PlanNode} from "../models/polyalg-plan.model"; -import {CardBodyComponent, CardComponent} from "@coreui/angular"; +import {createEditor} from '../alg-editor'; +import {PlanNode} from '../models/polyalg-plan.model'; +import {CardBodyComponent, CardComponent} from '@coreui/angular'; @Component({ selector: 'app-polyalg-viewer', @@ -16,8 +16,8 @@ import {CardBodyComponent, CardComponent} from "@coreui/angular"; export class PolyalgViewerComponent implements AfterViewInit{ @Input() polyAlg: string; @Input() planObject: string; - @Input() planType: "LOGICAL" | "ROUTED" | "PHYSICAL"; - @ViewChild("rete") container!: ElementRef; + @Input() planType: 'LOGICAL' | 'ROUTED' | 'PHYSICAL'; + @ViewChild('rete') container!: ElementRef; constructor(private injector: Injector) {} diff --git a/src/app/views/querying/polyalg/polyalg.component.ts b/src/app/views/querying/polyalg/polyalg.component.ts index 8e355d2d..596e0843 100644 --- a/src/app/views/querying/polyalg/polyalg.component.ts +++ b/src/app/views/querying/polyalg/polyalg.component.ts @@ -1,6 +1,6 @@ import {Component, ElementRef, Injector, ViewChild} from '@angular/core'; -import {createEditor} from "../../../components/polyalg/alg-editor"; -import {PlanNode} from "../../../components/polyalg/models/polyalg-plan.model"; +import {createEditor} from '../../../components/polyalg/alg-editor'; +import {PlanNode} from '../../../components/polyalg/models/polyalg-plan.model'; @Component({ selector: 'app-polyalg', @@ -8,9 +8,9 @@ import {PlanNode} from "../../../components/polyalg/models/polyalg-plan.model"; styleUrl: './polyalg.component.scss' }) export class PolyalgComponent { - samplePlan = "{\"opName\":\"PROJECT\",\"arguments\":{\"projects\":{\"type\":\"LIST\",\"value\":{\"innerType\":\"REX\",\"args\":[{\"type\":\"REX\",\"value\":{\"rex\":\"employeeno\",\"alias\":\"employeeno\"}},{\"type\":\"REX\",\"value\":{\"rex\":\"relationshipjoy\",\"alias\":\"happiness\"}}]}}},\"inputs\":[{\"opName\":\"FILTER\",\"arguments\":{\"condition\":{\"type\":\"REX\",\"value\":{\"rex\":\"<(age0, 30)\"}},\"variables\":{\"type\":\"LIST\",\"value\":{\"innerType\":\"LIST\",\"args\":[]}}},\"inputs\":[{\"opName\":\"JOIN\",\"arguments\":{\"type\":{\"type\":\"JOIN_TYPE_ENUM\",\"value\":{\"arg\":\"INNER\",\"enum\":\"JoinAlgType\"},\"isEnum\":true},\"semiJoinDone\":{\"type\":\"BOOLEAN\",\"value\":{\"arg\":false}},\"variables\":{\"type\":\"LIST\",\"value\":{\"innerType\":\"LIST\",\"args\":[]}},\"condition\":{\"type\":\"REX\",\"value\":{\"rex\":\"=(employeeno, employeeno0)\"}}},\"inputs\":[{\"opName\":\"SCAN\",\"arguments\":{\"entity\":{\"type\":\"ENTITY\",\"value\":{\"arg\":\"public.emp\",\"namespaceId\":0,\"id\":3}}},\"inputs\":[]},{\"opName\":\"PROJECT#\",\"arguments\":{\"projects\":{\"type\":\"LIST\",\"value\":{\"innerType\":\"STRING\",\"args\":[{\"type\":\"STRING\",\"value\":{\"arg\":\"employeeno\",\"alias\":\"employeeno0\"}},{\"type\":\"STRING\",\"value\":{\"arg\":\"age\",\"alias\":\"age0\"}}]}}},\"inputs\":[{\"opName\":\"PROJECT\",\"arguments\":{\"projects\":{\"type\":\"LIST\",\"value\":{\"innerType\":\"REX\",\"args\":[{\"type\":\"REX\",\"value\":{\"rex\":\"employeeno\",\"alias\":\"employeeno\"}},{\"type\":\"REX\",\"value\":{\"rex\":\"age\",\"alias\":\"age\"}}]}}},\"inputs\":[{\"opName\":\"SCAN\",\"arguments\":{\"entity\":{\"type\":\"ENTITY\",\"value\":{\"arg\":\"public.emp\",\"namespaceId\":0,\"id\":3}}},\"inputs\":[]}]}]}]}]}]}" + samplePlan = '{"opName":"PROJECT","arguments":{"projects":{"type":"LIST","value":{"innerType":"REX","args":[{"type":"REX","value":{"rex":"employeeno","alias":"employeeno"}},{"type":"REX","value":{"rex":"relationshipjoy","alias":"happiness"}}]}}},"inputs":[{"opName":"FILTER","arguments":{"condition":{"type":"REX","value":{"rex":"<(age0, 30)"}},"variables":{"type":"LIST","value":{"innerType":"LIST","args":[]}}},"inputs":[{"opName":"JOIN","arguments":{"type":{"type":"JOIN_TYPE_ENUM","value":{"arg":"INNER","enum":"JoinAlgType"},"isEnum":true},"semiJoinDone":{"type":"BOOLEAN","value":{"arg":false}},"variables":{"type":"LIST","value":{"innerType":"LIST","args":[]}},"condition":{"type":"REX","value":{"rex":"=(employeeno, employeeno0)"}}},"inputs":[{"opName":"SCAN","arguments":{"entity":{"type":"ENTITY","value":{"arg":"public.emp","namespaceId":0,"id":3}}},"inputs":[]},{"opName":"PROJECT#","arguments":{"projects":{"type":"LIST","value":{"innerType":"STRING","args":[{"type":"STRING","value":{"arg":"employeeno","alias":"employeeno0"}},{"type":"STRING","value":{"arg":"age","alias":"age0"}}]}}},"inputs":[{"opName":"PROJECT","arguments":{"projects":{"type":"LIST","value":{"innerType":"REX","args":[{"type":"REX","value":{"rex":"employeeno","alias":"employeeno"}},{"type":"REX","value":{"rex":"age","alias":"age"}}]}}},"inputs":[{"opName":"SCAN","arguments":{"entity":{"type":"ENTITY","value":{"arg":"public.emp","namespaceId":0,"id":3}}},"inputs":[]}]}]}]}]}]}'; - @ViewChild("rete") container!: ElementRef; + @ViewChild('rete') container!: ElementRef; constructor(private injector: Injector) { } From af9210f4130db8f0e729b7ef5806dbb8e1a51cfe Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Thu, 25 Apr 2024 18:46:29 +0200 Subject: [PATCH 04/54] Change alg-editor to vertical layout --- package-lock.json | 71 ++++++++++++++---- package.json | 6 +- src/app/components/polyalg/alg-editor.ts | 25 ++++++- .../polyalg/algnode/alg-node.component.html | 72 +++++++++---------- .../polyalg/algnode/alg-node.component.scss | 36 +++++----- .../custom-connection.component.ts | 2 +- .../custom-socket/custom-socket.component.ts | 2 +- src/app/components/polyalg/polyalg.service.ts | 11 +++ 8 files changed, 150 insertions(+), 75 deletions(-) create mode 100644 src/app/components/polyalg/polyalg.service.ts diff --git a/package-lock.json b/package-lock.json index bb2f60ec..985cee37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "bootstrap": "^5.3.1", "core-js": "^3.26.1", "d3": "^7.8.5", + "d3-shape": "^3.2.0", "dagre-d3": "^0.4.18", "eslib": "0.2.2", "flag-icons": "^6.10.0", @@ -60,6 +61,7 @@ "rete-angular-plugin": "^2.1.1", "rete-area-plugin": "^2.0.4", "rete-auto-arrange-plugin": "^2.0.1", + "rete-connection-path-plugin": "^2.0.3", "rete-connection-plugin": "^2.0.1", "rete-readonly-plugin": "^2.0.1", "rete-render-utils": "^2.0.2", @@ -80,6 +82,7 @@ "@angular/compiler-cli": "^17.2.3", "@angular/language-service": "^17.2.3", "@ngtools/webpack": "^17.2.2", + "@types/d3-shape": "^3.1.6", "@types/hammerjs": "^2.0.36", "@types/jasmine": "~3.6.0", "@types/jasminewd2": "^2.0.8", @@ -5359,6 +5362,12 @@ "@types/node": "*" } }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", + "dev": true + }, "node_modules/@types/d3-scale": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", @@ -5374,6 +5383,15 @@ "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==", "optional": true }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "dev": true, + "dependencies": { + "@types/d3-path": "*" + } + }, "node_modules/@types/d3-time": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", @@ -7970,6 +7988,15 @@ "d3-shape": "^1.2.0" } }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "optional": true, + "dependencies": { + "d3-path": "1" + } + }, "node_modules/d3-scale": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", @@ -8017,11 +8044,22 @@ } }, "node_modules/d3-shape": { - "version": "1.3.7", - "license": "BSD-3-Clause", - "optional": true, + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", "dependencies": { - "d3-path": "1" + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape/node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" } }, "node_modules/d3-time": { @@ -8117,17 +8155,6 @@ "node": ">=12" } }, - "node_modules/d3/node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/dagre": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.7.4.tgz", @@ -14705,6 +14732,20 @@ "web-worker": "^1.2.0" } }, + "node_modules/rete-connection-path-plugin": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/rete-connection-path-plugin/-/rete-connection-path-plugin-2.0.3.tgz", + "integrity": "sha512-YrRe5RYx7VURClvGxlSu51aeEED9DrT37S8V/261BKIcjNKUWS8vyLBZIR+Tr2Vwru1y88WzRlcht7hdhIM3BQ==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "peerDependencies": { + "d3-shape": "^3.0.0", + "rete": "^2.0.1", + "rete-area-plugin": "^2.0.0", + "rete-render-utils": "^2.0.0" + } + }, "node_modules/rete-connection-plugin": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/rete-connection-plugin/-/rete-connection-plugin-2.0.1.tgz", diff --git a/package.json b/package.json index 3550dc9a..451bdbb2 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,8 @@ "rete-auto-arrange-plugin": "^2.0.1", "rete-area-plugin": "^2.0.4", "rete-render-utils": "^2.0.2", - "rete-readonly-plugin": "^2.0.1" + "rete-readonly-plugin": "^2.0.1", + "rete-connection-path-plugin": "^2.0.3" }, "devDependencies": { "@angular-builders/custom-webpack": "^17.0.1", @@ -105,7 +106,8 @@ "tslint": "~6.1.0", "typescript": "^5.3.3", "webpack": "^5.54.0", - "webpack-bundle-analyzer": "^4.9.0" + "webpack-bundle-analyzer": "^4.9.0", + "@types/d3-shape": "^3.1.6" }, "engines": { "node": ">= 16.3.0", diff --git a/src/app/components/polyalg/alg-editor.ts b/src/app/components/polyalg/alg-editor.ts index db1a81b2..4cc81dfc 100644 --- a/src/app/components/polyalg/alg-editor.ts +++ b/src/app/components/polyalg/alg-editor.ts @@ -12,6 +12,8 @@ import {getControl} from './controls/arg-control-utils'; import {CustomSocketComponent} from './custom-socket/custom-socket.component'; import {CustomConnection, CustomConnectionComponent} from './custom-connection/custom-connection.component'; import {ReadonlyPlugin} from 'rete-readonly-plugin'; +import {ConnectionPathPlugin, Transformers} from 'rete-connection-path-plugin'; +import {getDOMSocketPosition} from 'rete-render-utils'; type Schemes = GetSchemes>; type AreaExtra = AngularArea2D; @@ -22,9 +24,14 @@ export async function createEditor(container: HTMLElement, injector: Injector, n const socket = new ClassicPreset.Socket('socket'); const editor = new NodeEditor(); const area = new AreaPlugin(container); - const connection = new ConnectionPlugin(); + const connection = new ConnectionPlugin; const render = new AngularPlugin({injector}); const arrange = new AutoArrangePlugin(); + const pathPlugin = new ConnectionPathPlugin({ + transformer: () => ( + (p) => Transformers.classic({vertical: true})(p.reverse()) + ) + }); AreaExtensions.selectableNodes(area, AreaExtensions.selector(), { accumulating: AreaExtensions.accumulateOnCtrl() @@ -47,7 +54,12 @@ export async function createEditor(container: HTMLElement, injector: Injector, n socket() { return CustomSocketComponent; } - } + }, + socketPositionWatcher: getDOMSocketPosition({ + offset({x, y}, nodeId, side, key) { + return {x, y}; + }, + }) })); connection.addPreset(ConnectionPresets.classic.setup()); @@ -66,6 +78,7 @@ export async function createEditor(container: HTMLElement, injector: Injector, n area.use(readonlyPlugin.area); area.use(render); area.use(arrange); + render.use(pathPlugin); AreaExtensions.simpleNodesOrder(area); addCustomBackground(area); @@ -79,7 +92,13 @@ export async function createEditor(container: HTMLElement, injector: Injector, n await editor.addConnection(c); } - await arrange.layout({applier, options: {'elk.direction': 'RIGHT', 'elk.alignment': 'RIGHT'}}); // https://github.com/retejs/rete/issues/697 + await arrange.layout({ + applier, options: { + 'algorithm': 'mrtree', + 'elk.direction': 'RIGHT', + 'elk.topdownLayout': 'true', + } + }); // https://github.com/retejs/rete/issues/697 AreaExtensions.zoomAt(area, editor.getNodes()); diff --git a/src/app/components/polyalg/algnode/alg-node.component.html b/src/app/components/polyalg/algnode/alg-node.component.html index 62e2111c..a94df4ab 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.html +++ b/src/app/components/polyalg/algnode/alg-node.component.html @@ -1,39 +1,39 @@ -
    -

    {{data.label}}

    +
    +
    +
    +
    -
    -
    {{output.value?.label}}
    -
    + +
    +

    {{ data.label }}

    -
    -
    -
    -
    {{input.value?.label}}
    -
    -
    \ No newline at end of file + +
    +
    +
    +
    +
    +
    + diff --git a/src/app/components/polyalg/algnode/alg-node.component.scss b/src/app/components/polyalg/algnode/alg-node.component.scss index 77afc39d..dcf11307 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.scss +++ b/src/app/components/polyalg/algnode/alg-node.component.scss @@ -41,35 +41,37 @@ $socket-size: 16px; } } - .output { - text-align: right; + .outputs { + display: flex; + justify-content: space-around; + margin-top: -18px; + height: 24px; + position: absolute; + top: 0; + left: 0; + width: 100%; + } + + .inputs { + display: flex; + justify-content: space-around; + margin-bottom: -18px; + height: 24px; } - .input { + .output, .input { text-align: left; + display: block; } .output-socket { text-align: right; - margin-right: -20px; display: inline-block; } .input-socket { text-align: left; - margin-left: -20px; - display: inline-block; - } - - .input-title, - .output-title { - vertical-align: middle; - color: white; display: inline-block; - font-family: sans-serif; - font-size: 14px; - margin: $socket-margin; - line-height: $socket-size; } .input-control { @@ -84,4 +86,4 @@ $socket-size: 16px; } -} \ No newline at end of file +} diff --git a/src/app/components/polyalg/custom-connection/custom-connection.component.ts b/src/app/components/polyalg/custom-connection/custom-connection.component.ts index 7f5a2177..a61ff058 100644 --- a/src/app/components/polyalg/custom-connection/custom-connection.component.ts +++ b/src/app/components/polyalg/custom-connection/custom-connection.component.ts @@ -14,7 +14,7 @@ import Position = Popper.Position; styleUrl: './custom-connection.component.scss' }) export class CustomConnectionComponent { - @Input() data!: ClassicPreset.Connection; + @Input() data!: CustomConnection; @Input() start: Position; @Input() end: Position; @Input() path: string; diff --git a/src/app/components/polyalg/custom-socket/custom-socket.component.ts b/src/app/components/polyalg/custom-socket/custom-socket.component.ts index ab853aeb..9d51f3e2 100644 --- a/src/app/components/polyalg/custom-socket/custom-socket.component.ts +++ b/src/app/components/polyalg/custom-socket/custom-socket.component.ts @@ -9,7 +9,7 @@ export class CustomSocketComponent implements OnChanges { @Input() data!: any; @Input() rendered!: any; - @HostBinding("title") get title() { + @HostBinding('title') get title() { return this.data.name; } diff --git a/src/app/components/polyalg/polyalg.service.ts b/src/app/components/polyalg/polyalg.service.ts new file mode 100644 index 00000000..d0f51550 --- /dev/null +++ b/src/app/components/polyalg/polyalg.service.ts @@ -0,0 +1,11 @@ +import {Injectable} from '@angular/core'; +import {CrudService} from '../../services/crud.service'; + +@Injectable({ + providedIn: 'root' +}) +export class PolyAlgService { + + constructor(private _crud: CrudService) { } + +} From 9331a55bf6d414ea07020b0d40301aa1ae344a9f Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Sat, 27 Apr 2024 14:38:15 +0200 Subject: [PATCH 05/54] Correctly display connections in vertical layout --- src/app/components/polyalg/alg-editor.ts | 41 ++++++++++++------- .../polyalg/algnode/alg-node.component.html | 38 +++++++++-------- .../polyalg/algnode/alg-node.component.scss | 5 ++- .../polyalg/algnode/alg-node.component.ts | 30 +++++++------- .../polyalg/controls/arg-control-utils.ts | 4 +- .../controls/list-arg/list-arg.component.html | 2 +- .../controls/list-arg/list-arg.component.ts | 11 +++-- .../querying/polyalg/polyalg.component.html | 1 + .../querying/polyalg/polyalg.component.ts | 16 +++----- 9 files changed, 82 insertions(+), 66 deletions(-) diff --git a/src/app/components/polyalg/alg-editor.ts b/src/app/components/polyalg/alg-editor.ts index 4cc81dfc..ba2d8761 100644 --- a/src/app/components/polyalg/alg-editor.ts +++ b/src/app/components/polyalg/alg-editor.ts @@ -29,7 +29,7 @@ export async function createEditor(container: HTMLElement, injector: Injector, n const arrange = new AutoArrangePlugin(); const pathPlugin = new ConnectionPathPlugin({ transformer: () => ( - (p) => Transformers.classic({vertical: true})(p.reverse()) + (p) => Transformers.classic({vertical: true})(p) ) }); @@ -83,7 +83,7 @@ export async function createEditor(container: HTMLElement, injector: Injector, n AreaExtensions.simpleNodesOrder(area); addCustomBackground(area); - const [nodes, connections] = addNode(socket, node, readonly); + const [nodes, connections] = addNode(socket, node, readonly, area); for (const n of nodes) { await editor.addNode(n); } @@ -92,12 +92,14 @@ export async function createEditor(container: HTMLElement, injector: Injector, n await editor.addConnection(c); } + const layoutOpts = { + 'elk.algorithm': 'mrtree', + 'elk.mrtree.weighting': 'CONSTRAINT', + 'elk.spacing.edgeNode': '0', + 'elk.spacing.nodeNode': '50' + }; await arrange.layout({ - applier, options: { - 'algorithm': 'mrtree', - 'elk.direction': 'RIGHT', - 'elk.topdownLayout': 'true', - } + applier, options: layoutOpts }); // https://github.com/retejs/rete/issues/697 AreaExtensions.zoomAt(area, editor.getNodes()); @@ -117,31 +119,42 @@ export async function createEditor(container: HTMLElement, injector: Injector, n } - return () => area.destroy(); + return { + layout: async () => { + await arrange.layout({ + applier, options: layoutOpts + }); + AreaExtensions.zoomAt(area, editor.getNodes()); + }, + destroy: () => area.destroy() + }; } -function addNode(socket: ClassicPreset.Socket, node: PlanNode, readonly: boolean): [AlgNode[], CustomConnection[]] { +function addNode(socket: ClassicPreset.Socket, node: PlanNode, readonly: boolean, area: AreaPlugin): [AlgNode[], CustomConnection[]] { const nodes = []; const connections = []; const algNode = new AlgNode(node.opName, node.inputs.length); - algNode.addOutput('out', new ClassicPreset.Output(socket)); + algNode.addInput('top', new ClassicPreset.Input(socket)); const heights = {}; for (const [key, arg] of Object.entries(node.arguments)) { - const c = getControl(key, arg, readonly); + const c = getControl(key, arg, readonly, (height: number) => { + algNode.updateControlHeight(key, height); + area.update('node', algNode.id).then(); + }); heights[key] = c.getHeight(); algNode.addControl(key, c); } algNode.updateControlHeights(heights); for (let i = 0; i < node.inputs.length; i++) { - const [childNodes, childConnections] = addNode(socket, node.inputs[i], readonly); + const [childNodes, childConnections] = addNode(socket, node.inputs[i], readonly, area); const childNode = childNodes[childNodes.length - 1]; nodes.push(...childNodes); connections.push(...childConnections); - algNode.addInput(i.toString(), new ClassicPreset.Input(socket)); - connections.push(new CustomConnection(childNode, 'out', algNode, i.toString())); + algNode.addOutput(i.toString(), new ClassicPreset.Output(socket)); + connections.push(new CustomConnection(algNode, i.toString(), childNode, 'top')); } nodes.push(algNode); return [nodes, connections]; diff --git a/src/app/components/polyalg/algnode/alg-node.component.html b/src/app/components/polyalg/algnode/alg-node.component.html index a94df4ab..7d6d8a7a 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.html +++ b/src/app/components/polyalg/algnode/alg-node.component.html @@ -1,22 +1,6 @@ -
    -
    -
    -
    -
    -
    -

    {{ data.label }}

    -
    -
    +
    {{ data.label }}
    +
    +

    {{ data.label }}

    +
    + +
    + +
    +
    +
    +
    +
    + diff --git a/src/app/components/polyalg/algnode/alg-node.component.scss b/src/app/components/polyalg/algnode/alg-node.component.scss index dcf11307..835f32b8 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.scss +++ b/src/app/components/polyalg/algnode/alg-node.component.scss @@ -41,7 +41,7 @@ $socket-size: 16px; } } - .outputs { + .inputs { display: flex; justify-content: space-around; margin-top: -18px; @@ -52,8 +52,9 @@ $socket-size: 16px; width: 100%; } - .inputs { + .outputs { display: flex; + flex-direction: row-reverse; // needed for non-overlapping connections when arranging nodes justify-content: space-around; margin-bottom: -18px; height: 24px; diff --git a/src/app/components/polyalg/algnode/alg-node.component.ts b/src/app/components/polyalg/algnode/alg-node.component.ts index c6a0e91f..5fcce484 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.ts +++ b/src/app/components/polyalg/algnode/alg-node.component.ts @@ -1,8 +1,8 @@ import {ChangeDetectorRef, Component, HostBinding, Input, OnChanges} from '@angular/core'; -import {ClassicPreset} from "rete"; -import {KeyValue} from "@angular/common"; +import {ClassicPreset} from 'rete'; +import {KeyValue} from '@angular/common'; -type SortValue = (N['controls'] | N['inputs'] | N['outputs'])[string] +type SortValue = (N['controls'] | N['inputs'] | N['outputs'])[string]; @Component({ selector: 'app-alg-node', @@ -11,30 +11,30 @@ type SortValue = (N['controls'] | N['inputs'] | N[ }) export class AlgNodeComponent implements OnChanges { @Input() data!: AlgNode; - @Input() emit!: (data: any) => void - @Input() rendered!: () => void + @Input() emit!: (data: any) => void; + @Input() rendered!: () => void; - seed = 0 + seed = 0; @HostBinding('class.selected') get selected() { - return this.data.selected + return this.data.selected; } constructor(private cdr: ChangeDetectorRef) { - this.cdr.detach() + this.cdr.detach(); } ngOnChanges(): void { - this.cdr.detectChanges() - requestAnimationFrame(() => this.rendered()) - this.seed++ // force render sockets + this.cdr.detectChanges(); + requestAnimationFrame(() => this.rendered()); + this.seed++; // force render sockets } sortByIndex>>(a: I, b: I) { - const ai = a.value?.index || 0 - const bi = b.value?.index || 0 + const ai = a.value?.index || 0; + const bi = b.value?.index || 0; - return ai - bi + return ai - bi; } } @@ -63,7 +63,7 @@ export class AlgNode extends ClassicPreset.Node { this.recomputeHeight(); } - recomputeHeight() { + private recomputeHeight() { const sum = Object.values(this.controlHeights).reduce((total, value) => total + value, 0); this.height = BASE_HEIGHT + this.numOfInputs * 40 + sum; } diff --git a/src/app/components/polyalg/controls/arg-control-utils.ts b/src/app/components/polyalg/controls/arg-control-utils.ts index baadc43c..adf22649 100644 --- a/src/app/components/polyalg/controls/arg-control-utils.ts +++ b/src/app/components/polyalg/controls/arg-control-utils.ts @@ -6,7 +6,7 @@ import {EntityControl} from './entity-arg/entity-arg.component'; import {ListControl} from './list-arg/list-arg.component'; import {BooleanArg, EntityArg, ListArg, PlanArgument, RexArg, StringArg} from '../models/polyalg-plan.model'; -export function getControl(name: string, arg: PlanArgument, readonly: boolean): ArgControl { +export function getControl(name: string, arg: PlanArgument, readonly: boolean, updateHeight: (height: number) => void): ArgControl { switch (arg.type) { case 'ANY': break; @@ -31,7 +31,7 @@ export function getControl(name: string, arg: PlanArgument, readonly: boolean): case 'FIELD': break; case 'LIST': - return new ListControl(name, arg.value as ListArg, readonly); + return new ListControl(name, arg.value as ListArg, readonly, updateHeight); case 'COLLATION': break; case 'CORR_ID': diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.html b/src/app/components/polyalg/controls/list-arg/list-arg.component.html index 3c3e8e31..446535d2 100644 --- a/src/app/components/polyalg/controls/list-arg/list-arg.component.html +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.html @@ -20,4 +20,4 @@
  • Add element
  • - \ No newline at end of file + diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts index 0563c178..aeab68ec 100644 --- a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts @@ -17,9 +17,9 @@ export class ListArgComponent { export class ListControl extends ArgControl { children: ArgControl[]; - constructor(name: string, public value: ListArg, readonly: boolean) { + constructor(name: string, public value: ListArg, readonly: boolean, public updateHeight: any) { super(name, readonly); - this.children = value.args.map(arg => getControl('', arg, readonly)); + this.children = value.args.map(arg => getControl('', arg, readonly, updateHeight)); } getHeight(): number { @@ -28,8 +28,11 @@ export class ListControl extends ArgControl { } addElement() { - console.log('add child'); - this.children.push(getControl('', {type: 'REX', value: {rex: 'abc', alias: ''}}, this.readonly)); + this.children.push(getControl('', { + type: 'REX', + value: {rex: 'abc', alias: ''} + }, this.readonly, this.updateHeight)); + this.updateHeight(this.getHeight()); } getArgComponent(): Type { diff --git a/src/app/views/querying/polyalg/polyalg.component.html b/src/app/views/querying/polyalg/polyalg.component.html index c843b783..74743003 100644 --- a/src/app/views/querying/polyalg/polyalg.component.html +++ b/src/app/views/querying/polyalg/polyalg.component.html @@ -1,3 +1,4 @@
    +
    diff --git a/src/app/views/querying/polyalg/polyalg.component.ts b/src/app/views/querying/polyalg/polyalg.component.ts index 596e0843..c6d98fed 100644 --- a/src/app/views/querying/polyalg/polyalg.component.ts +++ b/src/app/views/querying/polyalg/polyalg.component.ts @@ -1,4 +1,4 @@ -import {Component, ElementRef, Injector, ViewChild} from '@angular/core'; +import {AfterViewInit, Component, ElementRef, Injector, ViewChild} from '@angular/core'; import {createEditor} from '../../../components/polyalg/alg-editor'; import {PlanNode} from '../../../components/polyalg/models/polyalg-plan.model'; @@ -7,26 +7,20 @@ import {PlanNode} from '../../../components/polyalg/models/polyalg-plan.model'; templateUrl: './polyalg.component.html', styleUrl: './polyalg.component.scss' }) -export class PolyalgComponent { +export class PolyalgComponent implements AfterViewInit { samplePlan = '{"opName":"PROJECT","arguments":{"projects":{"type":"LIST","value":{"innerType":"REX","args":[{"type":"REX","value":{"rex":"employeeno","alias":"employeeno"}},{"type":"REX","value":{"rex":"relationshipjoy","alias":"happiness"}}]}}},"inputs":[{"opName":"FILTER","arguments":{"condition":{"type":"REX","value":{"rex":"<(age0, 30)"}},"variables":{"type":"LIST","value":{"innerType":"LIST","args":[]}}},"inputs":[{"opName":"JOIN","arguments":{"type":{"type":"JOIN_TYPE_ENUM","value":{"arg":"INNER","enum":"JoinAlgType"},"isEnum":true},"semiJoinDone":{"type":"BOOLEAN","value":{"arg":false}},"variables":{"type":"LIST","value":{"innerType":"LIST","args":[]}},"condition":{"type":"REX","value":{"rex":"=(employeeno, employeeno0)"}}},"inputs":[{"opName":"SCAN","arguments":{"entity":{"type":"ENTITY","value":{"arg":"public.emp","namespaceId":0,"id":3}}},"inputs":[]},{"opName":"PROJECT#","arguments":{"projects":{"type":"LIST","value":{"innerType":"STRING","args":[{"type":"STRING","value":{"arg":"employeeno","alias":"employeeno0"}},{"type":"STRING","value":{"arg":"age","alias":"age0"}}]}}},"inputs":[{"opName":"PROJECT","arguments":{"projects":{"type":"LIST","value":{"innerType":"REX","args":[{"type":"REX","value":{"rex":"employeeno","alias":"employeeno"}},{"type":"REX","value":{"rex":"age","alias":"age"}}]}}},"inputs":[{"opName":"SCAN","arguments":{"entity":{"type":"ENTITY","value":{"arg":"public.emp","namespaceId":0,"id":3}}},"inputs":[]}]}]}]}]}]}'; - + editor: { layout: () => Promise; destroy: () => void; }; @ViewChild('rete') container!: ElementRef; constructor(private injector: Injector) { } - initPlan() { - } - - ngOnInit() { - this.initPlan(); - } - ngAfterViewInit(): void { const el = this.container.nativeElement; if (el) { - createEditor(el, this.injector, JSON.parse(this.samplePlan) as PlanNode, false); + createEditor(el, this.injector, JSON.parse(this.samplePlan) as PlanNode, false) + .then(editor => this.editor = editor); } } From 48ba82610d330f88c7cb8377b6b2f9a96c3e4acb Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Mon, 29 Apr 2024 14:47:44 +0200 Subject: [PATCH 06/54] Add context menu for creating AlgNodes --- package-lock.json | 14 ++- package.json | 3 +- src/app/components/components.module.ts | 20 +++-- .../render-item/render-item.component.html | 2 +- .../polyalg/controls/arg-control-utils.ts | 78 +++++++++++++--- .../polyalg/controls/arg-control.ts | 6 +- .../boolean-arg/boolean-arg.component.html | 2 +- .../boolean-arg/boolean-arg.component.ts | 5 +- .../entity-arg/entity-arg.component.html | 2 +- .../entity-arg/entity-arg.component.ts | 27 +++--- .../controls/enum-arg/enum-arg.component.html | 13 +++ .../controls/enum-arg/enum-arg.component.scss | 0 .../controls/enum-arg/enum-arg.component.ts | 38 ++++++++ .../controls/list-arg/list-arg.component.html | 30 ++++--- .../controls/list-arg/list-arg.component.scss | 10 ++- .../controls/list-arg/list-arg.component.ts | 20 +++-- .../controls/rex-arg/rex-arg.component.html | 10 +-- .../controls/rex-arg/rex-arg.component.ts | 8 +- .../string-arg/string-arg.component.html | 8 +- .../string-arg/string-arg.component.ts | 8 +- .../polyalg/models/polyalg-plan.model.ts | 33 +++---- .../polyalg/models/polyalg-registry.ts | 56 ++++++++++++ .../{ => polyalg-viewer}/alg-editor.ts | 89 ++++++++++++++++--- ...mponent.html => alg-viewer.component.html} | 4 + .../polyalg-viewer/alg-viewer.component.scss | 4 + .../polyalg-viewer/alg-viewer.component.ts | 40 +++++++++ .../polyalg-viewer.component.scss | 3 - .../polyalg-viewer.component.ts | 38 -------- src/app/components/polyalg/polyalg.service.ts | 69 +++++++++++++- src/app/services/crud.service.ts | 41 +++------ .../querying/polyalg/polyalg.component.html | 6 +- .../querying/polyalg/polyalg.component.ts | 20 +---- src/app/views/querying/querying.component.ts | 3 +- src/scss/style.scss | 48 ++++++++++ 34 files changed, 552 insertions(+), 206 deletions(-) create mode 100644 src/app/components/polyalg/controls/enum-arg/enum-arg.component.html create mode 100644 src/app/components/polyalg/controls/enum-arg/enum-arg.component.scss create mode 100644 src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts create mode 100644 src/app/components/polyalg/models/polyalg-registry.ts rename src/app/components/polyalg/{ => polyalg-viewer}/alg-editor.ts (58%) rename src/app/components/polyalg/polyalg-viewer/{polyalg-viewer.component.html => alg-viewer.component.html} (55%) create mode 100644 src/app/components/polyalg/polyalg-viewer/alg-viewer.component.scss create mode 100644 src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts delete mode 100644 src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.scss delete mode 100644 src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.ts diff --git a/package-lock.json b/package-lock.json index 985cee37..93617980 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,6 @@ "bootstrap": "^5.3.1", "core-js": "^3.26.1", "d3": "^7.8.5", - "d3-shape": "^3.2.0", "dagre-d3": "^0.4.18", "eslib": "0.2.2", "flag-icons": "^6.10.0", @@ -63,6 +62,7 @@ "rete-auto-arrange-plugin": "^2.0.1", "rete-connection-path-plugin": "^2.0.3", "rete-connection-plugin": "^2.0.1", + "rete-context-menu-plugin": "^2.0.3", "rete-readonly-plugin": "^2.0.1", "rete-render-utils": "^2.0.2", "rxjs": "^7.8.1", @@ -14758,6 +14758,18 @@ "rete-area-plugin": "^2.0.0" } }, + "node_modules/rete-context-menu-plugin": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/rete-context-menu-plugin/-/rete-context-menu-plugin-2.0.3.tgz", + "integrity": "sha512-CxT0g7mIxiOXDiE5NIP2onupPdSps1grLPesQSk8P8RmSs07fMy6hC2BzmwREMEHEF7A2H6/37bFKpa996IPog==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "peerDependencies": { + "rete": "^2.0.1", + "rete-area-plugin": "^2.0.0" + } + }, "node_modules/rete-readonly-plugin": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/rete-readonly-plugin/-/rete-readonly-plugin-2.0.1.tgz", diff --git a/package.json b/package.json index 451bdbb2..fcb80545 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,8 @@ "rete-area-plugin": "^2.0.4", "rete-render-utils": "^2.0.2", "rete-readonly-plugin": "^2.0.1", - "rete-connection-path-plugin": "^2.0.3" + "rete-connection-path-plugin": "^2.0.3", + "rete-context-menu-plugin": "^2.0.3" }, "devDependencies": { "@angular-builders/custom-webpack": "^17.0.1", diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts index fcdc4351..19d71d12 100644 --- a/src/app/components/components.module.ts +++ b/src/app/components/components.module.ts @@ -99,17 +99,18 @@ import {ToastComponent as Toast} from './toast-exposer/toast/toast.component'; import {ReloadButtonComponent} from '../views/util/reload-button/reload-button.component'; import {ViewComponent} from './data-view/view/view.component'; import {DockerInstanceComponent} from './docker/dockerinstance/dockerinstance.component'; -import {PolyalgViewerComponent} from "./polyalg/polyalg-viewer/polyalg-viewer.component"; -import {AlgNodeComponent} from "./polyalg/algnode/alg-node.component"; -import {ReteModule} from "rete-angular-plugin/17"; -import {EntityArgComponent} from "./polyalg/controls/entity-arg/entity-arg.component"; -import {AutocompleteLibModule} from "angular-ng-autocomplete"; -import {ListArgComponent} from "./polyalg/controls/list-arg/list-arg.component"; +import {AlgViewerComponent} from './polyalg/polyalg-viewer/alg-viewer.component'; +import {AlgNodeComponent} from './polyalg/algnode/alg-node.component'; +import {ReteModule} from 'rete-angular-plugin/17'; +import {EntityArgComponent} from './polyalg/controls/entity-arg/entity-arg.component'; +import {AutocompleteLibModule} from 'angular-ng-autocomplete'; +import {ListArgComponent} from './polyalg/controls/list-arg/list-arg.component'; import {RexArgComponent} from './polyalg/controls/rex-arg/rex-arg.component'; import {StringArgComponent} from './polyalg/controls/string-arg/string-arg.component'; import {BooleanArgComponent} from './polyalg/controls/boolean-arg/boolean-arg.component'; import {CustomSocketComponent} from './polyalg/custom-socket/custom-socket.component'; import {CustomConnectionComponent} from './polyalg/custom-connection/custom-connection.component'; +import {EnumArgComponent} from './polyalg/controls/enum-arg/enum-arg.component'; //import 'hammerjs'; @@ -179,7 +180,7 @@ import {CustomConnectionComponent} from './polyalg/custom-connection/custom-conn DropdownMenuDirective, DropdownItemDirective, DropdownDividerDirective, - DropdownToggleDirective, ModalTitleDirective, FormDirective, RowDirective, DropdownComponent, FormSelectDirective, TooltipDirective, ContainerComponent, PolyalgViewerComponent, ReteModule, AutocompleteLibModule + DropdownToggleDirective, ModalTitleDirective, FormDirective, RowDirective, DropdownComponent, FormSelectDirective, TooltipDirective, ContainerComponent, ReteModule, AutocompleteLibModule ], declarations: [ BreadcrumbComponent, @@ -216,7 +217,9 @@ import {CustomConnectionComponent} from './polyalg/custom-connection/custom-conn StringArgComponent, BooleanArgComponent, CustomSocketComponent, - CustomConnectionComponent + CustomConnectionComponent, + EnumArgComponent, + AlgViewerComponent ], exports: [ BreadcrumbComponent, @@ -240,6 +243,7 @@ import {CustomConnectionComponent} from './polyalg/custom-connection/custom-conn Toast, ReloadButtonComponent, DockerInstanceComponent, + AlgViewerComponent ] }) export class ComponentsModule { diff --git a/src/app/components/information-manager/render-item/render-item.component.html b/src/app/components/information-manager/render-item/render-item.component.html index c797ecaf..8d2e7307 100644 --- a/src/app/components/information-manager/render-item/render-item.component.html +++ b/src/app/components/information-manager/render-item/render-item.component.html @@ -64,7 +64,7 @@
    - + diff --git a/src/app/components/polyalg/controls/arg-control-utils.ts b/src/app/components/polyalg/controls/arg-control-utils.ts index adf22649..99379da7 100644 --- a/src/app/components/polyalg/controls/arg-control-utils.ts +++ b/src/app/components/polyalg/controls/arg-control-utils.ts @@ -4,38 +4,92 @@ import {BooleanControl} from './boolean-arg/boolean-arg.component'; import {RexControl} from './rex-arg/rex-arg.component'; import {EntityControl} from './entity-arg/entity-arg.component'; import {ListControl} from './list-arg/list-arg.component'; -import {BooleanArg, EntityArg, ListArg, PlanArgument, RexArg, StringArg} from '../models/polyalg-plan.model'; +import {BooleanArg, EntityArg, EnumArg, ListArg, PlanArgument, RexArg, StringArg} from '../models/polyalg-plan.model'; +import {EnumControl} from './enum-arg/enum-arg.component'; +import {Parameter, ParamType} from '../models/polyalg-registry'; + +export function getControl(param: Parameter, arg: PlanArgument | null, + isReadOnly: boolean, updateHeight: (height: number) => void, isForOuter = false): ArgControl { + if (arg == null) { + arg = getInitialArg(param, isForOuter); + } + + if (arg.isEnum) { + console.log('creating new enumcontrol'); + return new EnumControl(param, arg.type, arg.value as EnumArg, isReadOnly); + } -export function getControl(name: string, arg: PlanArgument, readonly: boolean, updateHeight: (height: number) => void): ArgControl { switch (arg.type) { case 'ANY': break; case 'INTEGER': break; case 'STRING': - return new StringControl(name, arg.value as StringArg, readonly); + return new StringControl(param, arg.value as StringArg, isReadOnly); case 'BOOLEAN': - return new BooleanControl(name, arg.value as BooleanArg, readonly); + return new BooleanControl(param, arg.value as BooleanArg, isReadOnly); case 'REX': - return new RexControl(name, arg.value as RexArg, readonly); + return new RexControl(param, arg.value as RexArg, isReadOnly); case 'AGGREGATE': break; case 'LAX_AGGREGATE': break; case 'ENTITY': - return new EntityControl(name, arg.value as EntityArg, readonly); - case 'JOIN_TYPE_ENUM': - break; - case 'MODIFY_OP_ENUM': - break; + return new EntityControl(param, arg.value as EntityArg, isReadOnly); case 'FIELD': break; case 'LIST': - return new ListControl(name, arg.value as ListArg, readonly, updateHeight); + return new ListControl(param, arg.value as ListArg, isReadOnly, updateHeight); case 'COLLATION': break; case 'CORR_ID': break; } - return new StringControl(name, {'arg': JSON.stringify(arg)}, readonly); + return new StringControl(param, {'arg': JSON.stringify(arg)}, isReadOnly); +} + +export function getInitialArg(p: Parameter, isForOuter: boolean): PlanArgument { + if (p.defaultValue) { + return p.defaultValue; + } + const isListArg = p.isMultiValued && isForOuter; + return { + type: isListArg ? ParamType.LIST : p.type, + value: (function () { + if (isListArg) { + return {innerType: p.type, args: [getInitialArg(p, false)]}; + } + if (p.isEnum) { + return {arg: ''}; + } + switch (p.type) { + case ParamType.ANY: + break; + case ParamType.INTEGER: + break; + case ParamType.STRING: + return {arg: ''}; + case ParamType.BOOLEAN: + return {arg: false}; + case ParamType.REX: + return {rex: ''}; + case ParamType.AGGREGATE: + break; + case ParamType.LAX_AGGREGATE: + break; + case ParamType.ENTITY: + return {arg: ''}; + case ParamType.FIELD: + break; + case ParamType.COLLATION: + break; + case ParamType.CORR_ID: + break; + } + return null; + })(), + isEnum: p.isEnum + }; + + } diff --git a/src/app/components/polyalg/controls/arg-control.ts b/src/app/components/polyalg/controls/arg-control.ts index b3b974aa..f9d1aefa 100644 --- a/src/app/components/polyalg/controls/arg-control.ts +++ b/src/app/components/polyalg/controls/arg-control.ts @@ -1,9 +1,13 @@ import {ClassicPreset} from 'rete'; import {Type} from '@angular/core'; +import {Parameter} from '../models/polyalg-registry'; export abstract class ArgControl extends ClassicPreset.Control { - protected constructor(public name: string, public readonly: boolean) { + readonly name: string; + + protected constructor(public readonly param: Parameter, public isReadOnly: boolean, isForOuter = false) { super(); + this.name = param.isMultiValued && !isForOuter ? null : param.name; } abstract getHeight(): number; diff --git a/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.html b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.html index 941fbe91..38c3cd5c 100644 --- a/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.html +++ b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.html @@ -2,7 +2,7 @@
    \ No newline at end of file diff --git a/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts index 27f8c33d..e9820138 100644 --- a/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts +++ b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts @@ -1,6 +1,7 @@ import {Component, Input, Type} from '@angular/core'; import {BooleanArg} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; +import {Parameter} from '../../models/polyalg-registry'; @Component({ selector: 'app-boolean-arg', @@ -13,8 +14,8 @@ export class BooleanArgComponent { } export class BooleanControl extends ArgControl { - constructor(name: string, public value: BooleanArg, readonly: boolean) { - super(name, readonly); + constructor(param: Parameter, public value: BooleanArg, isReadOnly: boolean) { + super(param, isReadOnly); } getHeight(): number { diff --git a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.html b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.html index 39d6f957..0aad0e3f 100644 --- a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.html +++ b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.html @@ -1,6 +1,6 @@
    \ No newline at end of file diff --git a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts index 1cc7a4cb..ed8300f8 100644 --- a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts +++ b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts @@ -1,26 +1,27 @@ import {Component, Input, Type} from '@angular/core'; import {EntityArg} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; +import {Parameter} from '../../models/polyalg-registry'; @Component({ - selector: 'app-entity-arg', - templateUrl: './entity-arg.component.html', - styleUrl: './entity-arg.component.scss' + selector: 'app-entity-arg', + templateUrl: './entity-arg.component.html', + styleUrl: './entity-arg.component.scss' }) export class EntityArgComponent { - @Input() data: EntityControl; + @Input() data: EntityControl; } export class EntityControl extends ArgControl { - constructor(name: string, public value: EntityArg, readonly: boolean) { - super(name, readonly); - } + constructor(param: Parameter, public value: EntityArg, isReadOnly: boolean) { + super(param, isReadOnly); + } - getHeight(): number { - return 40; - } + getHeight(): number { + return 40; + } - getArgComponent(): Type { - return EntityArgComponent; - } + getArgComponent(): Type { + return EntityArgComponent; + } } diff --git a/src/app/components/polyalg/controls/enum-arg/enum-arg.component.html b/src/app/components/polyalg/controls/enum-arg/enum-arg.component.html new file mode 100644 index 00000000..2e41d056 --- /dev/null +++ b/src/app/components/polyalg/controls/enum-arg/enum-arg.component.html @@ -0,0 +1,13 @@ +
    + + + + + + +
    diff --git a/src/app/components/polyalg/controls/enum-arg/enum-arg.component.scss b/src/app/components/polyalg/controls/enum-arg/enum-arg.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts b/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts new file mode 100644 index 00000000..899c4e9e --- /dev/null +++ b/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts @@ -0,0 +1,38 @@ +import {Component, Input, OnInit, Type} from '@angular/core'; +import {ArgControl} from '../arg-control'; +import {EnumArg} from '../../models/polyalg-plan.model'; +import {PolyAlgService} from '../../polyalg.service'; +import {Parameter} from '../../models/polyalg-registry'; + +@Component({ + selector: 'app-enum-arg', + templateUrl: './enum-arg.component.html', + styleUrl: './enum-arg.component.scss' +}) +export class EnumArgComponent implements OnInit { + @Input() data: EnumControl; + choices: string[] = []; + + constructor(private _registry: PolyAlgService) { + } + + ngOnInit(): void { + this.choices = this._registry.getEnumValues(this.data.type); + } + +} + +export class EnumControl extends ArgControl { + constructor(param: Parameter, public type: string, public value: EnumArg, isReadOnly: boolean) { + super(param, isReadOnly); + } + + getHeight(): number { + return 55; + } + + getArgComponent(): Type { + return EnumArgComponent; + } + +} diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.html b/src/app/components/polyalg/controls/list-arg/list-arg.component.html index 446535d2..1021aca4 100644 --- a/src/app/components/polyalg/controls/list-arg/list-arg.component.html +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.html @@ -1,23 +1,25 @@
      -
    • +
    • +
      + + + -
      - -
      - - - + + + - - - + +

      {{child}}

      +
      +
      - -

      {{child}}

      -
      +
      + +
    • -
    • +
    • Add element
    diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.scss b/src/app/components/polyalg/controls/list-arg/list-arg.component.scss index a514e06f..a8ef1a49 100644 --- a/src/app/components/polyalg/controls/list-arg/list-arg.component.scss +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.scss @@ -1,4 +1,12 @@ .remove-element { - margin-top: -0.5rem; + //margin-top: -0.5rem; + display: none; +} +//.list-element:hover + .remove-element, .remove-element:hover { +// display: block; +//} + +.list-element-container:hover .remove-element, .remove-element:hover { + display: block; } \ No newline at end of file diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts index aeab68ec..1e4e3c4f 100644 --- a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts @@ -2,6 +2,7 @@ import {Component, Input, Type} from '@angular/core'; import {ListArg} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; import {getControl} from '../arg-control-utils'; +import {Parameter} from '../../models/polyalg-registry'; @Component({ selector: 'app-list-arg', @@ -17,9 +18,10 @@ export class ListArgComponent { export class ListControl extends ArgControl { children: ArgControl[]; - constructor(name: string, public value: ListArg, readonly: boolean, public updateHeight: any) { - super(name, readonly); - this.children = value.args.map(arg => getControl('', arg, readonly, updateHeight)); + constructor(param: Parameter, public value: ListArg, + isReadOnly: boolean, public updateHeight: (height: number) => void) { + super(param, isReadOnly, true); + this.children = value.args.map(arg => getControl(param, arg, isReadOnly, updateHeight)); } getHeight(): number { @@ -28,13 +30,17 @@ export class ListControl extends ArgControl { } addElement() { - this.children.push(getControl('', { - type: 'REX', - value: {rex: 'abc', alias: ''} - }, this.readonly, this.updateHeight)); + this.children.push(getControl(this.param, null, this.isReadOnly, this.updateHeight, false)); this.updateHeight(this.getHeight()); } + removeElement(child: ArgControl) { + const index = this.children.indexOf(child); + this.children.splice(index, 1); + this.updateHeight(this.getHeight()); + + } + getArgComponent(): Type { return ListArgComponent; } diff --git a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.html b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.html index 34240de1..27c8f986 100644 --- a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.html +++ b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.html @@ -1,14 +1,14 @@
    - + - - AS + + AS diff --git a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts index 1cf66192..033ac936 100644 --- a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts +++ b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts @@ -1,6 +1,7 @@ import {Component, Input, Type} from '@angular/core'; import {RexArg} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; +import {Parameter, ParamTag} from '../../models/polyalg-registry'; @Component({ selector: 'app-rex-arg', @@ -13,8 +14,11 @@ export class RexArgComponent { } export class RexControl extends ArgControl { - constructor(name: string, public value: RexArg, readonly: boolean) { - super(name, readonly); + readonly showAlias: boolean; + + constructor(param: Parameter, public value: RexArg, isReadOnly: boolean) { + super(param, isReadOnly); + this.showAlias = param.tags.includes(ParamTag.ALIAS); } getHeight(): number { diff --git a/src/app/components/polyalg/controls/string-arg/string-arg.component.html b/src/app/components/polyalg/controls/string-arg/string-arg.component.html index b52386e6..5237527f 100644 --- a/src/app/components/polyalg/controls/string-arg/string-arg.component.html +++ b/src/app/components/polyalg/controls/string-arg/string-arg.component.html @@ -1,14 +1,14 @@
    - + - AS + AS diff --git a/src/app/components/polyalg/controls/string-arg/string-arg.component.ts b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts index 357073a2..d01d16b5 100644 --- a/src/app/components/polyalg/controls/string-arg/string-arg.component.ts +++ b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts @@ -1,6 +1,7 @@ import {Component, Input, Type} from '@angular/core'; import {StringArg} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; +import {Parameter, ParamTag} from '../../models/polyalg-registry'; @Component({ selector: 'app-string-arg', @@ -13,8 +14,11 @@ export class StringArgComponent { } export class StringControl extends ArgControl { - constructor(name: string, public value: StringArg, readonly: boolean) { - super(name, readonly); + readonly showAlias: boolean; + + constructor(param: Parameter, public value: StringArg, isReadOnly: boolean) { + super(param, isReadOnly); + this.showAlias = param.tags.includes(ParamTag.ALIAS); } getHeight(): number { diff --git a/src/app/components/polyalg/models/polyalg-plan.model.ts b/src/app/components/polyalg/models/polyalg-plan.model.ts index 855817ea..48635cd7 100644 --- a/src/app/components/polyalg/models/polyalg-plan.model.ts +++ b/src/app/components/polyalg/models/polyalg-plan.model.ts @@ -1,3 +1,5 @@ +import {ParamType} from './polyalg-registry'; + export interface PlanNode { opName: string; arguments: { @@ -8,14 +10,15 @@ export interface PlanNode { } export interface PlanArgument { - type: ParamType; + type: ParamType; // if isEnum, then type identifies the type of enum and is not a ParamType value: ArgType; + isEnum?: boolean; } export interface EntityArg { arg: string; - namespaceId: number; - entityId: number; + namespaceId?: number; + entityId?: number; } export interface RexArg { @@ -37,22 +40,8 @@ export interface StringArg { alias?: string; } -export type ArgType = EntityArg | RexArg | ListArg | StringArg | BooleanArg; - -export type ParamType = - | 'ANY' - | 'INTEGER' - | 'STRING' - | 'BOOLEAN' - | 'REX' - | 'AGGREGATE' - | 'LAX_AGGREGATE' - | 'ENTITY' - | 'JOIN_TYPE_ENUM' // TODO: handle enums dynamically based on the PolyAlgRegistry - | 'SEMI_JOIN_TYPE_ENUM' - | 'MODIFY_OP_ENUM' - | 'DISTRIBUTION_TYPE_ENUM' - | 'FIELD' - | 'LIST' - | 'COLLATION' - | 'CORR_ID'; +export interface EnumArg { + arg: string; +} + +export type ArgType = EntityArg | RexArg | ListArg | StringArg | BooleanArg | EnumArg; diff --git a/src/app/components/polyalg/models/polyalg-registry.ts b/src/app/components/polyalg/models/polyalg-registry.ts new file mode 100644 index 00000000..d2ea1a5e --- /dev/null +++ b/src/app/components/polyalg/models/polyalg-registry.ts @@ -0,0 +1,56 @@ +import {DataModel} from '../../../models/ui-request.model'; +import {PlanArgument} from './polyalg-plan.model'; + +export interface PolyAlgRegistry { + declarations: { [key: string]: Declaration }; + enums: { [key: string]: string[] }; +} + +export interface Declaration { + name: string; + aliases: string[]; + model: DataModel; + numInputs: number; + tags: OperatorTag[]; + posParams: Parameter[]; + kwParams: Parameter[]; +} + +export interface Parameter { + name: string; + aliases: string[]; + tags: ParamTag[]; + type: ParamType; // if isEnum, then type identifies the type of enum and is not a ParamType + isEnum: boolean; + isMultiValued: boolean; + requiresAlias: boolean; + defaultValue?: PlanArgument; +} + +export enum ParamType { + ANY = 'ANY', + INTEGER = 'INTEGER', + STRING = 'STRING', + BOOLEAN = 'BOOLEAN', + REX = 'REX', + AGGREGATE = 'AGGREGATE', + LAX_AGGREGATE = 'LAX_AGGREGATE', + ENTITY = 'ENTITY', + FIELD = 'FIELD', + LIST = 'LIST', + COLLATION = 'COLLATION', + CORR_ID = 'CORR_ID' +} + +export enum OperatorTag { + LOGICAL = 'LOGICAL', + PHYSICAL = 'PHYSICAL', + ALLOCATION = 'ALLOCATION', + ADVANCED = 'ADVANCED' +} + +export enum ParamTag { + ALIAS = 'ALIAS', + ADVANCED = 'ADVANCED' +} + diff --git a/src/app/components/polyalg/alg-editor.ts b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts similarity index 58% rename from src/app/components/polyalg/alg-editor.ts rename to src/app/components/polyalg/polyalg-viewer/alg-editor.ts index ba2d8761..25d70976 100644 --- a/src/app/components/polyalg/alg-editor.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts @@ -3,22 +3,26 @@ import {ClassicPreset, GetSchemes, NodeEditor} from 'rete'; import {AreaExtensions, AreaPlugin} from 'rete-area-plugin'; import {ConnectionPlugin, Presets as ConnectionPresets} from 'rete-connection-plugin'; import {AngularArea2D, AngularPlugin, Presets} from 'rete-angular-plugin/17'; -import {PlanNode} from './models/polyalg-plan.model'; +import {PlanNode} from '../models/polyalg-plan.model'; import {ArrangeAppliers, AutoArrangePlugin, Presets as ArrangePresets} from 'rete-auto-arrange-plugin'; -import {AlgNode, AlgNodeComponent} from './algnode/alg-node.component'; -import {addCustomBackground} from './algnode/background'; -import {ArgControl} from './controls/arg-control'; -import {getControl} from './controls/arg-control-utils'; -import {CustomSocketComponent} from './custom-socket/custom-socket.component'; -import {CustomConnection, CustomConnectionComponent} from './custom-connection/custom-connection.component'; +import {AlgNode, AlgNodeComponent} from '../algnode/alg-node.component'; +import {addCustomBackground} from '../algnode/background'; +import {ArgControl} from '../controls/arg-control'; +import {getControl} from '../controls/arg-control-utils'; +import {CustomSocketComponent} from '../custom-socket/custom-socket.component'; +import {CustomConnection, CustomConnectionComponent} from '../custom-connection/custom-connection.component'; import {ReadonlyPlugin} from 'rete-readonly-plugin'; import {ConnectionPathPlugin, Transformers} from 'rete-connection-path-plugin'; import {getDOMSocketPosition} from 'rete-render-utils'; +import {ContextMenuExtra, ContextMenuPlugin, Presets as ContextMenuPresets} from 'rete-context-menu-plugin'; +import {PolyAlgService} from '../polyalg.service'; +import {Declaration} from '../models/polyalg-registry'; +import {DataModel} from '../../../models/ui-request.model'; type Schemes = GetSchemes>; -type AreaExtra = AngularArea2D; +type AreaExtra = AngularArea2D | ContextMenuExtra; -export async function createEditor(container: HTMLElement, injector: Injector, node: PlanNode, readonly: boolean) { +export async function createEditor(container: HTMLElement, injector: Injector, registry: PolyAlgService, node: PlanNode, isReadOnly: boolean) { const readonlyPlugin = new ReadonlyPlugin(); const socket = new ClassicPreset.Socket('socket'); @@ -32,6 +36,9 @@ export async function createEditor(container: HTMLElement, injector: Injector, n (p) => Transformers.classic({vertical: true})(p) ) }); + const contextMenu = new ContextMenuPlugin({ + items: getContextMenuItems(registry, socket, isReadOnly, area) + }); AreaExtensions.selectableNodes(area, AreaExtensions.selector(), { accumulating: AreaExtensions.accumulateOnCtrl() @@ -61,6 +68,7 @@ export async function createEditor(container: HTMLElement, injector: Injector, n }, }) })); + render.addPreset(Presets.contextMenu.setup({delay: 250})); connection.addPreset(ConnectionPresets.classic.setup()); @@ -78,12 +86,13 @@ export async function createEditor(container: HTMLElement, injector: Injector, n area.use(readonlyPlugin.area); area.use(render); area.use(arrange); + area.use(contextMenu); render.use(pathPlugin); AreaExtensions.simpleNodesOrder(area); addCustomBackground(area); - const [nodes, connections] = addNode(socket, node, readonly, area); + const [nodes, connections] = addNode(socket, registry, node, isReadOnly, area); for (const n of nodes) { await editor.addNode(n); } @@ -112,7 +121,7 @@ export async function createEditor(container: HTMLElement, injector: Injector, n return context })*/ - if (readonly) { + if (isReadOnly) { readonlyPlugin.enable(); // disable interaction with nodes (control interaction is deactivated separately) } else { area.use(connection); // make connections editable @@ -130,7 +139,7 @@ export async function createEditor(container: HTMLElement, injector: Injector, n }; } -function addNode(socket: ClassicPreset.Socket, node: PlanNode, readonly: boolean, area: AreaPlugin): [AlgNode[], CustomConnection[]] { +function addNode(socket: ClassicPreset.Socket, registry: PolyAlgService, node: PlanNode, isReadOnly: boolean, area: AreaPlugin): [AlgNode[], CustomConnection[]] { const nodes = []; const connections = []; const algNode = new AlgNode(node.opName, node.inputs.length); @@ -138,7 +147,8 @@ function addNode(socket: ClassicPreset.Socket, node: PlanNode, readonly: boolean const heights = {}; for (const [key, arg] of Object.entries(node.arguments)) { - const c = getControl(key, arg, readonly, (height: number) => { + const param = registry.getParameter(node.opName, key); + const c = getControl(param, arg, isReadOnly, (height: number) => { algNode.updateControlHeight(key, height); area.update('node', algNode.id).then(); }); @@ -148,7 +158,7 @@ function addNode(socket: ClassicPreset.Socket, node: PlanNode, readonly: boolean algNode.updateControlHeights(heights); for (let i = 0; i < node.inputs.length; i++) { - const [childNodes, childConnections] = addNode(socket, node.inputs[i], readonly, area); + const [childNodes, childConnections] = addNode(socket, registry, node.inputs[i], isReadOnly, area); const childNode = childNodes[childNodes.length - 1]; nodes.push(...childNodes); connections.push(...childConnections); @@ -160,4 +170,55 @@ function addNode(socket: ClassicPreset.Socket, node: PlanNode, readonly: boolean return [nodes, connections]; } +function getContextMenuItems(registry: PolyAlgService, socket: ClassicPreset.Socket, isReadOnly: boolean, area: AreaPlugin) { + const nodes = []; + for (const model of Object.keys(DataModel).map(key => DataModel[key])) { + const innerNodes = []; + for (const decl of registry.getSortedDeclarations(model)) { + innerNodes.push([ + decl.name, + () => createAlgNode(decl, socket, isReadOnly, (algNode) => area.update('node', algNode.id).then()) + ]); + } + nodes.push([model, innerNodes]); + } + + + const items = ContextMenuPresets.classic.setup(nodes); + + return (context, plugin) => { + const result = items(context, plugin); + result.searchBar = false; + return result; + }; +} + +export function createAlgNode(decl: Declaration, socket: ClassicPreset.Socket, isReadOnly: boolean, updateArea: (a: AlgNode) => void): AlgNode { + console.log(decl); + + const algNode = new AlgNode(decl.name, decl.numInputs); + algNode.addInput('top', new ClassicPreset.Input(socket)); + + const heights = {}; + for (const p of decl.posParams.concat(decl.kwParams)) { + const c = getControl(p, null, isReadOnly, (height: number) => { + algNode.updateControlHeight(p.name, height); + updateArea(algNode); + }, p.isMultiValued); + heights[p.name] = c.getHeight(); + algNode.addControl(p.name, c); + } + algNode.updateControlHeights(heights); + + if (decl.numInputs > 0) { + for (let i = 0; i < decl.numInputs; i++) { + algNode.addOutput(i.toString(), new ClassicPreset.Output(socket)); + } + } else if (decl.numInputs === -1) { + algNode.addOutput('0', new ClassicPreset.Output(socket)); + } + + return algNode; +} + diff --git a/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.html b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html similarity index 55% rename from src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.html rename to src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html index d44999de..5ec4d62c 100644 --- a/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.html +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html @@ -2,6 +2,10 @@
    +
    {{polyAlg}}
    diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.scss b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.scss new file mode 100644 index 00000000..10fbd924 --- /dev/null +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.scss @@ -0,0 +1,4 @@ +.rete { + min-height: 800px; + border: 2px solid lightblue; +} \ No newline at end of file diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts new file mode 100644 index 00000000..9e806195 --- /dev/null +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts @@ -0,0 +1,40 @@ +import {AfterViewInit, Component, computed, effect, ElementRef, Injector, Input, ViewChild} from '@angular/core'; +import {createEditor} from './alg-editor'; +import {PlanNode} from '../models/polyalg-plan.model'; +import {PolyAlgService} from '../polyalg.service'; + +@Component({ + selector: 'app-alg-viewer', + templateUrl: './alg-viewer.component.html', + styleUrl: './alg-viewer.component.scss' +}) +export class AlgViewerComponent implements AfterViewInit { + @Input() polyAlg: string; + @Input() planObject: string; + @Input() planType: 'LOGICAL' | 'ROUTED' | 'PHYSICAL'; + @Input() isReadOnly: boolean; + @ViewChild('rete') container!: ElementRef; + + editor: { layout: () => Promise; destroy: () => void; }; + showAlgEditor = computed(() => this._registry.registryLoaded()); + + constructor(private injector: Injector, private _registry: PolyAlgService) { + effect(() => { + const el = this.container.nativeElement; + + if (this.showAlgEditor() && el) { + createEditor(el, this.injector, _registry, JSON.parse(this.planObject) as PlanNode, this.isReadOnly) + .then(editor => this.editor = editor); + } + }); + } + + ngAfterViewInit(): void { + /*const el = this.container.nativeElement; + + if (el && this.showAlgEditor()) { + createEditor(el, this.injector, JSON.parse(this.planObject) as PlanNode, this.readonly) + .then(editor => this.editor = editor); + }*/ + } +} diff --git a/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.scss b/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.scss deleted file mode 100644 index 7bacefa5..00000000 --- a/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.rete { - min-height: 600px; -} \ No newline at end of file diff --git a/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.ts b/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.ts deleted file mode 100644 index 1ac6e195..00000000 --- a/src/app/components/polyalg/polyalg-viewer/polyalg-viewer.component.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {AfterViewInit, Component, ElementRef, Injector, Input, ViewChild} from '@angular/core'; -import {createEditor} from '../alg-editor'; -import {PlanNode} from '../models/polyalg-plan.model'; -import {CardBodyComponent, CardComponent} from '@coreui/angular'; - -@Component({ - selector: 'app-polyalg-viewer', - standalone: true, - imports: [ - CardComponent, - CardBodyComponent - ], - templateUrl: './polyalg-viewer.component.html', - styleUrl: './polyalg-viewer.component.scss' -}) -export class PolyalgViewerComponent implements AfterViewInit{ - @Input() polyAlg: string; - @Input() planObject: string; - @Input() planType: 'LOGICAL' | 'ROUTED' | 'PHYSICAL'; - @ViewChild('rete') container!: ElementRef; - - constructor(private injector: Injector) {} - - initPlan() { - } - - ngOnInit() { - this.initPlan(); - } - - ngAfterViewInit(): void { - const el = this.container.nativeElement; - - if (el) { - createEditor(el, this.injector, JSON.parse(this.planObject) as PlanNode, true); - } - } -} diff --git a/src/app/components/polyalg/polyalg.service.ts b/src/app/components/polyalg/polyalg.service.ts index d0f51550..2ce82a7b 100644 --- a/src/app/components/polyalg/polyalg.service.ts +++ b/src/app/components/polyalg/polyalg.service.ts @@ -1,11 +1,76 @@ -import {Injectable} from '@angular/core'; +import {Injectable, signal} from '@angular/core'; import {CrudService} from '../../services/crud.service'; +import {Declaration, Parameter, ParamTag, PolyAlgRegistry} from './models/polyalg-registry'; +import {DataModel} from '../../models/ui-request.model'; @Injectable({ providedIn: 'root' }) export class PolyAlgService { - constructor(private _crud: CrudService) { } + registryLoaded = signal(false); + private registry: PolyAlgRegistry; + private declarations: Map = new Map(); + private sortedDeclarations: Declaration[] = []; + private parameters: Map> = new Map(); + + constructor(private _crud: CrudService) { + _crud.getPolyAlgRegistry().subscribe({ + next: res => { + this.registry = res; + this.buildLookups(); + this.registryLoaded.set(true); + }, + error: err => { + console.log(err); + } + }); + } + + private buildLookups() { + console.log('in lookups with ', this.registry); + for (const opName in this.registry.declarations) { + if (this.registry.declarations.hasOwnProperty(opName)) { + const decl: Declaration = this.registry.declarations[opName]; + this.declarations.set(opName, decl); + decl.aliases.forEach(a => this.declarations.set(a, decl)); + this.sortedDeclarations.push(decl); + + const params: Map = new Map(); + for (const p of decl.posParams.concat(decl.kwParams)) { + params.set(p.name, p); + p.aliases.forEach(a => params.set(a, p)); + } + this.parameters.set(decl, params); + } + } + this.sortedDeclarations.sort((a, b) => a.name.localeCompare(b.name)); + + } + + getEnumValues(enumType: string) { + return this.registry?.enums[enumType] || []; + } + + getDeclaration(opName: string) { + return this.declarations.get(opName); + } + + getSortedDeclarations(model: DataModel = null): Declaration[] { + if (model === null) { + return this.sortedDeclarations; + } + return this.sortedDeclarations.filter(d => d.model === model); + } + + getParameter(opNameOrDecl: string | Declaration, pName: string): any { + const decl: Declaration = (typeof opNameOrDecl === 'string') ? this.getDeclaration(opNameOrDecl) : opNameOrDecl; + return this.parameters.get(decl)?.get(pName); + } + + showAlias(opName: string, pName: string) { + const p = this.getParameter(opName, pName); + return p.tags.includes(ParamTag.ALIAS); + } } diff --git a/src/app/services/crud.service.ts b/src/app/services/crud.service.ts index 17118864..b480bc1c 100644 --- a/src/app/services/crud.service.ts +++ b/src/app/services/crud.service.ts @@ -1,34 +1,10 @@ import {EventEmitter, inject, Injectable} from '@angular/core'; -import {HttpClient, HttpHeaders, HttpUrlEncodingCodec} from '@angular/common/http'; +import {HttpClient, HttpHeaders} from '@angular/common/http'; import {WebuiSettingsService} from './webui-settings.service'; -import { - EntityMeta, - IndexModel, - ModifyPartitionRequest, - PartitionFunctionModel, - PartitioningRequest, - PathAccessRequest, - PlacementFieldsModel -} from '../components/data-view/models/result-set.model'; +import {EntityMeta, IndexModel, ModifyPartitionRequest, PartitionFunctionModel, PartitioningRequest, PathAccessRequest, PlacementFieldsModel} from '../components/data-view/models/result-set.model'; import {webSocket} from 'rxjs/webSocket'; -import { - ColumnRequest, - ConstraintRequest, - DeleteRequest, - EditCollectionRequest, - EditTableRequest, - EntityRequest, - ExploreTable, - GraphRequest, - MaterializedRequest, - Method, - MonitoringRequest, - Namespace, - QueryRequest, - RelAlgRequest, - StatisticRequest -} from '../models/ui-request.model'; -import {CreateDockerResponse, AutoDockerResult, AutoDockerStatus, DockerInstanceInfo, DockerSettings, HandshakeInfo, InstancesAndAutoDocker, UpdateDockerResponse} from '../models/docker.model'; +import {ColumnRequest, ConstraintRequest, DeleteRequest, EditCollectionRequest, EditTableRequest, EntityRequest, ExploreTable, GraphRequest, MaterializedRequest, Method, MonitoringRequest, Namespace, QueryRequest, RelAlgRequest, StatisticRequest} from '../models/ui-request.model'; +import {AutoDockerResult, AutoDockerStatus, CreateDockerResponse, DockerInstanceInfo, DockerSettings, HandshakeInfo, InstancesAndAutoDocker, UpdateDockerResponse} from '../models/docker.model'; import {ForeignKey, Uml} from '../views/uml/uml.model'; import {Validators} from '@angular/forms'; import {AdapterModel} from '../views/adapters/adapter.model'; @@ -37,6 +13,7 @@ import {AlgNodeModel, Node} from '../views/querying/algebra/algebra.model'; import {WebSocket} from './webSocket'; import {Observable} from 'rxjs'; import {map} from 'rxjs/operators'; +import {PolyAlgRegistry} from '../components/polyalg/models/polyalg-registry'; @Injectable({ @@ -472,7 +449,7 @@ export class CrudService { } } else { request = new RelAlgRequest(relAlg, cache, analyzeQuery); - console.log(request) + console.log(request); } return socket.sendMessage(request); } @@ -532,7 +509,7 @@ export class CrudService { } createAdapter(adapter: AdapterModel, formdata: FormData) { - formdata.set("body", JSON.stringify(adapter)) + formdata.set('body', JSON.stringify(adapter)); return this._http.post(`${this.httpUrl}/createAdapter`, formdata); } @@ -679,6 +656,10 @@ export class CrudService { return this._http.patch(`${this.httpUrl}/docker/settings`, settings, this.httpOptions); } + getPolyAlgRegistry() { + return this._http.get(`${this.httpUrl}/getPolyAlgRegistry`); + } + getNameValidator(required: boolean = false) { if (required) { return [Validators.pattern('^[a-zA-Z_][a-zA-Z0-9_]*$'), Validators.required, Validators.max(100)]; diff --git a/src/app/views/querying/polyalg/polyalg.component.html b/src/app/views/querying/polyalg/polyalg.component.html index 74743003..2925cd07 100644 --- a/src/app/views/querying/polyalg/polyalg.component.html +++ b/src/app/views/querying/polyalg/polyalg.component.html @@ -1,4 +1,6 @@
    -
    - +
    + + + diff --git a/src/app/views/querying/polyalg/polyalg.component.ts b/src/app/views/querying/polyalg/polyalg.component.ts index c6d98fed..ca35ed29 100644 --- a/src/app/views/querying/polyalg/polyalg.component.ts +++ b/src/app/views/querying/polyalg/polyalg.component.ts @@ -1,27 +1,11 @@ -import {AfterViewInit, Component, ElementRef, Injector, ViewChild} from '@angular/core'; -import {createEditor} from '../../../components/polyalg/alg-editor'; -import {PlanNode} from '../../../components/polyalg/models/polyalg-plan.model'; +import {Component} from '@angular/core'; @Component({ selector: 'app-polyalg', templateUrl: './polyalg.component.html', styleUrl: './polyalg.component.scss' }) -export class PolyalgComponent implements AfterViewInit { +export class PolyalgComponent { samplePlan = '{"opName":"PROJECT","arguments":{"projects":{"type":"LIST","value":{"innerType":"REX","args":[{"type":"REX","value":{"rex":"employeeno","alias":"employeeno"}},{"type":"REX","value":{"rex":"relationshipjoy","alias":"happiness"}}]}}},"inputs":[{"opName":"FILTER","arguments":{"condition":{"type":"REX","value":{"rex":"<(age0, 30)"}},"variables":{"type":"LIST","value":{"innerType":"LIST","args":[]}}},"inputs":[{"opName":"JOIN","arguments":{"type":{"type":"JOIN_TYPE_ENUM","value":{"arg":"INNER","enum":"JoinAlgType"},"isEnum":true},"semiJoinDone":{"type":"BOOLEAN","value":{"arg":false}},"variables":{"type":"LIST","value":{"innerType":"LIST","args":[]}},"condition":{"type":"REX","value":{"rex":"=(employeeno, employeeno0)"}}},"inputs":[{"opName":"SCAN","arguments":{"entity":{"type":"ENTITY","value":{"arg":"public.emp","namespaceId":0,"id":3}}},"inputs":[]},{"opName":"PROJECT#","arguments":{"projects":{"type":"LIST","value":{"innerType":"STRING","args":[{"type":"STRING","value":{"arg":"employeeno","alias":"employeeno0"}},{"type":"STRING","value":{"arg":"age","alias":"age0"}}]}}},"inputs":[{"opName":"PROJECT","arguments":{"projects":{"type":"LIST","value":{"innerType":"REX","args":[{"type":"REX","value":{"rex":"employeeno","alias":"employeeno"}},{"type":"REX","value":{"rex":"age","alias":"age"}}]}}},"inputs":[{"opName":"SCAN","arguments":{"entity":{"type":"ENTITY","value":{"arg":"public.emp","namespaceId":0,"id":3}}},"inputs":[]}]}]}]}]}]}'; - editor: { layout: () => Promise; destroy: () => void; }; - @ViewChild('rete') container!: ElementRef; - - constructor(private injector: Injector) { - } - - ngAfterViewInit(): void { - const el = this.container.nativeElement; - - if (el) { - createEditor(el, this.injector, JSON.parse(this.samplePlan) as PlanNode, false) - .then(editor => this.editor = editor); - } - } } diff --git a/src/app/views/querying/querying.component.ts b/src/app/views/querying/querying.component.ts index 8ea7bddd..b081dbc8 100644 --- a/src/app/views/querying/querying.component.ts +++ b/src/app/views/querying/querying.component.ts @@ -1,5 +1,6 @@ import {Component, inject, OnInit} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; +import {PolyAlgService} from '../../components/polyalg/polyalg.service'; @Component({ selector: 'app-querying', @@ -12,7 +13,7 @@ export class QueryingComponent implements OnInit { public route = 'console'; - constructor() { + constructor(private _registry: PolyAlgService) { } ngOnInit() { diff --git a/src/scss/style.scss b/src/scss/style.scss index 85d8aa41..7a18eb46 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -79,3 +79,51 @@ transform: rotate(0deg); } } + + +[rete-context-menu] { + // TODO: adjust style of context menu + width: 320px !important; + border-radius: 4px; + + .block { + + background: $dark !important; + //color: black !important; + //border: 4px solid $dark; + //border-radius: 4px; + border: none !important; + } + + .block.item { + border: none; + font-weight: 600; + font-family: "PathWay", sans-serif; + } + + .block.item:hover { + background: cyan; + color: orange; + } + + .block:first-child input { + background: blue; + border: none; + } + + .block:first-child input:focus { + background: lightseagreen; + border: none; + } + + .block:first-child:hover { + background: green; + border: none; + } + + .search { + color: purple !important; + font-weight: 600; + font-family: "PathWay", sans-serif; + } +} \ No newline at end of file From cc31ad4c88f47f91155c017d95a0428383ba7aee Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Tue, 30 Apr 2024 11:27:29 +0200 Subject: [PATCH 07/54] Make AlgNodes cloneable --- package-lock.json | 12 +++ package.json | 3 +- .../polyalg/algnode/alg-node.component.ts | 45 ++++++++- .../polyalg/controls/arg-control-utils.ts | 1 - .../polyalg/polyalg-viewer/alg-editor.ts | 94 ++++++++----------- src/app/components/polyalg/polyalg.service.ts | 1 - src/scss/style.scss | 14 ++- 7 files changed, 104 insertions(+), 66 deletions(-) diff --git a/package-lock.json b/package-lock.json index 93617980..60c958fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,6 +63,7 @@ "rete-connection-path-plugin": "^2.0.3", "rete-connection-plugin": "^2.0.1", "rete-context-menu-plugin": "^2.0.3", + "rete-engine": "^2.0.1", "rete-readonly-plugin": "^2.0.1", "rete-render-utils": "^2.0.2", "rxjs": "^7.8.1", @@ -14770,6 +14771,17 @@ "rete-area-plugin": "^2.0.0" } }, + "node_modules/rete-engine": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/rete-engine/-/rete-engine-2.0.1.tgz", + "integrity": "sha512-htwfb1oMTv/TsEfAzJEkRPCnjfUJYibcERqWz3AIcPdJYeD9Sw7v/Ta8YZXOls7LePMO+nc5b+3HUTHPMitPew==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "peerDependencies": { + "rete": "^2.0.1" + } + }, "node_modules/rete-readonly-plugin": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/rete-readonly-plugin/-/rete-readonly-plugin-2.0.1.tgz", diff --git a/package.json b/package.json index fcb80545..dfc71054 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,8 @@ "rete-render-utils": "^2.0.2", "rete-readonly-plugin": "^2.0.1", "rete-connection-path-plugin": "^2.0.3", - "rete-context-menu-plugin": "^2.0.3" + "rete-context-menu-plugin": "^2.0.3", + "rete-engine": "^2.0.1" }, "devDependencies": { "@angular-builders/custom-webpack": "^17.0.1", diff --git a/src/app/components/polyalg/algnode/alg-node.component.ts b/src/app/components/polyalg/algnode/alg-node.component.ts index 5fcce484..9d97c07e 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.ts +++ b/src/app/components/polyalg/algnode/alg-node.component.ts @@ -1,6 +1,10 @@ import {ChangeDetectorRef, Component, HostBinding, Input, OnChanges} from '@angular/core'; import {ClassicPreset} from 'rete'; import {KeyValue} from '@angular/common'; +import {SOCKET_PRESET} from '../polyalg-viewer/alg-editor'; +import {Declaration} from '../models/polyalg-registry'; +import {PlanArgument} from '../models/polyalg-plan.model'; +import {getControl} from '../controls/arg-control-utils'; type SortValue = (N['controls'] | N['inputs'] | N['outputs'])[string]; @@ -47,10 +51,33 @@ export class AlgNode extends ClassicPreset.Node { controlHeights: { [key: string]: number } = {}; numOfInputs = 0; - constructor(props, numberOfInputs: number) { - super(props); - this.numOfInputs = numberOfInputs; - this.recomputeHeight(); + constructor(public decl: Declaration, args: { [key: string]: PlanArgument } | null, private isReadOnly: boolean, private updateArea: (a: AlgNode) => void) { + super(decl.name); + this.numOfInputs = decl.numInputs; + + + this.addInput('top', new ClassicPreset.Input(SOCKET_PRESET)); + + const heights = {}; + for (const p of decl.posParams.concat(decl.kwParams)) { + const arg = args?.[p.name]; + const c = getControl(p, arg, isReadOnly, (height: number) => { + this.updateControlHeight(p.name, height); + updateArea(this); + }, p.isMultiValued); + heights[p.name] = c.getHeight(); + this.addControl(p.name, c); + } + this.updateControlHeights(heights); + + if (decl.numInputs > 0) { + for (let i = 0; i < decl.numInputs; i++) { + this.addOutput(i.toString(), new ClassicPreset.Output(SOCKET_PRESET)); + } + } else if (decl.numInputs === -1) { + // TODO: handle variable number of inputs + this.addOutput('0', new ClassicPreset.Output(SOCKET_PRESET)); + } } updateControlHeights(heights: { [key: string]: number; }) { @@ -68,4 +95,14 @@ export class AlgNode extends ClassicPreset.Node { this.height = BASE_HEIGHT + this.numOfInputs * 40 + sum; } + data(inputs: any = {}) { + + return {'0': {name: this.label, inputs: inputs}}; + } + + clone() { + // TODO: infer args from controls of this node + return new AlgNode(this.decl, null, this.isReadOnly, this.updateArea); + } + } diff --git a/src/app/components/polyalg/controls/arg-control-utils.ts b/src/app/components/polyalg/controls/arg-control-utils.ts index 99379da7..1ff5c82b 100644 --- a/src/app/components/polyalg/controls/arg-control-utils.ts +++ b/src/app/components/polyalg/controls/arg-control-utils.ts @@ -15,7 +15,6 @@ export function getControl(param: Parameter, arg: PlanArgument | null, } if (arg.isEnum) { - console.log('creating new enumcontrol'); return new EnumControl(param, arg.type, arg.value as EnumArg, isReadOnly); } diff --git a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts index 25d70976..7cd8f86d 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts @@ -1,6 +1,6 @@ import {Injector} from '@angular/core'; import {ClassicPreset, GetSchemes, NodeEditor} from 'rete'; -import {AreaExtensions, AreaPlugin} from 'rete-area-plugin'; +import {AreaExtensions, AreaPlugin, BaseAreaPlugin} from 'rete-area-plugin'; import {ConnectionPlugin, Presets as ConnectionPresets} from 'rete-connection-plugin'; import {AngularArea2D, AngularPlugin, Presets} from 'rete-angular-plugin/17'; import {PlanNode} from '../models/polyalg-plan.model'; @@ -8,7 +8,6 @@ import {ArrangeAppliers, AutoArrangePlugin, Presets as ArrangePresets} from 'ret import {AlgNode, AlgNodeComponent} from '../algnode/alg-node.component'; import {addCustomBackground} from '../algnode/background'; import {ArgControl} from '../controls/arg-control'; -import {getControl} from '../controls/arg-control-utils'; import {CustomSocketComponent} from '../custom-socket/custom-socket.component'; import {CustomConnection, CustomConnectionComponent} from '../custom-connection/custom-connection.component'; import {ReadonlyPlugin} from 'rete-readonly-plugin'; @@ -16,20 +15,23 @@ import {ConnectionPathPlugin, Transformers} from 'rete-connection-path-plugin'; import {getDOMSocketPosition} from 'rete-render-utils'; import {ContextMenuExtra, ContextMenuPlugin, Presets as ContextMenuPresets} from 'rete-context-menu-plugin'; import {PolyAlgService} from '../polyalg.service'; -import {Declaration} from '../models/polyalg-registry'; import {DataModel} from '../../../models/ui-request.model'; +import {DataflowEngine} from 'rete-engine'; type Schemes = GetSchemes>; type AreaExtra = AngularArea2D | ContextMenuExtra; +export const SOCKET_PRESET = new ClassicPreset.Socket('socket'); + export async function createEditor(container: HTMLElement, injector: Injector, registry: PolyAlgService, node: PlanNode, isReadOnly: boolean) { const readonlyPlugin = new ReadonlyPlugin(); - const socket = new ClassicPreset.Socket('socket'); + //const socket = new ClassicPreset.Socket('socket'); const editor = new NodeEditor(); const area = new AreaPlugin(container); const connection = new ConnectionPlugin; const render = new AngularPlugin({injector}); + const engine = new DataflowEngine(); const arrange = new AutoArrangePlugin(); const pathPlugin = new ConnectionPathPlugin({ transformer: () => ( @@ -37,14 +39,14 @@ export async function createEditor(container: HTMLElement, injector: Injector, r ) }); const contextMenu = new ContextMenuPlugin({ - items: getContextMenuItems(registry, socket, isReadOnly, area) + items: getContextMenuItems(registry, isReadOnly, area) }); AreaExtensions.selectableNodes(area, AreaExtensions.selector(), { accumulating: AreaExtensions.accumulateOnCtrl() }); - render.addPreset(Presets.classic.setup({ + render.addPreset(Presets.classic.setup>({ customize: { node() { return AlgNodeComponent; @@ -68,7 +70,7 @@ export async function createEditor(container: HTMLElement, injector: Injector, r }, }) })); - render.addPreset(Presets.contextMenu.setup({delay: 250})); + render.addPreset(Presets.contextMenu.setup({delay: 100})); connection.addPreset(ConnectionPresets.classic.setup()); @@ -83,6 +85,7 @@ export async function createEditor(container: HTMLElement, injector: Injector, r editor.use(readonlyPlugin.root); editor.use(area); + editor.use(engine); area.use(readonlyPlugin.area); area.use(render); area.use(arrange); @@ -92,7 +95,7 @@ export async function createEditor(container: HTMLElement, injector: Injector, r AreaExtensions.simpleNodesOrder(area); addCustomBackground(area); - const [nodes, connections] = addNode(socket, registry, node, isReadOnly, area); + const [nodes, connections] = addNode(registry, node, isReadOnly, area); for (const n of nodes) { await editor.addNode(n); } @@ -127,6 +130,9 @@ export async function createEditor(container: HTMLElement, injector: Injector, r area.use(connection); // make connections editable } + //const result = await engine.fetch(nodes[nodes.length - 1].id); + //console.log('result', result); + return { layout: async () => { @@ -139,45 +145,34 @@ export async function createEditor(container: HTMLElement, injector: Injector, r }; } -function addNode(socket: ClassicPreset.Socket, registry: PolyAlgService, node: PlanNode, isReadOnly: boolean, area: AreaPlugin): [AlgNode[], CustomConnection[]] { +function addNode(registry: PolyAlgService, node: PlanNode, isReadOnly: boolean, area: AreaPlugin): [AlgNode[], CustomConnection[]] { const nodes = []; const connections = []; - const algNode = new AlgNode(node.opName, node.inputs.length); - algNode.addInput('top', new ClassicPreset.Input(socket)); - - const heights = {}; - for (const [key, arg] of Object.entries(node.arguments)) { - const param = registry.getParameter(node.opName, key); - const c = getControl(param, arg, isReadOnly, (height: number) => { - algNode.updateControlHeight(key, height); - area.update('node', algNode.id).then(); - }); - heights[key] = c.getHeight(); - algNode.addControl(key, c); - } - algNode.updateControlHeights(heights); + const algNode = new AlgNode(registry.getDeclaration(node.opName), node.arguments, isReadOnly, + (a: AlgNode) => area.update('node', a.id).then()); for (let i = 0; i < node.inputs.length; i++) { - const [childNodes, childConnections] = addNode(socket, registry, node.inputs[i], isReadOnly, area); + const [childNodes, childConnections] = addNode(registry, node.inputs[i], isReadOnly, area); const childNode = childNodes[childNodes.length - 1]; nodes.push(...childNodes); connections.push(...childConnections); - algNode.addOutput(i.toString(), new ClassicPreset.Output(socket)); + //algNode.addOutput(i.toString(), new ClassicPreset.Output(SOCKET_PRESET)); connections.push(new CustomConnection(algNode, i.toString(), childNode, 'top')); } nodes.push(algNode); return [nodes, connections]; } -function getContextMenuItems(registry: PolyAlgService, socket: ClassicPreset.Socket, isReadOnly: boolean, area: AreaPlugin) { +function getContextMenuItems(registry: PolyAlgService, isReadOnly: boolean, area: AreaPlugin) { const nodes = []; for (const model of Object.keys(DataModel).map(key => DataModel[key])) { const innerNodes = []; for (const decl of registry.getSortedDeclarations(model)) { innerNodes.push([ decl.name, - () => createAlgNode(decl, socket, isReadOnly, (algNode) => area.update('node', algNode.id).then()) + () => new AlgNode(decl, null, isReadOnly, + (algNode) => area.update('node', algNode.id).then()) ]); } nodes.push([model, innerNodes]); @@ -186,39 +181,26 @@ function getContextMenuItems(registry: PolyAlgService, socket: ClassicPreset.Soc const items = ContextMenuPresets.classic.setup(nodes); - return (context, plugin) => { + // adjust classic preset to hide the search bar and enable cloning (clone handler of the preset is broken) + return (context: any, plugin: any) => { const result = items(context, plugin); result.searchBar = false; - return result; - }; -} - -export function createAlgNode(decl: Declaration, socket: ClassicPreset.Socket, isReadOnly: boolean, updateArea: (a: AlgNode) => void): AlgNode { - console.log(decl); - - const algNode = new AlgNode(decl.name, decl.numInputs); - algNode.addInput('top', new ClassicPreset.Input(socket)); - const heights = {}; - for (const p of decl.posParams.concat(decl.kwParams)) { - const c = getControl(p, null, isReadOnly, (height: number) => { - algNode.updateControlHeight(p.name, height); - updateArea(algNode); - }, p.isMultiValued); - heights[p.name] = c.getHeight(); - algNode.addControl(p.name, c); - } - algNode.updateControlHeights(heights); - - if (decl.numInputs > 0) { - for (let i = 0; i < decl.numInputs; i++) { - algNode.addOutput(i.toString(), new ClassicPreset.Output(socket)); + if (result.list[result.list.length - 1].key === 'clone') { + const area = plugin.parentScope(BaseAreaPlugin); + const editor = area.parentScope(NodeEditor); + result.list[result.list.length - 1] = { + label: 'Clone', + key: 'clone', + async handler() { + const node = context.clone(context); + await editor.addNode(node); + area.translate(node.id, area.area.pointer); + } + }; } - } else if (decl.numInputs === -1) { - algNode.addOutput('0', new ClassicPreset.Output(socket)); - } - - return algNode; + return result; + }; } diff --git a/src/app/components/polyalg/polyalg.service.ts b/src/app/components/polyalg/polyalg.service.ts index 2ce82a7b..4bf4023f 100644 --- a/src/app/components/polyalg/polyalg.service.ts +++ b/src/app/components/polyalg/polyalg.service.ts @@ -29,7 +29,6 @@ export class PolyAlgService { } private buildLookups() { - console.log('in lookups with ', this.registry); for (const opName in this.registry.declarations) { if (this.registry.declarations.hasOwnProperty(opName)) { const decl: Declaration = this.registry.declarations[opName]; diff --git a/src/scss/style.scss b/src/scss/style.scss index 7a18eb46..b59c6921 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -90,15 +90,23 @@ background: $dark !important; //color: black !important; - //border: 4px solid $dark; + border-bottom: 1px solid white !important; //border-radius: 4px; + //border: none !important; + } + + .block:hover { + background: $secondary !important; + } + + .block:last-child { border: none !important; } .block.item { border: none; font-weight: 600; - font-family: "PathWay", sans-serif; + //font-family: "PathWay", sans-serif; } .block.item:hover { @@ -124,6 +132,6 @@ .search { color: purple !important; font-weight: 600; - font-family: "PathWay", sans-serif; + //font-family: "PathWay", sans-serif; } } \ No newline at end of file From bfd3fadad866e80c42b8242c55ed0ea18d8e9951 Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Tue, 30 Apr 2024 15:21:52 +0200 Subject: [PATCH 08/54] Generate PolyAlg from nodes using Dataflow --- .../polyalg/algnode/alg-node.component.html | 37 +++--- .../polyalg/algnode/alg-node.component.scss | 7 +- .../polyalg/algnode/alg-node.component.ts | 42 +++++-- .../polyalg/controls/arg-control.ts | 5 + .../boolean-arg/boolean-arg.component.html | 2 +- .../boolean-arg/boolean-arg.component.ts | 12 +- .../entity-arg/entity-arg.component.ts | 12 +- .../controls/enum-arg/enum-arg.component.ts | 9 +- .../controls/list-arg/list-arg.component.ts | 20 +++- .../controls/rex-arg/rex-arg.component.ts | 18 ++- .../string-arg/string-arg.component.ts | 18 ++- .../polyalg/models/polyalg-plan.model.ts | 6 +- .../polyalg/models/polyalg-registry.ts | 2 + .../polyalg/polyalg-viewer/alg-editor.ts | 106 ++++++++++++++---- .../polyalg-viewer/alg-viewer.component.html | 23 +++- .../polyalg-viewer/alg-viewer.component.ts | 15 ++- .../querying/polyalg/polyalg.component.ts | 12 +- 17 files changed, 270 insertions(+), 76 deletions(-) diff --git a/src/app/components/polyalg/algnode/alg-node.component.html b/src/app/components/polyalg/algnode/alg-node.component.html index 7d6d8a7a..f1c95f8f 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.html +++ b/src/app/components/polyalg/algnode/alg-node.component.html @@ -1,5 +1,22 @@ +
    +
    +
    +
    +
    +
    +

    {{ data.label }}

    +
    +
    @@ -21,23 +38,3 @@
    -
    -

    {{ data.label }}

    -
    - -
    - -
    -
    -
    -
    -
    - diff --git a/src/app/components/polyalg/algnode/alg-node.component.scss b/src/app/components/polyalg/algnode/alg-node.component.scss index 835f32b8..e4f5998c 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.scss +++ b/src/app/components/polyalg/algnode/alg-node.component.scss @@ -41,7 +41,7 @@ $socket-size: 16px; } } - .inputs { + .outputs { display: flex; justify-content: space-around; margin-top: -18px; @@ -52,11 +52,10 @@ $socket-size: 16px; width: 100%; } - .outputs { + .inputs { display: flex; - flex-direction: row-reverse; // needed for non-overlapping connections when arranging nodes justify-content: space-around; - margin-bottom: -18px; + margin-bottom: -14px; height: 24px; } diff --git a/src/app/components/polyalg/algnode/alg-node.component.ts b/src/app/components/polyalg/algnode/alg-node.component.ts index 9d97c07e..c040027d 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.ts +++ b/src/app/components/polyalg/algnode/alg-node.component.ts @@ -5,6 +5,7 @@ import {SOCKET_PRESET} from '../polyalg-viewer/alg-editor'; import {Declaration} from '../models/polyalg-registry'; import {PlanArgument} from '../models/polyalg-plan.model'; import {getControl} from '../controls/arg-control-utils'; +import {ArgControl} from '../controls/arg-control'; type SortValue = (N['controls'] | N['inputs'] | N['outputs'])[string]; @@ -56,7 +57,7 @@ export class AlgNode extends ClassicPreset.Node { this.numOfInputs = decl.numInputs; - this.addInput('top', new ClassicPreset.Input(SOCKET_PRESET)); + this.addOutput('out', new ClassicPreset.Output(SOCKET_PRESET)); const heights = {}; for (const p of decl.posParams.concat(decl.kwParams)) { @@ -72,11 +73,11 @@ export class AlgNode extends ClassicPreset.Node { if (decl.numInputs > 0) { for (let i = 0; i < decl.numInputs; i++) { - this.addOutput(i.toString(), new ClassicPreset.Output(SOCKET_PRESET)); + this.addInput(i.toString(), new ClassicPreset.Input(SOCKET_PRESET)); } } else if (decl.numInputs === -1) { // TODO: handle variable number of inputs - this.addOutput('0', new ClassicPreset.Output(SOCKET_PRESET)); + this.addInput('0', new ClassicPreset.Input(SOCKET_PRESET)); } } @@ -95,14 +96,41 @@ export class AlgNode extends ClassicPreset.Node { this.height = BASE_HEIGHT + this.numOfInputs * 40 + sum; } - data(inputs: any = {}) { + data(inputs: { [key: string]: string } = {}) { + // https://retejs.org/docs/guides/processing/dataflow + // build PolyAlg representation of this node - return {'0': {name: this.label, inputs: inputs}}; + const args = []; + for (const p of this.decl.posParams) { + const arg = this.controls[p.name] as ArgControl; + args.push(arg.toPolyAlg()); + } + for (const p of this.decl.kwParams) { + const polyAlg = (this.controls[p.name] as ArgControl).toPolyAlg(); + if (polyAlg !== p.defaultPolyAlg) { + args.push(polyAlg); + } + } + + const values = Object.keys(inputs) + .sort((a, b) => parseInt(a, 10) - parseInt(b, 10)) // keys correspond to input socket key + .map(key => inputs[key]); + let children = ''; + if (values.length > 0) { + children = `(\n${values.join(',\n')})`; + } + + const polyAlg = ` ${this.decl.name}[${args.join(', ')}]${children}`; + + return {'out': polyAlg}; } clone() { - // TODO: infer args from controls of this node - return new AlgNode(this.decl, null, this.isReadOnly, this.updateArea); + const args = {}; + for (const p of this.decl.posParams.concat(this.decl.kwParams)) { + args[p.name] = (this.controls[p.name] as ArgControl).copyArg(); + } + return new AlgNode(this.decl, args, this.isReadOnly, this.updateArea); } } diff --git a/src/app/components/polyalg/controls/arg-control.ts b/src/app/components/polyalg/controls/arg-control.ts index f9d1aefa..bd3a79fe 100644 --- a/src/app/components/polyalg/controls/arg-control.ts +++ b/src/app/components/polyalg/controls/arg-control.ts @@ -1,6 +1,7 @@ import {ClassicPreset} from 'rete'; import {Type} from '@angular/core'; import {Parameter} from '../models/polyalg-registry'; +import {PlanArgument} from '../models/polyalg-plan.model'; export abstract class ArgControl extends ClassicPreset.Control { readonly name: string; @@ -13,4 +14,8 @@ export abstract class ArgControl extends ClassicPreset.Control { abstract getHeight(): number; abstract getArgComponent(): Type; + + abstract toPolyAlg(): string; + + abstract copyArg(): PlanArgument; } diff --git a/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.html b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.html index 38c3cd5c..36d7b6f9 100644 --- a/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.html +++ b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.html @@ -2,7 +2,7 @@
    \ No newline at end of file diff --git a/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts index e9820138..6c23fbb0 100644 --- a/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts +++ b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts @@ -1,7 +1,7 @@ import {Component, Input, Type} from '@angular/core'; -import {BooleanArg} from '../../models/polyalg-plan.model'; +import {BooleanArg, PlanArgument} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; -import {Parameter} from '../../models/polyalg-registry'; +import {Parameter, ParamType} from '../../models/polyalg-registry'; @Component({ selector: 'app-boolean-arg', @@ -25,4 +25,12 @@ export class BooleanControl extends ArgControl { getArgComponent(): Type { return BooleanArgComponent; } + + toPolyAlg(): string { + return this.value.arg.toString(); + } + + copyArg(): PlanArgument { + return {type: ParamType.BOOLEAN, value: JSON.parse(JSON.stringify(this.value))}; + } } diff --git a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts index ed8300f8..3b19a3fa 100644 --- a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts +++ b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts @@ -1,7 +1,7 @@ import {Component, Input, Type} from '@angular/core'; -import {EntityArg} from '../../models/polyalg-plan.model'; +import {EntityArg, PlanArgument} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; -import {Parameter} from '../../models/polyalg-registry'; +import {Parameter, ParamType} from '../../models/polyalg-registry'; @Component({ selector: 'app-entity-arg', @@ -24,4 +24,12 @@ export class EntityControl extends ArgControl { getArgComponent(): Type { return EntityArgComponent; } + + toPolyAlg(): string { + return this.value.arg; + } + + copyArg(): PlanArgument { + return {type: ParamType.ENTITY, value: JSON.parse(JSON.stringify(this.value))}; + } } diff --git a/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts b/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts index 899c4e9e..cdc3160b 100644 --- a/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts +++ b/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts @@ -1,6 +1,6 @@ import {Component, Input, OnInit, Type} from '@angular/core'; import {ArgControl} from '../arg-control'; -import {EnumArg} from '../../models/polyalg-plan.model'; +import {EnumArg, PlanArgument} from '../../models/polyalg-plan.model'; import {PolyAlgService} from '../../polyalg.service'; import {Parameter} from '../../models/polyalg-registry'; @@ -35,4 +35,11 @@ export class EnumControl extends ArgControl { return EnumArgComponent; } + toPolyAlg(): string { + return this.value.arg; + } + + copyArg(): PlanArgument { + return {type: this.type, value: JSON.parse(JSON.stringify(this.value)), isEnum: true}; + } } diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts index 1e4e3c4f..ac7ecbf9 100644 --- a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts @@ -1,8 +1,8 @@ import {Component, Input, Type} from '@angular/core'; -import {ListArg} from '../../models/polyalg-plan.model'; +import {ListArg, PlanArgument} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; import {getControl} from '../arg-control-utils'; -import {Parameter} from '../../models/polyalg-registry'; +import {Parameter, ParamType} from '../../models/polyalg-registry'; @Component({ selector: 'app-list-arg', @@ -44,4 +44,20 @@ export class ListControl extends ArgControl { getArgComponent(): Type { return ListArgComponent; } + + toPolyAlg(): string { + if (this.children.length === 0) { + return '[]'; + } + + const args = this.children.map(arg => arg.toPolyAlg()).join(', '); + if (this.children.length === 1 || this.param.canUnpackValues) { + return args; + } + return `[${args}]`; + } + + copyArg(): PlanArgument { + return {type: ParamType.LIST, value: JSON.parse(JSON.stringify(this.value))}; + } } diff --git a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts index 033ac936..feb9d120 100644 --- a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts +++ b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts @@ -1,7 +1,7 @@ import {Component, Input, Type} from '@angular/core'; -import {RexArg} from '../../models/polyalg-plan.model'; +import {PlanArgument, RexArg} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; -import {Parameter, ParamTag} from '../../models/polyalg-registry'; +import {Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; @Component({ selector: 'app-rex-arg', @@ -18,6 +18,9 @@ export class RexControl extends ArgControl { constructor(param: Parameter, public value: RexArg, isReadOnly: boolean) { super(param, isReadOnly); + if (value.alias === value.rex) { + value.alias = ''; + } this.showAlias = param.tags.includes(ParamTag.ALIAS); } @@ -32,4 +35,15 @@ export class RexControl extends ArgControl { getArgComponent(): Type { return RexArgComponent; } + + toPolyAlg(): string { + if (this.showAlias && this.value.alias !== '' && this.value.alias !== this.value.rex) { + return `${this.value.rex} AS ${this.value.alias}`; + } + return this.value.rex; + } + + copyArg(): PlanArgument { + return {type: ParamType.REX, value: JSON.parse(JSON.stringify(this.value))}; + } } diff --git a/src/app/components/polyalg/controls/string-arg/string-arg.component.ts b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts index d01d16b5..554aaaea 100644 --- a/src/app/components/polyalg/controls/string-arg/string-arg.component.ts +++ b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts @@ -1,7 +1,7 @@ import {Component, Input, Type} from '@angular/core'; -import {StringArg} from '../../models/polyalg-plan.model'; +import {PlanArgument, StringArg} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; -import {Parameter, ParamTag} from '../../models/polyalg-registry'; +import {Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; @Component({ selector: 'app-string-arg', @@ -18,6 +18,9 @@ export class StringControl extends ArgControl { constructor(param: Parameter, public value: StringArg, isReadOnly: boolean) { super(param, isReadOnly); + if (value.alias === value.arg) { + value.alias = ''; + } this.showAlias = param.tags.includes(ParamTag.ALIAS); } @@ -29,4 +32,15 @@ export class StringControl extends ArgControl { return StringArgComponent; } + toPolyAlg(): string { + if (this.showAlias && this.value.alias !== '' && this.value.alias !== this.value.arg) { + return `${this.value.arg} AS ${this.value.alias}`; + } + return this.value.arg; + } + + copyArg(): PlanArgument { + return {type: ParamType.STRING, value: JSON.parse(JSON.stringify(this.value))}; + } + } diff --git a/src/app/components/polyalg/models/polyalg-plan.model.ts b/src/app/components/polyalg/models/polyalg-plan.model.ts index 48635cd7..b14090b0 100644 --- a/src/app/components/polyalg/models/polyalg-plan.model.ts +++ b/src/app/components/polyalg/models/polyalg-plan.model.ts @@ -10,7 +10,7 @@ export interface PlanNode { } export interface PlanArgument { - type: ParamType; // if isEnum, then type identifies the type of enum and is not a ParamType + type: ParamType | string; // if isEnum, then type identifies the type of enum and is not a ParamType value: ArgType; isEnum?: boolean; } @@ -31,7 +31,7 @@ export interface BooleanArg { } export interface ListArg { - innerType: ParamType; + innerType: ParamType | string; args: PlanArgument[]; } @@ -44,4 +44,4 @@ export interface EnumArg { arg: string; } -export type ArgType = EntityArg | RexArg | ListArg | StringArg | BooleanArg | EnumArg; +type ArgType = EntityArg | RexArg | ListArg | StringArg | BooleanArg | EnumArg; diff --git a/src/app/components/polyalg/models/polyalg-registry.ts b/src/app/components/polyalg/models/polyalg-registry.ts index d2ea1a5e..9908bcbd 100644 --- a/src/app/components/polyalg/models/polyalg-registry.ts +++ b/src/app/components/polyalg/models/polyalg-registry.ts @@ -25,6 +25,8 @@ export interface Parameter { isMultiValued: boolean; requiresAlias: boolean; defaultValue?: PlanArgument; + defaultPolyAlg?: string; + canUnpackValues?: boolean; } export enum ParamType { diff --git a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts index 7cd8f86d..1070dcf6 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts @@ -4,7 +4,7 @@ import {AreaExtensions, AreaPlugin, BaseAreaPlugin} from 'rete-area-plugin'; import {ConnectionPlugin, Presets as ConnectionPresets} from 'rete-connection-plugin'; import {AngularArea2D, AngularPlugin, Presets} from 'rete-angular-plugin/17'; import {PlanNode} from '../models/polyalg-plan.model'; -import {ArrangeAppliers, AutoArrangePlugin, Presets as ArrangePresets} from 'rete-auto-arrange-plugin'; +import {ArrangeAppliers, AutoArrangePlugin} from 'rete-auto-arrange-plugin'; import {AlgNode, AlgNodeComponent} from '../algnode/alg-node.component'; import {addCustomBackground} from '../algnode/background'; import {ArgControl} from '../controls/arg-control'; @@ -35,7 +35,7 @@ export async function createEditor(container: HTMLElement, injector: Injector, r const arrange = new AutoArrangePlugin(); const pathPlugin = new ConnectionPathPlugin({ transformer: () => ( - (p) => Transformers.classic({vertical: true})(p) + (p) => Transformers.classic({vertical: true})(p.reverse()) // reverse for correct UP direction ) }); const contextMenu = new ContextMenuPlugin({ @@ -81,7 +81,19 @@ export async function createEditor(container: HTMLElement, injector: Injector, r await AreaExtensions.zoomAt(area, editor.getNodes()); } }); - arrange.addPreset(ArrangePresets.classic.setup()); + arrange.addPreset(() => { + return { + port(n) { + return { + x: n.width / (n.ports + 1) * (n.index + 1), + y: 0, + width: 15, + height: 15, + side: 'output' === n.side ? 'NORTH' : 'SOUTH' + }; + } + }; + }); editor.use(readonlyPlugin.root); editor.use(area); @@ -92,6 +104,12 @@ export async function createEditor(container: HTMLElement, injector: Injector, r area.use(contextMenu); render.use(pathPlugin); + if (isReadOnly) { + readonlyPlugin.enable(); // disable interaction with nodes (control interaction is deactivated separately) + } else { + area.use(connection); // make connections editable + } + AreaExtensions.simpleNodesOrder(area); addCustomBackground(area); @@ -105,14 +123,11 @@ export async function createEditor(container: HTMLElement, injector: Injector, r } const layoutOpts = { - 'elk.algorithm': 'mrtree', - 'elk.mrtree.weighting': 'CONSTRAINT', - 'elk.spacing.edgeNode': '0', - 'elk.spacing.nodeNode': '50' + 'elk.direction': 'UP' }; await arrange.layout({ applier, options: layoutOpts - }); // https://github.com/retejs/rete/issues/697 + }); AreaExtensions.zoomAt(area, editor.getNodes()); @@ -124,15 +139,6 @@ export async function createEditor(container: HTMLElement, injector: Injector, r return context })*/ - if (isReadOnly) { - readonlyPlugin.enable(); // disable interaction with nodes (control interaction is deactivated separately) - } else { - area.use(connection); // make connections editable - } - - //const result = await engine.fetch(nodes[nodes.length - 1].id); - //console.log('result', result); - return { layout: async () => { @@ -141,7 +147,14 @@ export async function createEditor(container: HTMLElement, injector: Injector, r }); AreaExtensions.zoomAt(area, editor.getNodes()); }, - destroy: () => area.destroy() + destroy: () => area.destroy(), + toPolyAlg: async (): Promise => { + const rootId = findRootNodeId(editor.getNodes(), editor.getConnections()); + if (rootId) { + return await engine.fetch(rootId).then(res => res['out']); + } + return null; + } }; } @@ -150,15 +163,17 @@ function addNode(registry: PolyAlgService, node: PlanNode, isReadOnly: boolean, const connections = []; const algNode = new AlgNode(registry.getDeclaration(node.opName), node.arguments, isReadOnly, (a: AlgNode) => area.update('node', a.id).then()); + if (node.opName.endsWith('#')) { + // TODO: handle implicit project correctly + algNode.label = 'PROJECT#'; + } for (let i = 0; i < node.inputs.length; i++) { const [childNodes, childConnections] = addNode(registry, node.inputs[i], isReadOnly, area); const childNode = childNodes[childNodes.length - 1]; nodes.push(...childNodes); connections.push(...childConnections); - - //algNode.addOutput(i.toString(), new ClassicPreset.Output(SOCKET_PRESET)); - connections.push(new CustomConnection(algNode, i.toString(), childNode, 'top')); + connections.push(new CustomConnection(childNode, 'out', algNode, i.toString())); } nodes.push(algNode); return [nodes, connections]; @@ -203,4 +218,53 @@ function getContextMenuItems(registry: PolyAlgService, isReadOnly: boolean, area }; } +function findRootNodeId(nodes: AlgNode[], connections: CustomConnection[]): string | null { + if (connections.length === 0) { + if (nodes.length === 1) { + return nodes[0].id; + } + return null; + } + const visited = new Set(); + + let root: string = null; + + for (const c of connections) { + visited.add(c.source); + let curr = c.target; + if (visited.has(curr)) { + continue; + } + + let next = c.target; + while (next !== null) { + if (visited.has(next)) { + break; + } + curr = next; + next = getSuccessor(curr, connections); + visited.add(curr); + } + if (next === null) { + // arrived at root of subtree + if (root === null) { + root = curr; + } else if (root !== curr) { + // found different root => multiple subtrees + return null; + } + } + } + if (visited.size < nodes.length) { + console.log('found orphan nodes'); + } + return root; +} +function getSuccessor(nodeId: string, connections: CustomConnection[]): string | null { + const outgoing = connections.filter(c => c.source === nodeId); + if (outgoing.length === 0) { + return null; + } + return outgoing[0].target; +} diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html index 5ec4d62c..39c36257 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html @@ -2,11 +2,24 @@
    - - + + + + + + +
    {{generatedPolyAlg}}
    +
    +
    +
    + +
    {{polyAlg}}
    diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts index 9e806195..0472b0d9 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts @@ -15,7 +15,9 @@ export class AlgViewerComponent implements AfterViewInit { @Input() isReadOnly: boolean; @ViewChild('rete') container!: ElementRef; - editor: { layout: () => Promise; destroy: () => void; }; + generatedPolyAlg: string; + + editor: { layout: () => Promise; destroy: () => void; toPolyAlg: () => Promise; }; showAlgEditor = computed(() => this._registry.registryLoaded()); constructor(private injector: Injector, private _registry: PolyAlgService) { @@ -24,7 +26,10 @@ export class AlgViewerComponent implements AfterViewInit { if (this.showAlgEditor() && el) { createEditor(el, this.injector, _registry, JSON.parse(this.planObject) as PlanNode, this.isReadOnly) - .then(editor => this.editor = editor); + .then(editor => { + this.editor = editor; + this.generatePolyAlg(); + }); } }); } @@ -37,4 +42,10 @@ export class AlgViewerComponent implements AfterViewInit { .then(editor => this.editor = editor); }*/ } + + generatePolyAlg() { + this.editor?.toPolyAlg().then(str => { + this.generatedPolyAlg = str || 'Cannot determine root of tree'; + }); + } } diff --git a/src/app/views/querying/polyalg/polyalg.component.ts b/src/app/views/querying/polyalg/polyalg.component.ts index ca35ed29..e44b55ff 100644 --- a/src/app/views/querying/polyalg/polyalg.component.ts +++ b/src/app/views/querying/polyalg/polyalg.component.ts @@ -1,11 +1,19 @@ -import {Component} from '@angular/core'; +import {Component, OnInit} from '@angular/core'; +import {LeftSidebarService} from '../../../components/left-sidebar/left-sidebar.service'; @Component({ selector: 'app-polyalg', templateUrl: './polyalg.component.html', styleUrl: './polyalg.component.scss' }) -export class PolyalgComponent { +export class PolyalgComponent implements OnInit { samplePlan = '{"opName":"PROJECT","arguments":{"projects":{"type":"LIST","value":{"innerType":"REX","args":[{"type":"REX","value":{"rex":"employeeno","alias":"employeeno"}},{"type":"REX","value":{"rex":"relationshipjoy","alias":"happiness"}}]}}},"inputs":[{"opName":"FILTER","arguments":{"condition":{"type":"REX","value":{"rex":"<(age0, 30)"}},"variables":{"type":"LIST","value":{"innerType":"LIST","args":[]}}},"inputs":[{"opName":"JOIN","arguments":{"type":{"type":"JOIN_TYPE_ENUM","value":{"arg":"INNER","enum":"JoinAlgType"},"isEnum":true},"semiJoinDone":{"type":"BOOLEAN","value":{"arg":false}},"variables":{"type":"LIST","value":{"innerType":"LIST","args":[]}},"condition":{"type":"REX","value":{"rex":"=(employeeno, employeeno0)"}}},"inputs":[{"opName":"SCAN","arguments":{"entity":{"type":"ENTITY","value":{"arg":"public.emp","namespaceId":0,"id":3}}},"inputs":[]},{"opName":"PROJECT#","arguments":{"projects":{"type":"LIST","value":{"innerType":"STRING","args":[{"type":"STRING","value":{"arg":"employeeno","alias":"employeeno0"}},{"type":"STRING","value":{"arg":"age","alias":"age0"}}]}}},"inputs":[{"opName":"PROJECT","arguments":{"projects":{"type":"LIST","value":{"innerType":"REX","args":[{"type":"REX","value":{"rex":"employeeno","alias":"employeeno"}},{"type":"REX","value":{"rex":"age","alias":"age"}}]}}},"inputs":[{"opName":"SCAN","arguments":{"entity":{"type":"ENTITY","value":{"arg":"public.emp","namespaceId":0,"id":3}}},"inputs":[]}]}]}]}]}]}'; + constructor(private _left: LeftSidebarService) { + } + + ngOnInit(): void { + this._left.close(); + } + } From 065064ecd33642b82eeb14d89aa3c112bfec8a54 Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Wed, 1 May 2024 23:21:43 +0200 Subject: [PATCH 09/54] Add support for more PolyArg types --- src/app/components/components.module.ts | 29 ++++++++- .../polyalg/algnode/alg-node.component.ts | 8 +-- .../controls/agg-arg/agg-arg.component.html | 50 ++++++++++++++ .../controls/agg-arg/agg-arg.component.scss | 0 .../controls/agg-arg/agg-arg.component.ts | 56 ++++++++++++++++ .../polyalg/controls/arg-control-utils.ts | 48 +++++++++----- .../collation-arg.component.html | 20 ++++++ .../collation-arg.component.scss | 0 .../collation-arg/collation-arg.component.ts | 53 +++++++++++++++ .../correlation-arg.component.html | 4 ++ .../correlation-arg.component.scss | 0 .../correlation-arg.component.ts | 38 +++++++++++ .../entity-arg/entity-arg.component.ts | 2 +- .../controls/enum-arg/enum-arg.component.ts | 2 +- .../field-arg/field-arg.component.html | 4 ++ .../field-arg/field-arg.component.scss | 0 .../controls/field-arg/field-arg.component.ts | 38 +++++++++++ .../controls/int-arg/int-arg.component.html | 4 ++ .../controls/int-arg/int-arg.component.scss | 0 .../controls/int-arg/int-arg.component.ts | 43 ++++++++++++ .../lax-agg/lax-agg-arg.component.html | 26 ++++++++ .../lax-agg/lax-agg-arg.component.scss | 0 .../controls/lax-agg/lax-agg-arg.component.ts | 46 +++++++++++++ .../controls/list-arg/list-arg.component.html | 32 ++++++++- .../controls/list-arg/list-arg.component.ts | 15 ++++- .../controls/rex-arg/rex-arg.component.html | 4 +- .../controls/rex-arg/rex-arg.component.ts | 2 +- .../string-arg/string-arg.component.html | 6 +- .../string-arg/string-arg.component.ts | 2 +- .../polyalg/models/polyalg-plan.model.ts | 65 ++++++++++++++++++- .../polyalg/models/polyalg-registry.ts | 3 +- .../polyalg/polyalg-viewer/alg-editor.ts | 11 ++-- .../polyalg-viewer/alg-viewer.component.html | 1 + src/scss/style.scss | 33 +--------- 34 files changed, 572 insertions(+), 73 deletions(-) create mode 100644 src/app/components/polyalg/controls/agg-arg/agg-arg.component.html create mode 100644 src/app/components/polyalg/controls/agg-arg/agg-arg.component.scss create mode 100644 src/app/components/polyalg/controls/agg-arg/agg-arg.component.ts create mode 100644 src/app/components/polyalg/controls/collation-arg/collation-arg.component.html create mode 100644 src/app/components/polyalg/controls/collation-arg/collation-arg.component.scss create mode 100644 src/app/components/polyalg/controls/collation-arg/collation-arg.component.ts create mode 100644 src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.html create mode 100644 src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.scss create mode 100644 src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.ts create mode 100644 src/app/components/polyalg/controls/field-arg/field-arg.component.html create mode 100644 src/app/components/polyalg/controls/field-arg/field-arg.component.scss create mode 100644 src/app/components/polyalg/controls/field-arg/field-arg.component.ts create mode 100644 src/app/components/polyalg/controls/int-arg/int-arg.component.html create mode 100644 src/app/components/polyalg/controls/int-arg/int-arg.component.scss create mode 100644 src/app/components/polyalg/controls/int-arg/int-arg.component.ts create mode 100644 src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.html create mode 100644 src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.scss create mode 100644 src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts index 19d71d12..c9b50fd0 100644 --- a/src/app/components/components.module.ts +++ b/src/app/components/components.module.ts @@ -21,6 +21,8 @@ import { DropdownItemDirective, DropdownMenuDirective, DropdownToggleDirective, + FormCheckComponent, + FormCheckLabelDirective, FormControlDirective, FormDirective, FormFeedbackComponent, @@ -111,6 +113,12 @@ import {BooleanArgComponent} from './polyalg/controls/boolean-arg/boolean-arg.co import {CustomSocketComponent} from './polyalg/custom-socket/custom-socket.component'; import {CustomConnectionComponent} from './polyalg/custom-connection/custom-connection.component'; import {EnumArgComponent} from './polyalg/controls/enum-arg/enum-arg.component'; +import {IntArgComponent} from './polyalg/controls/int-arg/int-arg.component'; +import {FieldArgComponent} from './polyalg/controls/field-arg/field-arg.component'; +import {CorrelationArgComponent} from './polyalg/controls/correlation-arg/correlation-arg.component'; +import {CollationArgComponent} from './polyalg/controls/collation-arg/collation-arg.component'; +import {AggArgComponent} from './polyalg/controls/agg-arg/agg-arg.component'; +import {LaxAggArgComponent} from './polyalg/controls/lax-agg/lax-agg-arg.component'; //import 'hammerjs'; @@ -180,7 +188,18 @@ import {EnumArgComponent} from './polyalg/controls/enum-arg/enum-arg.component'; DropdownMenuDirective, DropdownItemDirective, DropdownDividerDirective, - DropdownToggleDirective, ModalTitleDirective, FormDirective, RowDirective, DropdownComponent, FormSelectDirective, TooltipDirective, ContainerComponent, ReteModule, AutocompleteLibModule + DropdownToggleDirective, + ModalTitleDirective, + FormDirective, + RowDirective, + DropdownComponent, + FormSelectDirective, + TooltipDirective, + ContainerComponent, + ReteModule, + AutocompleteLibModule, + FormCheckComponent, + FormCheckLabelDirective ], declarations: [ BreadcrumbComponent, @@ -219,7 +238,13 @@ import {EnumArgComponent} from './polyalg/controls/enum-arg/enum-arg.component'; CustomSocketComponent, CustomConnectionComponent, EnumArgComponent, - AlgViewerComponent + AlgViewerComponent, + IntArgComponent, + FieldArgComponent, + CorrelationArgComponent, + CollationArgComponent, + AggArgComponent, + LaxAggArgComponent ], exports: [ BreadcrumbComponent, diff --git a/src/app/components/polyalg/algnode/alg-node.component.ts b/src/app/components/polyalg/algnode/alg-node.component.ts index c040027d..16abe175 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.ts +++ b/src/app/components/polyalg/algnode/alg-node.component.ts @@ -61,7 +61,7 @@ export class AlgNode extends ClassicPreset.Node { const heights = {}; for (const p of decl.posParams.concat(decl.kwParams)) { - const arg = args?.[p.name]; + const arg = args?.[p.name] || null; const c = getControl(p, arg, isReadOnly, (height: number) => { this.updateControlHeight(p.name, height); updateArea(this); @@ -92,8 +92,8 @@ export class AlgNode extends ClassicPreset.Node { } private recomputeHeight() { - const sum = Object.values(this.controlHeights).reduce((total, value) => total + value, 0); - this.height = BASE_HEIGHT + this.numOfInputs * 40 + sum; + const sum = Object.values(this.controlHeights).reduce((total, value) => total + value + 12, 0); + this.height = BASE_HEIGHT + sum; } data(inputs: { [key: string]: string } = {}) { @@ -108,7 +108,7 @@ export class AlgNode extends ClassicPreset.Node { for (const p of this.decl.kwParams) { const polyAlg = (this.controls[p.name] as ArgControl).toPolyAlg(); if (polyAlg !== p.defaultPolyAlg) { - args.push(polyAlg); + args.push(`${p.name}=${polyAlg}`); } } diff --git a/src/app/components/polyalg/controls/agg-arg/agg-arg.component.html b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.html new file mode 100644 index 00000000..93d1b699 --- /dev/null +++ b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.html @@ -0,0 +1,50 @@ + + + + + + + + + + TODO Argument(s) + + + + + Alias + + + + + + + + + + + + + + + + Filter + + + +

    WITHIN GROUP: TODO ({{data.value.collList}})

    \ No newline at end of file diff --git a/src/app/components/polyalg/controls/agg-arg/agg-arg.component.scss b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/polyalg/controls/agg-arg/agg-arg.component.ts b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.ts new file mode 100644 index 00000000..fdc37823 --- /dev/null +++ b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.ts @@ -0,0 +1,56 @@ +import {Component, Input, OnInit, Type} from '@angular/core'; +import {ArgControl} from '../arg-control'; +import {Parameter, ParamType} from '../../models/polyalg-registry'; +import {AggArg, PlanArgument} from '../../models/polyalg-plan.model'; +import {CollationControl} from '../collation-arg/collation-arg.component'; +import {PolyAlgService} from '../../polyalg.service'; + +@Component({ + selector: 'app-agg-arg', + templateUrl: './agg-arg.component.html', + styleUrl: './agg-arg.component.scss' +}) +export class AggArgComponent implements OnInit { + @Input() data: AggControl; // TODO: support multiple args, colls + fChoices: string[] = []; + + constructor(private _registry: PolyAlgService) { + } + + ngOnInit(): void { + this.fChoices = this._registry.getEnumValues('AggFunctionOperator').slice().sort(); // sort shallow copy + } + +} + + +export class AggControl extends ArgControl { + + constructor(param: Parameter, public value: AggArg, isReadOnly: boolean) { + super(param, isReadOnly); + } + + getHeight(): number { + return 188; + } + + getArgComponent(): Type { + return AggArgComponent; + } + + toPolyAlg(): string { + const argList = this.value.argList.join(', '); + const distStr = this.value.distinct ? (argList.length > 0 ? 'DISTINCT ' : 'DISTINCT') : ''; + const approx = this.value.approximate ? ' APPROXIMATE' : ''; + const collation = this.value.collList.length > 0 ? + ` WITHIN GROUP (${this.value.collList.map(coll => CollationControl.collToPolyAlg(coll)).join(', ')})` : ''; + const filter = this.value.filter ? ' FILTER ' + this.value.filter : ''; + const alias = this.value.alias ? ` AS ${this.value.alias}` : ''; + + return `${this.value.function}(${distStr}${argList})${approx}${collation}${filter}${alias}`; + } + + copyArg(): PlanArgument { + return {type: ParamType.AGGREGATE, value: JSON.parse(JSON.stringify(this.value))}; + } +} diff --git a/src/app/components/polyalg/controls/arg-control-utils.ts b/src/app/components/polyalg/controls/arg-control-utils.ts index 1ff5c82b..a01f6097 100644 --- a/src/app/components/polyalg/controls/arg-control-utils.ts +++ b/src/app/components/polyalg/controls/arg-control-utils.ts @@ -4,9 +4,15 @@ import {BooleanControl} from './boolean-arg/boolean-arg.component'; import {RexControl} from './rex-arg/rex-arg.component'; import {EntityControl} from './entity-arg/entity-arg.component'; import {ListControl} from './list-arg/list-arg.component'; -import {BooleanArg, EntityArg, EnumArg, ListArg, PlanArgument, RexArg, StringArg} from '../models/polyalg-plan.model'; +import {AggArg, BooleanArg, CollationArg, CollDirection, CorrelationArg, defaultNullDirection, EntityArg, EnumArg, FieldArg, IntArg, LaxAggArg, ListArg, PlanArgument, RexArg, StringArg} from '../models/polyalg-plan.model'; import {EnumControl} from './enum-arg/enum-arg.component'; import {Parameter, ParamType} from '../models/polyalg-registry'; +import {IntControl} from './int-arg/int-arg.component'; +import {FieldControl} from './field-arg/field-arg.component'; +import {CorrelationControl} from './correlation-arg/correlation-arg.component'; +import {CollationControl} from './collation-arg/collation-arg.component'; +import {AggControl} from './agg-arg/agg-arg.component'; +import {LaxAggControl} from './lax-agg/lax-agg-arg.component'; export function getControl(param: Parameter, arg: PlanArgument | null, isReadOnly: boolean, updateHeight: (height: number) => void, isForOuter = false): ArgControl { @@ -22,7 +28,7 @@ export function getControl(param: Parameter, arg: PlanArgument | null, case 'ANY': break; case 'INTEGER': - break; + return new IntControl(param, arg.value as IntArg, isReadOnly); case 'STRING': return new StringControl(param, arg.value as StringArg, isReadOnly); case 'BOOLEAN': @@ -30,28 +36,36 @@ export function getControl(param: Parameter, arg: PlanArgument | null, case 'REX': return new RexControl(param, arg.value as RexArg, isReadOnly); case 'AGGREGATE': - break; + return new AggControl(param, arg.value as AggArg, isReadOnly); case 'LAX_AGGREGATE': - break; + return new LaxAggControl(param, arg.value as LaxAggArg, isReadOnly); case 'ENTITY': return new EntityControl(param, arg.value as EntityArg, isReadOnly); case 'FIELD': - break; + return new FieldControl(param, arg.value as FieldArg, isReadOnly); case 'LIST': return new ListControl(param, arg.value as ListArg, isReadOnly, updateHeight); case 'COLLATION': - break; + return new CollationControl(param, arg.value as CollationArg, isReadOnly); case 'CORR_ID': - break; + return new CorrelationControl(param, arg.value as CorrelationArg, isReadOnly); } return new StringControl(param, {'arg': JSON.stringify(arg)}, isReadOnly); } export function getInitialArg(p: Parameter, isForOuter: boolean): PlanArgument { - if (p.defaultValue) { - return p.defaultValue; + if (p.defaultValue && !p.isMultiValued) { + return p.defaultValue; // kwParams always have a defaultValue } const isListArg = p.isMultiValued && isForOuter; + + if (isListArg && p.defaultValue) { + if ((p.defaultValue?.value as ListArg).args.length === 0) { + // Handle case of EMPTY_LIST + return {type: ParamType.LIST, value: {innerType: p.type, args: []}}; + } + return p.defaultValue; + } return { type: isListArg ? ParamType.LIST : p.type, value: (function () { @@ -65,7 +79,7 @@ export function getInitialArg(p: Parameter, isForOuter: boolean): PlanArgument { case ParamType.ANY: break; case ParamType.INTEGER: - break; + return {arg: 0}; case ParamType.STRING: return {arg: ''}; case ParamType.BOOLEAN: @@ -73,17 +87,21 @@ export function getInitialArg(p: Parameter, isForOuter: boolean): PlanArgument { case ParamType.REX: return {rex: ''}; case ParamType.AGGREGATE: - break; + return {function: 'COUNT', distinct: false, approximate: false, argList: [], collList: [], alias: ''}; case ParamType.LAX_AGGREGATE: - break; + return {function: 'COUNT', input: '', alias: ''}; case ParamType.ENTITY: return {arg: ''}; case ParamType.FIELD: - break; + return {arg: ''}; case ParamType.COLLATION: - break; + return { + field: '', + direction: CollDirection.ASCENDING, + nullDirection: defaultNullDirection(CollDirection.ASCENDING) + }; case ParamType.CORR_ID: - break; + return {arg: 0}; } return null; })(), diff --git a/src/app/components/polyalg/controls/collation-arg/collation-arg.component.html b/src/app/components/polyalg/controls/collation-arg/collation-arg.component.html new file mode 100644 index 00000000..4d7d6c62 --- /dev/null +++ b/src/app/components/polyalg/controls/collation-arg/collation-arg.component.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/components/polyalg/controls/collation-arg/collation-arg.component.scss b/src/app/components/polyalg/controls/collation-arg/collation-arg.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/polyalg/controls/collation-arg/collation-arg.component.ts b/src/app/components/polyalg/controls/collation-arg/collation-arg.component.ts new file mode 100644 index 00000000..0c30fe86 --- /dev/null +++ b/src/app/components/polyalg/controls/collation-arg/collation-arg.component.ts @@ -0,0 +1,53 @@ +import {Component, Input, Type} from '@angular/core'; +import {ArgControl} from '../arg-control'; +import {Parameter, ParamType} from '../../models/polyalg-registry'; +import {CollationArg, CollDirection, CollNullDirection, defaultNullDirection, PlanArgument} from '../../models/polyalg-plan.model'; + +@Component({ + selector: 'app-collation-arg', + templateUrl: './collation-arg.component.html', + styleUrl: './collation-arg.component.scss' +}) +export class CollationArgComponent { + @Input() data: CollationControl; + + dirChoices = Object.keys(CollDirection); + nullDirChoices = Object.keys(CollNullDirection); + protected readonly CollNullDirection = CollNullDirection; + protected readonly CollDirection = CollDirection; +} + +export class CollationControl extends ArgControl { + + constructor(param: Parameter, public value: CollationArg, isReadOnly: boolean) { + super(param, isReadOnly); + } + + static collToPolyAlg(value: CollationArg): string { + let str = value.field; + const notDefaultNull = value.nullDirection !== defaultNullDirection(value.direction); + if (value.direction !== CollDirection.ASCENDING || notDefaultNull) { + str += ` ${value.direction}`; + if (notDefaultNull) { + str += ` ${value.nullDirection}`; + } + } + return str; + } + + getHeight(): number { + return 101; + } + + getArgComponent(): Type { + return CollationArgComponent; + } + + toPolyAlg(): string { + return CollationControl.collToPolyAlg(this.value); + } + + copyArg(): PlanArgument { + return {type: ParamType.COLLATION, value: JSON.parse(JSON.stringify(this.value))}; + } +} diff --git a/src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.html b/src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.html new file mode 100644 index 00000000..07982739 --- /dev/null +++ b/src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.html @@ -0,0 +1,4 @@ + + diff --git a/src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.scss b/src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.ts b/src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.ts new file mode 100644 index 00000000..474b6d9c --- /dev/null +++ b/src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.ts @@ -0,0 +1,38 @@ +import {Component, Input, Type} from '@angular/core'; +import {ArgControl} from '../arg-control'; +import {Parameter, ParamType} from '../../models/polyalg-registry'; +import {CorrelationArg, PlanArgument} from '../../models/polyalg-plan.model'; + +@Component({ + selector: 'app-correlation-arg', + templateUrl: './correlation-arg.component.html', + styleUrl: './correlation-arg.component.scss' +}) +export class CorrelationArgComponent { + @Input() data: CorrelationControl; + +} + +export class CorrelationControl extends ArgControl { + + constructor(param: Parameter, public value: CorrelationArg, isReadOnly: boolean) { + super(param, isReadOnly); + } + + getHeight(): number { + return this.name ? 55 : 31; + } + + getArgComponent(): Type { + return CorrelationArgComponent; + } + + toPolyAlg(): string { + return this.value.arg.toString(); + } + + copyArg(): PlanArgument { + return {type: ParamType.CORR_ID, value: JSON.parse(JSON.stringify(this.value))}; + } + +} diff --git a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts index 3b19a3fa..e00efa36 100644 --- a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts +++ b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts @@ -18,7 +18,7 @@ export class EntityControl extends ArgControl { } getHeight(): number { - return 40; + return this.name ? 55 : 31; } getArgComponent(): Type { diff --git a/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts b/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts index cdc3160b..cc43c1a5 100644 --- a/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts +++ b/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts @@ -28,7 +28,7 @@ export class EnumControl extends ArgControl { } getHeight(): number { - return 55; + return this.name ? 55 : 31; } getArgComponent(): Type { diff --git a/src/app/components/polyalg/controls/field-arg/field-arg.component.html b/src/app/components/polyalg/controls/field-arg/field-arg.component.html new file mode 100644 index 00000000..f14a9dce --- /dev/null +++ b/src/app/components/polyalg/controls/field-arg/field-arg.component.html @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/src/app/components/polyalg/controls/field-arg/field-arg.component.scss b/src/app/components/polyalg/controls/field-arg/field-arg.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/polyalg/controls/field-arg/field-arg.component.ts b/src/app/components/polyalg/controls/field-arg/field-arg.component.ts new file mode 100644 index 00000000..14238a6d --- /dev/null +++ b/src/app/components/polyalg/controls/field-arg/field-arg.component.ts @@ -0,0 +1,38 @@ +import {Component, Input, Type} from '@angular/core'; +import {ArgControl} from '../arg-control'; +import {Parameter, ParamType} from '../../models/polyalg-registry'; +import {FieldArg, PlanArgument} from '../../models/polyalg-plan.model'; + +@Component({ + selector: 'app-field-arg', + templateUrl: './field-arg.component.html', + styleUrl: './field-arg.component.scss' +}) +export class FieldArgComponent { + @Input() data: FieldControl; + +} + +export class FieldControl extends ArgControl { + + constructor(param: Parameter, public value: FieldArg, isReadOnly: boolean) { + super(param, isReadOnly); + } + + getHeight(): number { + return this.name ? 55 : 31; + } + + getArgComponent(): Type { + return FieldArgComponent; + } + + toPolyAlg(): string { + return this.value.arg; + } + + copyArg(): PlanArgument { + return {type: ParamType.FIELD, value: JSON.parse(JSON.stringify(this.value))}; + } + +} diff --git a/src/app/components/polyalg/controls/int-arg/int-arg.component.html b/src/app/components/polyalg/controls/int-arg/int-arg.component.html new file mode 100644 index 00000000..7a2abddd --- /dev/null +++ b/src/app/components/polyalg/controls/int-arg/int-arg.component.html @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/src/app/components/polyalg/controls/int-arg/int-arg.component.scss b/src/app/components/polyalg/controls/int-arg/int-arg.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/polyalg/controls/int-arg/int-arg.component.ts b/src/app/components/polyalg/controls/int-arg/int-arg.component.ts new file mode 100644 index 00000000..3b56832b --- /dev/null +++ b/src/app/components/polyalg/controls/int-arg/int-arg.component.ts @@ -0,0 +1,43 @@ +import {Component, Input, Type} from '@angular/core'; +import {ArgControl} from '../arg-control'; +import {Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; +import {IntArg, PlanArgument} from '../../models/polyalg-plan.model'; + +@Component({ + selector: 'app-int-arg', + templateUrl: './int-arg.component.html', + styleUrl: './int-arg.component.scss' +}) +export class IntArgComponent { + @Input() data: IntControl; +} + +export class IntControl extends ArgControl { + valueRange: { min: number | null; max: number | null; }; + + constructor(param: Parameter, public value: IntArg, isReadOnly: boolean) { + super(param, isReadOnly); + + this.valueRange = {min: null, max: null}; + if (param.tags.includes(ParamTag.NON_NEGATIVE)) { + this.valueRange.min = 0; + } + } + + getHeight(): number { + return this.name ? 55 : 31; + } + + getArgComponent(): Type { + return IntArgComponent; + } + + toPolyAlg(): string { + return this.value.arg.toString(); + } + + copyArg(): PlanArgument { + return {type: ParamType.INTEGER, value: JSON.parse(JSON.stringify(this.value))}; + } + +} diff --git a/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.html b/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.html new file mode 100644 index 00000000..b294664b --- /dev/null +++ b/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.html @@ -0,0 +1,26 @@ + + + + + + + + + Input + + + + + Alias + + + diff --git a/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.scss b/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts b/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts new file mode 100644 index 00000000..8bb98717 --- /dev/null +++ b/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts @@ -0,0 +1,46 @@ +import {Component, Input, OnInit, Type} from '@angular/core'; +import {ArgControl} from '../arg-control'; +import {Parameter, ParamType} from '../../models/polyalg-registry'; +import {LaxAggArg, PlanArgument} from '../../models/polyalg-plan.model'; +import {PolyAlgService} from '../../polyalg.service'; + +@Component({ + selector: 'app-lax-agg-arg', + templateUrl: './lax-agg-arg.component.html', + styleUrl: './lax-agg-arg.component.scss' +}) +export class LaxAggArgComponent implements OnInit { + @Input() data: LaxAggControl; + fChoices: string[] = []; + + constructor(private _registry: PolyAlgService) { + } + + ngOnInit(): void { + this.fChoices = this._registry.getEnumValues('AggFunctionOperator').slice().sort(); // sort shallow copy + } + +} + +export class LaxAggControl extends ArgControl { + constructor(param: Parameter, public value: LaxAggArg, isReadOnly: boolean) { + super(param, isReadOnly); + } + + getHeight(): number { + return this.name ? 125 : 101; + } + + getArgComponent(): Type { + return LaxAggArgComponent; + } + + toPolyAlg(): string { + const alias = this.value.alias ? ` AS ${this.value.alias}` : ''; + return `${this.value.function}(${this.value.input})${alias}`; + } + + copyArg(): PlanArgument { + return {type: ParamType.LAX_AGGREGATE, value: JSON.parse(JSON.stringify(this.value))}; + } +} diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.html b/src/app/components/polyalg/controls/list-arg/list-arg.component.html index 1021aca4..0db2e390 100644 --- a/src/app/components/polyalg/controls/list-arg/list-arg.component.html +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.html @@ -2,15 +2,40 @@
    • - + - + + + + + + + + + + + + + + + + + + + + + + + + + +

      {{data.value.innerType}}

      {{child}}

      @@ -19,7 +44,8 @@
    • -
    • +
    • Add element
    diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts index ac7ecbf9..01490e7f 100644 --- a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts @@ -13,6 +13,7 @@ export class ListArgComponent { @Input() data: ListControl; + protected readonly ParamType = ParamType; } export class ListControl extends ArgControl { @@ -22,11 +23,15 @@ export class ListControl extends ArgControl { isReadOnly: boolean, public updateHeight: (height: number) => void) { super(param, isReadOnly, true); this.children = value.args.map(arg => getControl(param, arg, isReadOnly, updateHeight)); + if (this.children.length === 0 && value.innerType === ParamType.LIST) { + value.innerType = param.type; // TODO: handle nested lists + } } getHeight(): number { - return this.children.reduce((total, child) => total + child.getHeight(), 0) - + Object.keys(this.children).length * 16; + const childrenHeight = this.children.reduce((total, child) => total + child.getHeight() + 16, 0); + + return 36 + (this.isReadOnly ? childrenHeight : childrenHeight + 33); // 36: title, 33: add button } addElement() { @@ -58,6 +63,10 @@ export class ListControl extends ArgControl { } copyArg(): PlanArgument { - return {type: ParamType.LIST, value: JSON.parse(JSON.stringify(this.value))}; + const value: ListArg = { + innerType: this.value.innerType, + args: this.children.map(arg => arg.copyArg()) + }; + return {type: ParamType.LIST, value: value}; } } diff --git a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.html b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.html index 27c8f986..4c774405 100644 --- a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.html +++ b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.html @@ -3,13 +3,13 @@ AS + class="form-control form-control-sm param-input" [(ngModel)]="data.value.alias"> diff --git a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts index feb9d120..45079782 100644 --- a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts +++ b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts @@ -25,7 +25,7 @@ export class RexControl extends ArgControl { } getHeight(): number { - return 100; + return this.name ? 55 : 31; } trivialAlias(): boolean { diff --git a/src/app/components/polyalg/controls/string-arg/string-arg.component.html b/src/app/components/polyalg/controls/string-arg/string-arg.component.html index 5237527f..26a006d5 100644 --- a/src/app/components/polyalg/controls/string-arg/string-arg.component.html +++ b/src/app/components/polyalg/controls/string-arg/string-arg.component.html @@ -3,13 +3,13 @@ + autocomplete="off" [readonly]="data.isReadOnly" placeholder="string" + class="form-control form-control-sm" [(ngModel)]="data.value.arg"> AS + class="form-control form-control-sm" [(ngModel)]="data.value.alias"> diff --git a/src/app/components/polyalg/controls/string-arg/string-arg.component.ts b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts index 554aaaea..c0d95b55 100644 --- a/src/app/components/polyalg/controls/string-arg/string-arg.component.ts +++ b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts @@ -25,7 +25,7 @@ export class StringControl extends ArgControl { } getHeight(): number { - return 55; + return this.name ? 55 : 31; } getArgComponent(): Type { diff --git a/src/app/components/polyalg/models/polyalg-plan.model.ts b/src/app/components/polyalg/models/polyalg-plan.model.ts index b14090b0..9c2c583f 100644 --- a/src/app/components/polyalg/models/polyalg-plan.model.ts +++ b/src/app/components/polyalg/models/polyalg-plan.model.ts @@ -44,4 +44,67 @@ export interface EnumArg { arg: string; } -type ArgType = EntityArg | RexArg | ListArg | StringArg | BooleanArg | EnumArg; +export interface IntArg { + arg: number; +} + +export interface LaxAggArg { + arg?: string; + function: string; + input: string; + alias: string; +} + +export interface AggArg { + arg?: string; + function: string; + distinct: boolean; + approximate: boolean; + argList: string[]; + collList: CollationArg[]; + filter?: string; + alias: string; +} + +export interface CollationArg { + field: string; + direction: CollDirection; + nullDirection: CollNullDirection; +} + +export interface CorrelationArg { + arg: number; +} + +export interface FieldArg { + arg: string; +} + +type ArgType = EntityArg | RexArg | ListArg | StringArg | BooleanArg | EnumArg | IntArg | LaxAggArg | AggArg | CollationArg | CorrelationArg | FieldArg; + +export enum CollDirection { + ASCENDING = 'ASC', + STRICTLY_ASCENDING = 'SASC', + DESCENDING = 'DESC', + STRICTLY_DESCENDING = 'SDESC', + CLUSTERED = 'CLU' +} + +export enum CollNullDirection { + FIRST = 'FIRST', + LAST = 'LAST', + UNSPECIFIED = 'UNSPECIFIED' +} + +export function defaultNullDirection(d: CollDirection) { + switch (d) { + case CollDirection.ASCENDING: + case CollDirection.STRICTLY_ASCENDING: + return CollNullDirection.LAST; + case CollDirection.DESCENDING: + case CollDirection.STRICTLY_DESCENDING: + return CollNullDirection.FIRST; + default: + return CollNullDirection.UNSPECIFIED; + } +} diff --git a/src/app/components/polyalg/models/polyalg-registry.ts b/src/app/components/polyalg/models/polyalg-registry.ts index 9908bcbd..91abd627 100644 --- a/src/app/components/polyalg/models/polyalg-registry.ts +++ b/src/app/components/polyalg/models/polyalg-registry.ts @@ -53,6 +53,7 @@ export enum OperatorTag { export enum ParamTag { ALIAS = 'ALIAS', - ADVANCED = 'ADVANCED' + ADVANCED = 'ADVANCED', + NON_NEGATIVE = 'NON_NEGATIVE' } diff --git a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts index 1070dcf6..eafb289a 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts @@ -101,13 +101,11 @@ export async function createEditor(container: HTMLElement, injector: Injector, r area.use(readonlyPlugin.area); area.use(render); area.use(arrange); - area.use(contextMenu); render.use(pathPlugin); - if (isReadOnly) { - readonlyPlugin.enable(); // disable interaction with nodes (control interaction is deactivated separately) - } else { + if (!isReadOnly) { area.use(connection); // make connections editable + area.use(contextMenu); // add context menu } AreaExtensions.simpleNodesOrder(area); @@ -139,6 +137,10 @@ export async function createEditor(container: HTMLElement, injector: Injector, r return context })*/ + if (isReadOnly) { + readonlyPlugin.enable(); // disable interaction with nodes (control interaction is deactivated separately) + } + return { layout: async () => { @@ -151,6 +153,7 @@ export async function createEditor(container: HTMLElement, injector: Injector, r toPolyAlg: async (): Promise => { const rootId = findRootNodeId(editor.getNodes(), editor.getConnections()); if (rootId) { + engine.reset(); // clear cache return await engine.fetch(rootId).then(res => res['out']); } return null; diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html index 39c36257..ee55b6ed 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html @@ -3,6 +3,7 @@
    +

    Use right-click to add or remove nodes

    - -
  • - Add element +
  • diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.scss b/src/app/components/polyalg/controls/list-arg/list-arg.component.scss index a8ef1a49..6aa66416 100644 --- a/src/app/components/polyalg/controls/list-arg/list-arg.component.scss +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.scss @@ -9,4 +9,8 @@ .list-element-container:hover .remove-element, .remove-element:hover { display: block; +} + +.add-element.disabled { + background-color: lightgray; } \ No newline at end of file diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts index 01490e7f..43299531 100644 --- a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts @@ -2,7 +2,7 @@ import {Component, Input, Type} from '@angular/core'; import {ListArg, PlanArgument} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; import {getControl} from '../arg-control-utils'; -import {Parameter, ParamType} from '../../models/polyalg-registry'; +import {Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; @Component({ selector: 'app-list-arg', @@ -18,6 +18,8 @@ export class ListArgComponent { export class ListControl extends ArgControl { children: ArgControl[]; + canHideTrivial = this.param.tags.includes(ParamTag.HIDE_TRIVIAL); + hideTrivial: boolean; constructor(param: Parameter, public value: ListArg, isReadOnly: boolean, public updateHeight: (height: number) => void) { @@ -26,15 +28,24 @@ export class ListControl extends ArgControl { if (this.children.length === 0 && value.innerType === ParamType.LIST) { value.innerType = param.type; // TODO: handle nested lists } + this.hideTrivial = this.isReadOnly && this.canHideTrivial && this.children.filter(c => c.isTrivial()).length > 2; + } getHeight(): number { - const childrenHeight = this.children.reduce((total, child) => total + child.getHeight() + 16, 0); - - return 36 + (this.isReadOnly ? childrenHeight : childrenHeight + 33); // 36: title, 33: add button + let height = this.children.filter(c => !(this.hideTrivial && c.isTrivial())) + .reduce((total, child) => total + child.getHeight() + 16, 0); + if (!this.isReadOnly) { + height += 33; // add button + } + if (this.canHideTrivial) { + height += 24; + } + return 36 + height; // 36: title } addElement() { + this.hideTrivial = false; this.children.push(getControl(this.param, null, this.isReadOnly, this.updateHeight, false)); this.updateHeight(this.getHeight()); } @@ -46,6 +57,11 @@ export class ListControl extends ArgControl { } + toggleHideTrivial() { + this.hideTrivial = !this.hideTrivial; + this.updateHeight(this.getHeight()); + } + getArgComponent(): Type { return ListArgComponent; } diff --git a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.html b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.html index 4c774405..25873873 100644 --- a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.html +++ b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.html @@ -4,12 +4,12 @@ + class="form-control form-control-sm param-input" [(ngModel)]="data.rex"> AS + autocomplete="off" [readonly]="data.isReadOnly" placeholder="{{data.rex() || 'alias'}}" + class="form-control form-control-sm param-input" [(ngModel)]="data.alias"> diff --git a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts index 45079782..46ef2bc9 100644 --- a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts +++ b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts @@ -1,4 +1,4 @@ -import {Component, Input, Type} from '@angular/core'; +import {Component, computed, Input, signal, Type} from '@angular/core'; import {PlanArgument, RexArg} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; import {Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; @@ -16,11 +16,17 @@ export class RexArgComponent { export class RexControl extends ArgControl { readonly showAlias: boolean; - constructor(param: Parameter, public value: RexArg, isReadOnly: boolean) { + // instead of changing this.value, we use signals (-> this.value might not reflect the current state!) + rex = signal(this.value.rex); + alias = signal(this.value.alias === this.value.rex ? '' : this.value.alias); + isTrivial = computed(() => { + const hasTrivialAlias = !this.showAlias || !this.alias() || this.alias() === this.rex(); + const hasTrivialRex = !this.rex() || /^[a-zA-Z0-9_$]+$/.test(this.rex()); + return hasTrivialAlias && hasTrivialRex; + }); + + constructor(param: Parameter, private value: RexArg, isReadOnly: boolean) { super(param, isReadOnly); - if (value.alias === value.rex) { - value.alias = ''; - } this.showAlias = param.tags.includes(ParamTag.ALIAS); } @@ -28,22 +34,20 @@ export class RexControl extends ArgControl { return this.name ? 55 : 31; } - trivialAlias(): boolean { - return this.value.alias === this.value.rex; - } - getArgComponent(): Type { return RexArgComponent; } toPolyAlg(): string { - if (this.showAlias && this.value.alias !== '' && this.value.alias !== this.value.rex) { - return `${this.value.rex} AS ${this.value.alias}`; + if (this.showAlias && this.alias() && this.alias() !== this.rex()) { + return `${this.rex()} AS ${this.alias()}`; } - return this.value.rex; + return this.rex(); } copyArg(): PlanArgument { + this.value.rex = this.rex(); + this.value.alias = this.alias(); return {type: ParamType.REX, value: JSON.parse(JSON.stringify(this.value))}; } } diff --git a/src/app/components/polyalg/controls/string-arg/string-arg.component.html b/src/app/components/polyalg/controls/string-arg/string-arg.component.html index 26a006d5..b0a1e502 100644 --- a/src/app/components/polyalg/controls/string-arg/string-arg.component.html +++ b/src/app/components/polyalg/controls/string-arg/string-arg.component.html @@ -4,12 +4,12 @@ - + class="form-control form-control-sm" [(ngModel)]="data.arg"> + AS + autocomplete="off" [readonly]="data.isReadOnly" placeholder="{{data.arg() || 'alias'}}" + class="form-control form-control-sm" [(ngModel)]="data.alias"> diff --git a/src/app/components/polyalg/controls/string-arg/string-arg.component.ts b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts index c0d95b55..3f2830bc 100644 --- a/src/app/components/polyalg/controls/string-arg/string-arg.component.ts +++ b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts @@ -1,46 +1,54 @@ -import {Component, Input, Type} from '@angular/core'; +import {Component, computed, Input, signal, Type} from '@angular/core'; import {PlanArgument, StringArg} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; import {Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; @Component({ - selector: 'app-string-arg', - templateUrl: './string-arg.component.html', - styleUrl: './string-arg.component.scss' + selector: 'app-string-arg', + templateUrl: './string-arg.component.html', + styleUrl: './string-arg.component.scss' }) export class StringArgComponent { - @Input() data: StringControl; + @Input() data: StringControl; } export class StringControl extends ArgControl { - readonly showAlias: boolean; - - constructor(param: Parameter, public value: StringArg, isReadOnly: boolean) { - super(param, isReadOnly); - if (value.alias === value.arg) { - value.alias = ''; + readonly showAlias: boolean; + + // instead of changing this.value, we use signals (-> this.value might not reflect the current state!) + arg = signal(this.value.arg); + alias = signal(this.value.alias === this.value.arg ? '' : this.value.alias); + isTrivial = computed(() => { + const hasTrivialAlias = !this.showAlias || !this.alias() || this.alias() === this.arg(); + const hasTrivialArg = !this.arg() || /^[a-zA-Z0-9_$]+$/.test(this.arg()); // TODO: use better way to determine whether arg is trivial + return hasTrivialAlias && hasTrivialArg; + }); + + constructor(param: Parameter, private value: StringArg, isReadOnly: boolean) { + super(param, isReadOnly); + this.showAlias = param.tags.includes(ParamTag.ALIAS); } - this.showAlias = param.tags.includes(ParamTag.ALIAS); - } - getHeight(): number { - return this.name ? 55 : 31; - } + getHeight(): number { + return this.name ? 55 : 31; + } - getArgComponent(): Type { - return StringArgComponent; - } + getArgComponent(): Type { + return StringArgComponent; + } - toPolyAlg(): string { - if (this.showAlias && this.value.alias !== '' && this.value.alias !== this.value.arg) { - return `${this.value.arg} AS ${this.value.alias}`; + toPolyAlg(): string { + if (this.showAlias && this.alias() !== '' && this.alias() !== this.arg()) { + return `${this.arg()} AS ${this.alias()}`; + } + return this.arg(); } - return this.value.arg; - } - copyArg(): PlanArgument { - return {type: ParamType.STRING, value: JSON.parse(JSON.stringify(this.value))}; - } + copyArg(): PlanArgument { + this.value.arg = this.arg(); + this.value.alias = this.alias(); + return {type: ParamType.STRING, value: JSON.parse(JSON.stringify(this.value))}; + } } diff --git a/src/app/components/polyalg/models/polyalg-registry.ts b/src/app/components/polyalg/models/polyalg-registry.ts index 91abd627..32199a70 100644 --- a/src/app/components/polyalg/models/polyalg-registry.ts +++ b/src/app/components/polyalg/models/polyalg-registry.ts @@ -54,6 +54,7 @@ export enum OperatorTag { export enum ParamTag { ALIAS = 'ALIAS', ADVANCED = 'ADVANCED', - NON_NEGATIVE = 'NON_NEGATIVE' + NON_NEGATIVE = 'NON_NEGATIVE', + HIDE_TRIVIAL = 'HIDE_TRIVIAL' } diff --git a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts index eafb289a..6a7ae354 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts @@ -17,6 +17,7 @@ import {ContextMenuExtra, ContextMenuPlugin, Presets as ContextMenuPresets} from import {PolyAlgService} from '../polyalg.service'; import {DataModel} from '../../../models/ui-request.model'; import {DataflowEngine} from 'rete-engine'; +import {Position} from 'rete-angular-plugin/17/types'; type Schemes = GetSchemes>; type AreaExtra = AngularArea2D | ContextMenuExtra; @@ -38,9 +39,6 @@ export async function createEditor(container: HTMLElement, injector: Injector, r (p) => Transformers.classic({vertical: true})(p.reverse()) // reverse for correct UP direction ) }); - const contextMenu = new ContextMenuPlugin({ - items: getContextMenuItems(registry, isReadOnly, area) - }); AreaExtensions.selectableNodes(area, AreaExtensions.selector(), { accumulating: AreaExtensions.accumulateOnCtrl() @@ -94,6 +92,16 @@ export async function createEditor(container: HTMLElement, injector: Injector, r } }; }); + const layoutOpts = { + 'elk.direction': 'UP' + }; + + const updateSizeFct = (a: AlgNode, delta: Position) => updateSize(a, delta, area, readonlyPlugin, + () => arrange.layout({applier: undefined, options: layoutOpts})); + + const contextMenu = new ContextMenuPlugin({ + items: getContextMenuItems(registry, isReadOnly, updateSizeFct) + }); editor.use(readonlyPlugin.root); editor.use(area); @@ -111,7 +119,7 @@ export async function createEditor(container: HTMLElement, injector: Injector, r AreaExtensions.simpleNodesOrder(area); addCustomBackground(area); - const [nodes, connections] = addNode(registry, node, isReadOnly, area); + const [nodes, connections] = addNode(registry, node, isReadOnly, updateSizeFct); for (const n of nodes) { await editor.addNode(n); } @@ -120,9 +128,6 @@ export async function createEditor(container: HTMLElement, injector: Injector, r await editor.addConnection(c); } - const layoutOpts = { - 'elk.direction': 'UP' - }; await arrange.layout({ applier, options: layoutOpts }); @@ -161,18 +166,18 @@ export async function createEditor(container: HTMLElement, injector: Injector, r }; } -function addNode(registry: PolyAlgService, node: PlanNode, isReadOnly: boolean, area: AreaPlugin): [AlgNode[], CustomConnection[]] { +function addNode(registry: PolyAlgService, node: PlanNode, isReadOnly: boolean, updateSize: (a: AlgNode, delta: Position) => void): [AlgNode[], CustomConnection[]] { const nodes = []; const connections = []; const algNode = new AlgNode(registry.getDeclaration(node.opName), node.arguments, isReadOnly, - (a: AlgNode) => area.update('node', a.id).then()); + updateSize); if (node.opName.endsWith('#')) { // TODO: handle implicit project correctly algNode.label = 'PROJECT#'; } for (let i = 0; i < node.inputs.length; i++) { - const [childNodes, childConnections] = addNode(registry, node.inputs[i], isReadOnly, area); + const [childNodes, childConnections] = addNode(registry, node.inputs[i], isReadOnly, updateSize); const childNode = childNodes[childNodes.length - 1]; nodes.push(...childNodes); connections.push(...childConnections); @@ -182,15 +187,14 @@ function addNode(registry: PolyAlgService, node: PlanNode, isReadOnly: boolean, return [nodes, connections]; } -function getContextMenuItems(registry: PolyAlgService, isReadOnly: boolean, area: AreaPlugin) { +function getContextMenuItems(registry: PolyAlgService, isReadOnly: boolean, updateSize: (a: AlgNode, delta: Position) => void) { const nodes = []; for (const model of Object.keys(DataModel).map(key => DataModel[key])) { const innerNodes = []; for (const decl of registry.getSortedDeclarations(model)) { innerNodes.push([ decl.name, - () => new AlgNode(decl, null, isReadOnly, - (algNode) => area.update('node', algNode.id).then()) + () => new AlgNode(decl, null, isReadOnly, updateSize) ]); } nodes.push([model, innerNodes]); @@ -221,6 +225,25 @@ function getContextMenuItems(registry: PolyAlgService, isReadOnly: boolean, area }; } +function updateSize(algNode: AlgNode, {x, y}: Position, area: AreaPlugin, + readonlyPlugin: ReadonlyPlugin, + arrange: () => Promise) { + const oldPos = area.nodeViews.get(algNode.id).position; + + // update location of sockets + area.update('node', algNode.id).then( + () => { + const isReadOnly = readonlyPlugin.enabled; + if (isReadOnly) { + readonlyPlugin.disable(); + arrange().then(() => readonlyPlugin.enable()); + } else { + area.translate(algNode.id, {x: oldPos.x + x, y: oldPos.y + y}).then(); + } + } + ); +} + function findRootNodeId(nodes: AlgNode[], connections: CustomConnection[]): string | null { if (connections.length === 0) { if (nodes.length === 1) { From b57c55e6bd08a122a5523b0a5e3897388fdb648a Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Thu, 2 May 2024 18:10:23 +0200 Subject: [PATCH 11/54] Add functionality to execute relational PolyAlg plans --- .../polyalg-viewer/alg-viewer.component.ts | 12 +- .../default-layout.component.html | 2 +- src/app/models/ui-request.model.ts | 11 ++ src/app/services/crud.service.ts | 13 +- .../querying/polyalg/polyalg.component.html | 50 +++++- .../querying/polyalg/polyalg.component.ts | 144 +++++++++++++++++- 6 files changed, 215 insertions(+), 17 deletions(-) diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts index 0472b0d9..49d7ab2b 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts @@ -35,17 +35,15 @@ export class AlgViewerComponent implements AfterViewInit { } ngAfterViewInit(): void { - /*const el = this.container.nativeElement; - - if (el && this.showAlgEditor()) { - createEditor(el, this.injector, JSON.parse(this.planObject) as PlanNode, this.readonly) - .then(editor => this.editor = editor); - }*/ } generatePolyAlg() { - this.editor?.toPolyAlg().then(str => { + this.getPolyAlg().then(str => { this.generatedPolyAlg = str || 'Cannot determine root of tree'; }); } + + getPolyAlg() { + return this.editor?.toPolyAlg(); + } } diff --git a/src/app/containers/default-layout/default-layout.component.html b/src/app/containers/default-layout/default-layout.component.html index 96f7bee5..7a15d607 100644 --- a/src/app/containers/default-layout/default-layout.component.html +++ b/src/app/containers/default-layout/default-layout.component.html @@ -62,7 +62,7 @@
    - PolyAlgebra Editor + PolyPlan Builder
  • diff --git a/src/app/models/ui-request.model.ts b/src/app/models/ui-request.model.ts index df804a57..cb439675 100644 --- a/src/app/models/ui-request.model.ts +++ b/src/app/models/ui-request.model.ts @@ -62,6 +62,17 @@ export class RelAlgRequest extends UIRequest { } } +export class PolyAlgRequest { + type = 'PolyAlgRequest'; + polyAlg: string; + runQuery: boolean; // false if the parsed tree should only be generated, but not executed + + constructor(polyAlg: string, runQuery: boolean) { + this.polyAlg = polyAlg; + this.runQuery = runQuery; + } +} + export class QueryRequest extends UIRequest { type = 'QueryRequest'; query: string; diff --git a/src/app/services/crud.service.ts b/src/app/services/crud.service.ts index b480bc1c..6caaf177 100644 --- a/src/app/services/crud.service.ts +++ b/src/app/services/crud.service.ts @@ -3,7 +3,7 @@ import {HttpClient, HttpHeaders} from '@angular/common/http'; import {WebuiSettingsService} from './webui-settings.service'; import {EntityMeta, IndexModel, ModifyPartitionRequest, PartitionFunctionModel, PartitioningRequest, PathAccessRequest, PlacementFieldsModel} from '../components/data-view/models/result-set.model'; import {webSocket} from 'rxjs/webSocket'; -import {ColumnRequest, ConstraintRequest, DeleteRequest, EditCollectionRequest, EditTableRequest, EntityRequest, ExploreTable, GraphRequest, MaterializedRequest, Method, MonitoringRequest, Namespace, QueryRequest, RelAlgRequest, StatisticRequest} from '../models/ui-request.model'; +import {ColumnRequest, ConstraintRequest, DeleteRequest, EditCollectionRequest, EditTableRequest, EntityRequest, ExploreTable, GraphRequest, MaterializedRequest, Method, MonitoringRequest, Namespace, PolyAlgRequest, QueryRequest, RelAlgRequest, StatisticRequest} from '../models/ui-request.model'; import {AutoDockerResult, AutoDockerStatus, CreateDockerResponse, DockerInstanceInfo, DockerSettings, HandshakeInfo, InstancesAndAutoDocker, UpdateDockerResponse} from '../models/docker.model'; import {ForeignKey, Uml} from '../views/uml/uml.model'; import {Validators} from '@angular/forms'; @@ -660,6 +660,16 @@ export class CrudService { return this._http.get(`${this.httpUrl}/getPolyAlgRegistry`); } + executePolyAlg(socket: WebSocket, polyAlg: string) { + const request = new PolyAlgRequest(polyAlg, true); + return socket.sendMessage(request); + } + + buildTreeFromPolyAlg(socket: WebSocket, polyAlg: string) { + const request = new PolyAlgRequest(polyAlg, false); + return socket.sendMessage(request); + } + getNameValidator(required: boolean = false) { if (required) { return [Validators.pattern('^[a-zA-Z_][a-zA-Z0-9_]*$'), Validators.required, Validators.max(100)]; @@ -713,5 +723,4 @@ export class CrudService { return this._http.post(`${this.httpUrl}/loadPlugins`, formData); } - } diff --git a/src/app/views/querying/polyalg/polyalg.component.html b/src/app/views/querying/polyalg/polyalg.component.html index 2925cd07..451eed05 100644 --- a/src/app/views/querying/polyalg/polyalg.component.html +++ b/src/app/views/querying/polyalg/polyalg.component.html @@ -1,6 +1,50 @@ -
    - -
    + + + +
    + +
    +
    + + +
    + + + + + {{ result().query }} + + ! + + + + +
    + Error: +

    {{ result().error }}

    +
    + +
    +

    + Successfully executed +

    +
    + + + + + + +
    + +
    +
    \ No newline at end of file diff --git a/src/app/views/querying/polyalg/polyalg.component.ts b/src/app/views/querying/polyalg/polyalg.component.ts index e44b55ff..4a8ac2a5 100644 --- a/src/app/views/querying/polyalg/polyalg.component.ts +++ b/src/app/views/querying/polyalg/polyalg.component.ts @@ -1,19 +1,155 @@ -import {Component, OnInit} from '@angular/core'; +import {Component, OnDestroy, OnInit, signal, ViewChild, WritableSignal} from '@angular/core'; import {LeftSidebarService} from '../../../components/left-sidebar/left-sidebar.service'; +import {CrudService} from '../../../services/crud.service'; +import {WebSocket} from '../../../services/webSocket'; +import {RelationalResult, Result} from '../../../components/data-view/models/result-set.model'; +import {AlgViewerComponent} from '../../../components/polyalg/polyalg-viewer/alg-viewer.component'; +import {ToasterService} from '../../../components/toast-exposer/toaster.service'; +import {Subscription} from 'rxjs'; +import {BreadcrumbService} from '../../../components/breadcrumb/breadcrumb.service'; +import {WebuiSettingsService} from '../../../services/webui-settings.service'; +import {InformationObject, InformationPage} from '../../../models/information-page.model'; +import {SidebarNode} from '../../../models/sidebar-node.model'; +import {BreadcrumbItem} from '../../../components/breadcrumb/breadcrumb-item'; @Component({ selector: 'app-polyalg', templateUrl: './polyalg.component.html', styleUrl: './polyalg.component.scss' }) -export class PolyalgComponent implements OnInit { +export class PolyalgComponent implements OnInit, OnDestroy { + + @ViewChild('algViewer') algViewer: AlgViewerComponent; + + websocket: WebSocket; + readonly loading: WritableSignal = signal(false); + result: WritableSignal> = signal(null); + private subscriptions = new Subscription(); + showingAnalysis = false; + queryAnalysis: InformationPage; + samplePlan = '{"opName":"PROJECT","arguments":{"projects":{"type":"LIST","value":{"innerType":"REX","args":[{"type":"REX","value":{"rex":"employeeno","alias":"employeeno"}},{"type":"REX","value":{"rex":"relationshipjoy","alias":"happiness"}}]}}},"inputs":[{"opName":"FILTER","arguments":{"condition":{"type":"REX","value":{"rex":"<(age0, 30)"}},"variables":{"type":"LIST","value":{"innerType":"LIST","args":[]}}},"inputs":[{"opName":"JOIN","arguments":{"type":{"type":"JOIN_TYPE_ENUM","value":{"arg":"INNER","enum":"JoinAlgType"},"isEnum":true},"semiJoinDone":{"type":"BOOLEAN","value":{"arg":false}},"variables":{"type":"LIST","value":{"innerType":"LIST","args":[]}},"condition":{"type":"REX","value":{"rex":"=(employeeno, employeeno0)"}}},"inputs":[{"opName":"SCAN","arguments":{"entity":{"type":"ENTITY","value":{"arg":"public.emp","namespaceId":0,"id":3}}},"inputs":[]},{"opName":"PROJECT#","arguments":{"projects":{"type":"LIST","value":{"innerType":"STRING","args":[{"type":"STRING","value":{"arg":"employeeno","alias":"employeeno0"}},{"type":"STRING","value":{"arg":"age","alias":"age0"}}]}}},"inputs":[{"opName":"PROJECT","arguments":{"projects":{"type":"LIST","value":{"innerType":"REX","args":[{"type":"REX","value":{"rex":"employeeno","alias":"employeeno"}},{"type":"REX","value":{"rex":"age","alias":"age"}}]}}},"inputs":[{"opName":"SCAN","arguments":{"entity":{"type":"ENTITY","value":{"arg":"public.emp","namespaceId":0,"id":3}}},"inputs":[]}]}]}]}]}]}'; - constructor(private _left: LeftSidebarService) { + constructor( + private _crud: CrudService, + private _leftSidebar: LeftSidebarService, + private _toast: ToasterService, + private _breadcrumb: BreadcrumbService, + private _settings: WebuiSettingsService) { + + this.websocket = new WebSocket(); + this.initWebsocket(); } ngOnInit(): void { - this._left.close(); + this._leftSidebar.close(); + } + + ngOnDestroy() { + this._leftSidebar.close(); + this.subscriptions.unsubscribe(); + this.websocket.close(); + } + + async executePolyAlg() { + const polyAlg = await this.algViewer.getPolyAlg(); + if (polyAlg == null) { + this._toast.warn('Plan is invalid'); + return; + } + this._leftSidebar.setNodes([]); + this._leftSidebar.open(); + + this.loading.set(true); + if (!this._crud.executePolyAlg(this.websocket, polyAlg)) { + this.loading.set(false); + this.result.set(new RelationalResult('Could not establish a connection with the server.')); + } + } + + initWebsocket() { + //function to define behavior when clicking on a page link + const nodeBehavior = (tree, node, $event) => { + if (node.data.id === 'polyPlanBuilder') { + //this.queryAnalysis = null; + this.showingAnalysis = false; + this._breadcrumb.hide(); + node.setIsActive(true); + return; + } + const split = node.data.routerLink.split('/'); + const analyzerId = split[0]; + const analyzerPage = split[1]; + if (analyzerId && analyzerPage) { + this._crud.getAnalyzerPage(analyzerId, analyzerPage).subscribe({ + next: res => { + console.log(res); + this.queryAnalysis = res; + this.showingAnalysis = true; + this._breadcrumb.setBreadcrumbs([new BreadcrumbItem(node.data.name)]); + if (this.queryAnalysis.fullWidth) { + this._breadcrumb.hideZoom(); + } + node.setIsActive(true); + }, error: err => { + console.log(err); + } + }); + } + }; + + const sub = this.websocket.onMessage().subscribe({ + next: msg => { + //if msg contains nodes of the sidebar + if (Array.isArray(msg) && msg[0].hasOwnProperty('routerLink')) { + const sidebarNodesTemp: SidebarNode[] = msg; + const sidebarNodes: SidebarNode[] = []; + const labels = new Set(); + sidebarNodesTemp.sort(this._leftSidebar.sortNodes).forEach((s) => { + if (s.label) { + labels.add(s.label); + } else { + sidebarNodes.push(SidebarNode.fromJson(s, {allowRouting: false, action: nodeBehavior})); + } + }); + for (const l of [...labels].sort()) { + sidebarNodes.push(new SidebarNode(l, l).asSeparator()); + sidebarNodesTemp.filter((n) => n.label === l).sort(this._leftSidebar.sortNodes).forEach((n) => { + sidebarNodes.push(SidebarNode.fromJson(n, {allowRouting: false, action: nodeBehavior})); + }); + } + + sidebarNodes.unshift(new SidebarNode('polyPlanBuilder', 'PolyPlan Builder', 'fa fa-cubes').setAction(nodeBehavior)); + + this._leftSidebar.setNodes(sidebarNodes); + if (sidebarNodes.length > 0) { + this._leftSidebar.open(); + } else { + this._leftSidebar.close(); + } + + } else if (msg.hasOwnProperty('data') || msg.hasOwnProperty('affectedTuples') || msg.hasOwnProperty('error')) { // Result + this.loading.set(false); + this.result.set(>msg); + + } else if (msg.hasOwnProperty('type')) { //if msg contains a notification of a changed information object + const iObj = msg; + if (this.queryAnalysis) { + const group = this.queryAnalysis.groups[iObj.groupId]; + if (group != null) { + group.informationObjects[iObj.id] = iObj; + } + } + } + }, + error: err => { + //this._leftSidebar.setError('Lost connection with the server.'); + setTimeout(() => { + this.initWebsocket(); + }, +this._settings.getSetting('reconnection.timeout')); + } + }); + this.subscriptions.add(sub); } } From 43631b36bd88b853c6738429d55b5c11087d4198 Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Mon, 6 May 2024 12:18:10 +0200 Subject: [PATCH 12/54] Update PolyPlan from textual representation --- .../polyalg-viewer/alg-viewer.component.ts | 23 ++++++++++++++----- .../querying/polyalg/polyalg.component.html | 2 +- .../querying/polyalg/polyalg.component.ts | 15 +++++++++++- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts index 49d7ab2b..8eaf1b18 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts @@ -1,4 +1,4 @@ -import {AfterViewInit, Component, computed, effect, ElementRef, Injector, Input, ViewChild} from '@angular/core'; +import {AfterViewInit, Component, computed, effect, ElementRef, Injector, Input, OnChanges, signal, SimpleChanges, ViewChild} from '@angular/core'; import {createEditor} from './alg-editor'; import {PlanNode} from '../models/polyalg-plan.model'; import {PolyAlgService} from '../polyalg.service'; @@ -8,13 +8,14 @@ import {PolyAlgService} from '../polyalg.service'; templateUrl: './alg-viewer.component.html', styleUrl: './alg-viewer.component.scss' }) -export class AlgViewerComponent implements AfterViewInit { +export class AlgViewerComponent implements AfterViewInit, OnChanges { @Input() polyAlg: string; @Input() planObject: string; @Input() planType: 'LOGICAL' | 'ROUTED' | 'PHYSICAL'; @Input() isReadOnly: boolean; @ViewChild('rete') container!: ElementRef; + polyAlgPlan = signal(null); generatedPolyAlg: string; editor: { layout: () => Promise; destroy: () => void; toPolyAlg: () => Promise; }; @@ -24,8 +25,8 @@ export class AlgViewerComponent implements AfterViewInit { effect(() => { const el = this.container.nativeElement; - if (this.showAlgEditor() && el) { - createEditor(el, this.injector, _registry, JSON.parse(this.planObject) as PlanNode, this.isReadOnly) + if (this.showAlgEditor() && this.polyAlgPlan() != null && el) { + createEditor(el, this.injector, _registry, this.polyAlgPlan(), this.isReadOnly) .then(editor => { this.editor = editor; this.generatePolyAlg(); @@ -37,13 +38,23 @@ export class AlgViewerComponent implements AfterViewInit { ngAfterViewInit(): void { } + ngOnChanges(changes: SimpleChanges) { + if (changes.planObject) { + this.polyAlgPlan.set(JSON.parse(this.planObject) as PlanNode); + } + } + generatePolyAlg() { - this.getPolyAlg().then(str => { + this.getPolyAlgFromTree().then(str => { this.generatedPolyAlg = str || 'Cannot determine root of tree'; }); } - getPolyAlg() { + getPolyAlgFromTree() { return this.editor?.toPolyAlg(); } + + getPolyAlgFromText() { + return this.generatedPolyAlg; + } } diff --git a/src/app/views/querying/polyalg/polyalg.component.html b/src/app/views/querying/polyalg/polyalg.component.html index 451eed05..ef23cede 100644 --- a/src/app/views/querying/polyalg/polyalg.component.html +++ b/src/app/views/querying/polyalg/polyalg.component.html @@ -5,7 +5,7 @@
    - + diff --git a/src/app/views/querying/polyalg/polyalg.component.ts b/src/app/views/querying/polyalg/polyalg.component.ts index 4a8ac2a5..f4023be3 100644 --- a/src/app/views/querying/polyalg/polyalg.component.ts +++ b/src/app/views/querying/polyalg/polyalg.component.ts @@ -11,6 +11,7 @@ import {WebuiSettingsService} from '../../../services/webui-settings.service'; import {InformationObject, InformationPage} from '../../../models/information-page.model'; import {SidebarNode} from '../../../models/sidebar-node.model'; import {BreadcrumbItem} from '../../../components/breadcrumb/breadcrumb-item'; +import {PlanNode} from '../../../components/polyalg/models/polyalg-plan.model'; @Component({ selector: 'app-polyalg', @@ -52,7 +53,7 @@ export class PolyalgComponent implements OnInit, OnDestroy { } async executePolyAlg() { - const polyAlg = await this.algViewer.getPolyAlg(); + const polyAlg = await this.algViewer.getPolyAlgFromTree(); if (polyAlg == null) { this._toast.warn('Plan is invalid'); return; @@ -67,6 +68,13 @@ export class PolyalgComponent implements OnInit, OnDestroy { } } + buildPolyPlan() { + const polyAlg = this.algViewer.getPolyAlgFromText(); + if (!this._crud.buildTreeFromPolyAlg(this.websocket, polyAlg)) { + console.log('successfully requested plan for ', polyAlg); + } + } + initWebsocket() { //function to define behavior when clicking on a page link const nodeBehavior = (tree, node, $event) => { @@ -128,6 +136,11 @@ export class PolyalgComponent implements OnInit, OnDestroy { this._leftSidebar.close(); } + } else if (msg.hasOwnProperty('opName')) { + const plan: PlanNode = msg as PlanNode; + console.log(plan); + this.samplePlan = JSON.stringify(plan); + } else if (msg.hasOwnProperty('data') || msg.hasOwnProperty('affectedTuples') || msg.hasOwnProperty('error')) { // Result this.loading.set(false); this.result.set(>msg); From deb7367536a062612a2f1ece1aaa0329573097b1 Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Tue, 7 May 2024 16:06:07 +0200 Subject: [PATCH 13/54] Add textual polyAlg editor --- src/app/components/editor/editor.component.ts | 22 +- .../render-item/render-item.component.html | 2 +- .../polyalg/algnode/alg-node.component.ts | 7 +- .../controls/list-arg/list-arg.component.ts | 2 +- .../polyalg/polyalg-viewer/alg-editor.ts | 29 ++- .../polyalg-viewer/alg-validator.service.ts | 103 +++++++++ .../polyalg-viewer/alg-viewer.component.html | 51 +++-- .../polyalg-viewer/alg-viewer.component.scss | 20 +- .../polyalg-viewer/alg-viewer.component.ts | 215 ++++++++++++++++-- src/app/services/crud.service.ts | 5 +- .../querying/polyalg/polyalg.component.html | 13 +- .../querying/polyalg/polyalg.component.ts | 32 +-- 12 files changed, 407 insertions(+), 94 deletions(-) create mode 100644 src/app/components/polyalg/polyalg-viewer/alg-validator.service.ts diff --git a/src/app/components/editor/editor.component.ts b/src/app/components/editor/editor.component.ts index 974faba3..7c3dc559 100644 --- a/src/app/components/editor/editor.component.ts +++ b/src/app/components/editor/editor.component.ts @@ -1,16 +1,4 @@ -import { - AfterViewInit, - Component, - effect, - ElementRef, - inject, - Input, - OnChanges, - OnInit, - SimpleChanges, - untracked, - ViewChild -} from '@angular/core'; +import {AfterViewInit, Component, effect, ElementRef, inject, Input, OnChanges, OnInit, SimpleChanges, untracked, ViewChild} from '@angular/core'; import * as ace from 'ace-builds'; // ace module .. import 'ace-builds/src-noconflict/mode-sql'; import 'ace-builds/src-noconflict/mode-pgsql'; @@ -204,4 +192,12 @@ export class EditorComponent implements OnInit, AfterViewInit, OnChanges { this.codeEditor.renderer.setScrollMargin(top, bottom, left, right); } + onBlur(callback: (e: Event) => void) { + this.codeEditor.on('blur', callback); + } + + onChange(callback: (delta: any) => void) { + this.codeEditor.on('change', callback); + } + } diff --git a/src/app/components/information-manager/render-item/render-item.component.html b/src/app/components/information-manager/render-item/render-item.component.html index 8d2e7307..1ace9bc7 100644 --- a/src/app/components/information-manager/render-item/render-item.component.html +++ b/src/app/components/information-manager/render-item/render-item.component.html @@ -64,7 +64,7 @@ - + diff --git a/src/app/components/polyalg/algnode/alg-node.component.ts b/src/app/components/polyalg/algnode/alg-node.component.ts index 0ce2d37b..5c8f711d 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.ts +++ b/src/app/components/polyalg/algnode/alg-node.component.ts @@ -46,12 +46,14 @@ export class AlgNodeComponent implements OnChanges { const BASE_WIDTH = 350; const BASE_HEIGHT = 110; +const TAB_SIZE = 2; // the indentation width when generating PolyAlg export class AlgNode extends ClassicPreset.Node { width = BASE_WIDTH; height = BASE_HEIGHT; controlHeights: { [key: string]: number } = {}; numOfInputs = 0; + private readonly tabIndent = ' '.repeat(TAB_SIZE); constructor(public decl: Declaration, args: { [key: string]: PlanArgument } | null, private isReadOnly: boolean, private updateArea: (a: AlgNode, delta: Position) => void) { @@ -122,10 +124,11 @@ export class AlgNode extends ClassicPreset.Node { .map(key => inputs[key]); let children = ''; if (values.length > 0) { - children = `(\n${values.join(',\n')})`; + const indented = values.join(',\n').replace(/^/gm, this.tabIndent); + children = `(\n${indented}\n)`; } - const polyAlg = ` ${this.decl.name}[${args.join(', ')}]${children}`; + const polyAlg = `${this.decl.name}[${args.join(', ')}]${children}`; return {'out': polyAlg}; } diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts index 43299531..e886c68f 100644 --- a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts @@ -71,7 +71,7 @@ export class ListControl extends ArgControl { return '[]'; } - const args = this.children.map(arg => arg.toPolyAlg()).join(', '); + const args = this.children.map(arg => arg.toPolyAlg()).filter(s => s.length > 0).join(', '); if (this.children.length === 1 || this.param.canUnpackValues) { return args; } diff --git a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts index 6a7ae354..a6f95710 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts @@ -18,13 +18,15 @@ import {PolyAlgService} from '../polyalg.service'; import {DataModel} from '../../../models/ui-request.model'; import {DataflowEngine} from 'rete-engine'; import {Position} from 'rete-angular-plugin/17/types'; +import {Subject} from 'rxjs'; type Schemes = GetSchemes>; type AreaExtra = AngularArea2D | ContextMenuExtra; export const SOCKET_PRESET = new ClassicPreset.Socket('socket'); -export async function createEditor(container: HTMLElement, injector: Injector, registry: PolyAlgService, node: PlanNode, isReadOnly: boolean) { +export async function createEditor(container: HTMLElement, injector: Injector, registry: PolyAlgService, node: PlanNode, + isReadOnly: boolean) { const readonlyPlugin = new ReadonlyPlugin(); //const socket = new ClassicPreset.Socket('socket'); @@ -96,8 +98,9 @@ export async function createEditor(container: HTMLElement, injector: Injector, r 'elk.direction': 'UP' }; + const $modifyEvent = new Subject(); const updateSizeFct = (a: AlgNode, delta: Position) => updateSize(a, delta, area, readonlyPlugin, - () => arrange.layout({applier: undefined, options: layoutOpts})); + () => arrange.layout({applier: undefined, options: layoutOpts}), $modifyEvent); const contextMenu = new ContextMenuPlugin({ items: getContextMenuItems(registry, isReadOnly, updateSizeFct) @@ -129,18 +132,20 @@ export async function createEditor(container: HTMLElement, injector: Injector, r } await arrange.layout({ - applier, options: layoutOpts + applier: undefined, options: layoutOpts }); AreaExtensions.zoomAt(area, editor.getNodes()); - /*area.addPipe(context => { - if (context.type === 'nodepicked') { - const node = editor.getNode(context.data.id) - console.log(node, "was selected"); + const modifyingEventTypes = new Set(['nodecreated', 'noderemoved', 'connectioncreated', 'connectionremoved']); + editor.addPipe(context => { + if (modifyingEventTypes.has(context.type)) { + if (context.type !== 'nodecreated' || editor.getNodes().length === 1) { + $modifyEvent.next(); + } } - return context - })*/ + return context; + }); if (isReadOnly) { readonlyPlugin.enable(); // disable interaction with nodes (control interaction is deactivated separately) @@ -162,7 +167,8 @@ export async function createEditor(container: HTMLElement, injector: Injector, r return await engine.fetch(rootId).then(res => res['out']); } return null; - } + }, + onModify: $modifyEvent.asObservable() }; } @@ -227,7 +233,7 @@ function getContextMenuItems(registry: PolyAlgService, isReadOnly: boolean, upda function updateSize(algNode: AlgNode, {x, y}: Position, area: AreaPlugin, readonlyPlugin: ReadonlyPlugin, - arrange: () => Promise) { + arrange: () => Promise, $modifyEvent: Subject) { const oldPos = area.nodeViews.get(algNode.id).position; // update location of sockets @@ -239,6 +245,7 @@ function updateSize(algNode: AlgNode, {x, y}: Position, area: AreaPlugin readonlyPlugin.enable()); } else { area.translate(algNode.id, {x: oldPos.x + x, y: oldPos.y + y}).then(); + $modifyEvent.next(); // size has changed, so the content has probably also changed (e.g. when list item is deleted) } } ); diff --git a/src/app/components/polyalg/polyalg-viewer/alg-validator.service.ts b/src/app/components/polyalg/polyalg-viewer/alg-validator.service.ts new file mode 100644 index 00000000..f5700159 --- /dev/null +++ b/src/app/components/polyalg/polyalg-viewer/alg-validator.service.ts @@ -0,0 +1,103 @@ +import {Injectable} from '@angular/core'; +import {CrudService} from '../../../services/crud.service'; +import {tap} from 'rxjs/operators'; +import {PlanNode} from '../models/polyalg-plan.model'; +import {of} from 'rxjs'; + + +@Injectable({ + providedIn: 'root' +}) +export class AlgValidatorService { + private validPlans = new Map(); + private readonly maxSize = 1000; // FIFO cache + + constructor(private _crud: CrudService) { + } + + setValid(str: string, plan: PlanNode) { + if (this.validPlans.size > this.maxSize) { + const oldestKey = this.validPlans.keys().next().value; + this.validPlans.delete(oldestKey); + } + this.validPlans.set(trimLines(str), JSON.stringify(plan)); + } + + removeValid(str: string) { + this.validPlans.delete(trimLines(str)); + } + + /** + * Might result in false positives if the schema has changed. + * @param str the polyAlg to validate + */ + isConfirmedValid(str: string) { + return this.validPlans.has(trimLines(str)); + } + + /** + * Sufficient, but not necessary condition for a polyAlg to be invalid. + * @param str the polyAlg to check + */ + isInvalid(str: string) { + return !this.areParenthesesBalanced(str); + } + + getCachedPlan(str: string): PlanNode { + const serialized = this.validPlans.get(trimLines(str)); + return serialized ? JSON.parse(serialized) : undefined; + } + + /** + * Returns a plan for the given polyAlg string by either using the cached plan or + * calling the backend. + * If successful, the plan is added to the cache of valid plans. + * @param str + */ + buildPlan(str: string) { + const cachedPlan = this.getCachedPlan(str); + if (cachedPlan) { + return of(cachedPlan); + } + return this._crud.buildTreeFromPolyAlg(str).pipe( + tap({ + next: (plan) => this.setValid(str, plan), + error: () => this.removeValid(str) + }) + ); + } + + areParenthesesBalanced(str: string) { + const stack: string[] = []; + + // TODO: ignore parentheses in quoted text + for (const char of str) { + if (char === '(' || char === '[') { + stack.push(char); + } else if (char === ')') { + const lastOpen = stack.pop(); + if (!lastOpen || lastOpen !== '(') { + return false; + } + } else if (char === ']') { + const lastOpen = stack.pop(); + if (!lastOpen || lastOpen !== '[') { + return false; + } + } + } + + return stack.length === 0; + } + +} + +/** + * Splits the string into lines, trims each line, and then joins them back together. + * Useful for reducing the number of unnecessary recomputations of the plan. + * @param str + */ +export function trimLines(str: string): string { + // Split the string into lines, trim each line, and then join them back together + return str.split('\n').map(line => line.trim()).join('\n'); +} diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html index ee55b6ed..4c029477 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html @@ -1,27 +1,38 @@ - -
    -
    +
    + +
    + + + + -

    Use right-click to add or remove nodes

    - + - - - - -
    {{generatedPolyAlg}}
    -
    -
    - - -
    {{polyAlg}}
    -
    -
    \ No newline at end of file +
    +
    +
    + +

    + Open the context menu to add or remove nodes +

    +
    \ No newline at end of file diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.scss b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.scss index 10fbd924..f763f8a2 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.scss +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.scss @@ -1,4 +1,22 @@ +@import "scss/style.scss"; + .rete { min-height: 800px; - border: 2px solid lightblue; + border: 2px solid; +} + +.text-editor-wrapper { + border: 2px solid; +} + +.CHANGED { + border-color: $warning; +} + +.SYNCHRONIZED { + border-color: lightblue; +} + +.INVALID { + border-color: $danger; } \ No newline at end of file diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts index 8eaf1b18..488698f7 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts @@ -1,60 +1,237 @@ -import {AfterViewInit, Component, computed, effect, ElementRef, Injector, Input, OnChanges, signal, SimpleChanges, ViewChild} from '@angular/core'; +import {AfterViewInit, Component, computed, effect, ElementRef, EventEmitter, Injector, Input, OnChanges, OnDestroy, Output, signal, SimpleChanges, ViewChild} from '@angular/core'; import {createEditor} from './alg-editor'; import {PlanNode} from '../models/polyalg-plan.model'; import {PolyAlgService} from '../polyalg.service'; +import {EditorComponent} from '../../editor/editor.component'; +import {AlgValidatorService, trimLines} from './alg-validator.service'; +import {ToasterService} from '../../toast-exposer/toaster.service'; +import {Observable, Subscription, timer} from 'rxjs'; +import {switchMap} from 'rxjs/operators'; +import {ActivatedRoute, Router} from '@angular/router'; + +type editorState = 'SYNCHRONIZED' | 'CHANGED' | 'INVALID'; @Component({ selector: 'app-alg-viewer', templateUrl: './alg-viewer.component.html', styleUrl: './alg-viewer.component.scss' }) -export class AlgViewerComponent implements AfterViewInit, OnChanges { - @Input() polyAlg: string; - @Input() planObject: string; +export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { + @Input() polyAlg?: string; + @Input() initialPlan?: string; @Input() planType: 'LOGICAL' | 'ROUTED' | 'PHYSICAL'; @Input() isReadOnly: boolean; + @Output() execute = new EventEmitter(); @ViewChild('rete') container!: ElementRef; + @ViewChild('textEditor') textEditor: EditorComponent; + + private polyAlgPlan = signal(null); + textEditorState = signal('SYNCHRONIZED'); + nodeEditorState = signal('SYNCHRONIZED'); + canSyncEditors = computed(() => + (this.textEditorState() === 'SYNCHRONIZED' && this.nodeEditorState() === 'INVALID') || + (this.textEditorState() === 'INVALID' && this.nodeEditorState() === 'SYNCHRONIZED') + ); + isSynchronized = computed(() => this.nodeEditorState() === 'SYNCHRONIZED' && this.textEditorState() === 'SYNCHRONIZED'); + showEditButton: boolean; + + private modifySubscription: Subscription; + nodeEditor: { toPolyAlg: any; layout: () => Promise; destroy: () => void; onModify: Observable; }; + showNodeEditor = computed(() => this._registry.registryLoaded()); + private isNodeFocused = false; // If a node is focused we must assume that a control has changed. Thus, the nodeEditor cannot be 'SYNCHRONIZED'. - polyAlgPlan = signal(null); - generatedPolyAlg: string; + polyAlgSnapshot: string; // keep track whether the textEditor has changed + initialPolyAlg: string; // only used for initially setting the text representation + readonly textEditorOpts = { + minLines: 4, + maxLines: 15, + showLineNumbers: false, + highlightGutterLine: false, + highlightActiveLine: true, + fontSize: '1rem', + tabSize: 2 + }; - editor: { layout: () => Promise; destroy: () => void; toPolyAlg: () => Promise; }; - showAlgEditor = computed(() => this._registry.registryLoaded()); + constructor(private injector: Injector, + private _registry: PolyAlgService, + private _toast: ToasterService, + private _validator: AlgValidatorService, + private _router: Router, + private _route: ActivatedRoute) { - constructor(private injector: Injector, private _registry: PolyAlgService) { effect(() => { const el = this.container.nativeElement; - if (this.showAlgEditor() && this.polyAlgPlan() != null && el) { + if (this.showNodeEditor() && this.polyAlgPlan() != null && el) { + this.modifySubscription?.unsubscribe(); + createEditor(el, this.injector, _registry, this.polyAlgPlan(), this.isReadOnly) .then(editor => { - this.editor = editor; - this.generatePolyAlg(); + this.nodeEditor = editor; + this.generateTextFromNodeEditor(); + this.modifySubscription = this.nodeEditor.onModify.pipe( + switchMap(() => { + this.nodeEditorState.set('CHANGED'); + return timer(500); + }) + ).subscribe(() => this.generateTextFromNodeEditor(true)); }); } }); } ngAfterViewInit(): void { + this.showEditButton = this.isReadOnly && !(this.planType === 'LOGICAL' && this._route.snapshot.params.route === 'polyalg'); + + this.textEditor.setScrollMargin(5, 5); + //this.textEditor.onSelectionChange((f) => console.log(f)); + if (!this.isReadOnly) { + this.textEditor.onBlur(() => this.onTextEditorBlur()); + this.textEditor.onChange(() => this.onTextEditorChange()); + } } ngOnChanges(changes: SimpleChanges) { - if (changes.planObject) { - this.polyAlgPlan.set(JSON.parse(this.planObject) as PlanNode); + if (changes.polyAlg) { + if (!this.polyAlg) { + return; + } + this._validator.buildPlan(this.polyAlg).subscribe({ + next: (plan) => this.polyAlgPlan.set(plan), + error: () => { + this.nodeEditorState.set('INVALID'); + this.textEditorState.set('INVALID'); + this.textEditor.setCode(this.polyAlg); + } + }); + } + + if (changes.initialPlan && !this.polyAlg && !this.polyAlgPlan()) { + this.polyAlgPlan.set(JSON.parse(this.initialPlan)); } } - generatePolyAlg() { + ngOnDestroy() { + this.modifySubscription?.unsubscribe(); + this.nodeEditor?.destroy(); + } + + generateTextFromNodeEditor(validatePlanWithBackend = false) { this.getPolyAlgFromTree().then(str => { - this.generatedPolyAlg = str || 'Cannot determine root of tree'; + if (str) { + if (!this.initialPolyAlg) { + this.initialPolyAlg = str; + } + if (validatePlanWithBackend && !this._validator.isConfirmedValid(str)) { + this._validator.buildPlan(str).subscribe({ + next: () => this.updateTextEditor(str), + error: (err) => { + this.nodeEditorState.set('INVALID'); + this._toast.warn(err.error.errorMsg, 'Invalid PolyAlgebra'); + } + }); + } else { + this.updateTextEditor(str); + } + } else { + this.nodeEditorState.set('INVALID'); + } }); } + private updateTextEditor(str: string) { + this.textEditor.setCode(str); + this.polyAlgSnapshot = trimLines(str); + this.textEditorState.set('SYNCHRONIZED'); + if (!this.isNodeFocused) { + this.nodeEditorState.set('SYNCHRONIZED'); + } + } + + generateNodesFromTextEditor(updatedPolyAlg = this.textEditor.getCode()) { + this._validator.buildPlan(updatedPolyAlg).subscribe({ + next: (plan) => { + this.polyAlgPlan.set(plan); // this has the effect of calling generateTextFromNodeEditor() since the nodeEditor is the sync authority + }, + error: (err) => { + this.textEditorState.set('INVALID'); + this._toast.warn(err.error.errorMsg, 'Invalid PolyAlgebra'); + } + }); + + } + getPolyAlgFromTree() { - return this.editor?.toPolyAlg(); + return this.nodeEditor.toPolyAlg(); + } + + private onTextEditorBlur() { + if (this.isReadOnly) { + return; + } + const updatedPolyAlg = this.textEditor.getCode(); + const trimmed = trimLines(updatedPolyAlg); + if (trimmed === this.polyAlgSnapshot) { + return; + } + + if (this._validator.isInvalid(updatedPolyAlg)) { + this.textEditorState.set('INVALID'); + this._toast.warn('Parentheses are not balanced', 'Invalid PolyAlgebra'); + return; + } + + this.textEditorState.set('CHANGED'); + this.generateNodesFromTextEditor(updatedPolyAlg); + + this.polyAlgSnapshot = trimmed; + } + + private onTextEditorChange() { + if (this.isReadOnly) { + return; + } + const trimmed = trimLines(this.textEditor.getCode()); + if (trimmed !== this.polyAlgSnapshot) { + this.textEditorState.set('CHANGED'); + } else { + this.textEditorState.set('SYNCHRONIZED'); + } + } + + + onNodeEditorBlur() { + if (this.isReadOnly) { + return; + } + console.log('blurred node'); + this.isNodeFocused = false; + this.generateTextFromNodeEditor(true); + } + + onNodeEditorFocus() { + if (this.isReadOnly) { + return; + } + this.isNodeFocused = true; + this.nodeEditorState.set('CHANGED'); } - getPolyAlgFromText() { - return this.generatedPolyAlg; + synchronizeEditors() { + if (this.textEditorState() === 'INVALID') { + this.generateTextFromNodeEditor(); + } else { + this.generateNodesFromTextEditor(); + } + } + + openInPlanEditor() { + if (!this.isReadOnly) { + return; + } + localStorage.setItem('polyalg.polyAlg', this.textEditor.getCode()); + //const params = {polyAlg: this.textEditor.getCode()}; + this._router.navigate(['/views/querying/polyalg']).then(null); + } } diff --git a/src/app/services/crud.service.ts b/src/app/services/crud.service.ts index 6caaf177..b741871e 100644 --- a/src/app/services/crud.service.ts +++ b/src/app/services/crud.service.ts @@ -14,6 +14,7 @@ import {WebSocket} from './webSocket'; import {Observable} from 'rxjs'; import {map} from 'rxjs/operators'; import {PolyAlgRegistry} from '../components/polyalg/models/polyalg-registry'; +import {PlanNode} from '../components/polyalg/models/polyalg-plan.model'; @Injectable({ @@ -665,9 +666,9 @@ export class CrudService { return socket.sendMessage(request); } - buildTreeFromPolyAlg(socket: WebSocket, polyAlg: string) { + buildTreeFromPolyAlg(polyAlg: string) { const request = new PolyAlgRequest(polyAlg, false); - return socket.sendMessage(request); + return this._http.post(`${this.httpUrl}/buildPolyPlan`, request, this.httpOptions); } getNameValidator(required: boolean = false) { diff --git a/src/app/views/querying/polyalg/polyalg.component.html b/src/app/views/querying/polyalg/polyalg.component.html index ef23cede..08fbb10e 100644 --- a/src/app/views/querying/polyalg/polyalg.component.html +++ b/src/app/views/querying/polyalg/polyalg.component.html @@ -1,12 +1,9 @@ - - - -
    - -
    - -
    +
    + +
    { @@ -136,11 +141,6 @@ export class PolyalgComponent implements OnInit, OnDestroy { this._leftSidebar.close(); } - } else if (msg.hasOwnProperty('opName')) { - const plan: PlanNode = msg as PlanNode; - console.log(plan); - this.samplePlan = JSON.stringify(plan); - } else if (msg.hasOwnProperty('data') || msg.hasOwnProperty('affectedTuples') || msg.hasOwnProperty('error')) { // Result this.loading.set(false); this.result.set(>msg); From fa7bac7470f0cae2b10fdb4a95c1d548c657280f Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Tue, 7 May 2024 18:10:30 +0200 Subject: [PATCH 14/54] Use signals for node height --- .../polyalg/algnode/alg-node.component.ts | 43 ++++++++----------- .../controls/agg-arg/agg-arg.component.ts | 7 +-- .../polyalg/controls/arg-control-utils.ts | 4 +- .../polyalg/controls/arg-control.ts | 3 +- .../boolean-arg/boolean-arg.component.ts | 8 ++-- .../collation-arg/collation-arg.component.ts | 7 +-- .../correlation-arg.component.ts | 7 +-- .../entity-arg/entity-arg.component.ts | 8 ++-- .../controls/enum-arg/enum-arg.component.ts | 8 ++-- .../controls/field-arg/field-arg.component.ts | 7 +-- .../controls/int-arg/int-arg.component.ts | 7 +-- .../controls/lax-agg/lax-agg-arg.component.ts | 8 ++-- .../controls/list-arg/list-arg.component.html | 8 ++-- .../controls/list-arg/list-arg.component.ts | 41 ++++++++---------- .../controls/rex-arg/rex-arg.component.ts | 5 +-- .../string-arg/string-arg.component.ts | 5 +-- .../polyalg-viewer/alg-viewer.component.ts | 29 +++++++------ 17 files changed, 83 insertions(+), 122 deletions(-) diff --git a/src/app/components/polyalg/algnode/alg-node.component.ts b/src/app/components/polyalg/algnode/alg-node.component.ts index 5c8f711d..39eb40d2 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.ts +++ b/src/app/components/polyalg/algnode/alg-node.component.ts @@ -1,4 +1,4 @@ -import {ChangeDetectorRef, Component, HostBinding, Input, OnChanges} from '@angular/core'; +import {ChangeDetectorRef, Component, effect, HostBinding, Input, OnChanges, Signal} from '@angular/core'; import {ClassicPreset} from 'rete'; import {KeyValue} from '@angular/common'; import {SOCKET_PRESET} from '../polyalg-viewer/alg-editor'; @@ -28,6 +28,8 @@ export class AlgNodeComponent implements OnChanges { constructor(private cdr: ChangeDetectorRef) { this.cdr.detach(); + + effect(() => this.data.recomputeHeight()); } ngOnChanges(): void { @@ -50,10 +52,10 @@ const TAB_SIZE = 2; // the indentation width when generating PolyAlg export class AlgNode extends ClassicPreset.Node { width = BASE_WIDTH; - height = BASE_HEIGHT; - controlHeights: { [key: string]: number } = {}; + height: number; numOfInputs = 0; private readonly tabIndent = ' '.repeat(TAB_SIZE); + private controlHeights: Signal[] = []; constructor(public decl: Declaration, args: { [key: string]: PlanArgument } | null, private isReadOnly: boolean, private updateArea: (a: AlgNode, delta: Position) => void) { @@ -63,20 +65,15 @@ export class AlgNode extends ClassicPreset.Node { this.addOutput('out', new ClassicPreset.Output(SOCKET_PRESET)); - const heights = {}; + //const heights = {}; for (const p of decl.posParams.concat(decl.kwParams)) { const arg = args?.[p.name] || null; - const c = getControl(p, arg, isReadOnly, (height: number) => { - const oldHeight = this.height; - this.updateControlHeight(p.name, height); - let deltaY = this.height - oldHeight; - deltaY += deltaY > 0 ? 1 : -1; // slight adjustment required for growing node - updateArea(this, {x: 0, y: -deltaY}); - }, p.isMultiValued); - heights[p.name] = c.getHeight(); + const c = getControl(p, arg, isReadOnly, p.isMultiValued); + + this.controlHeights.push(c.height); + this.addControl(p.name, c); } - this.updateControlHeights(heights); if (decl.numInputs > 0) { for (let i = 0; i < decl.numInputs; i++) { @@ -88,19 +85,15 @@ export class AlgNode extends ClassicPreset.Node { } } - updateControlHeights(heights: { [key: string]: number; }) { - this.controlHeights = heights; - this.recomputeHeight(); - } - - updateControlHeight(controlName: string, height: number) { - this.controlHeights[controlName] = height; - this.recomputeHeight(); - } - - private recomputeHeight() { - const sum = Object.values(this.controlHeights).reduce((total, value) => total + value + 12, 0); + recomputeHeight() { + const oldHeight = this.height; + const sum = Object.values(this.controlHeights).reduce((total, value) => total + value() + 12, 0); this.height = BASE_HEIGHT + sum; + if (oldHeight) { + let deltaY = this.height - oldHeight; + deltaY += deltaY > 0 ? 1 : -1; // slight adjustment is required for smooth behavior + this.updateArea(this, {x: 0, y: -deltaY}); + } } data(inputs: { [key: string]: string } = {}) { diff --git a/src/app/components/polyalg/controls/agg-arg/agg-arg.component.ts b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.ts index fdc37823..dc932f95 100644 --- a/src/app/components/polyalg/controls/agg-arg/agg-arg.component.ts +++ b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.ts @@ -1,4 +1,4 @@ -import {Component, Input, OnInit, Type} from '@angular/core'; +import {Component, Input, OnInit, signal, Type} from '@angular/core'; import {ArgControl} from '../arg-control'; import {Parameter, ParamType} from '../../models/polyalg-registry'; import {AggArg, PlanArgument} from '../../models/polyalg-plan.model'; @@ -25,15 +25,12 @@ export class AggArgComponent implements OnInit { export class AggControl extends ArgControl { + height = signal(188); constructor(param: Parameter, public value: AggArg, isReadOnly: boolean) { super(param, isReadOnly); } - getHeight(): number { - return 188; - } - getArgComponent(): Type { return AggArgComponent; } diff --git a/src/app/components/polyalg/controls/arg-control-utils.ts b/src/app/components/polyalg/controls/arg-control-utils.ts index a01f6097..3c3499ca 100644 --- a/src/app/components/polyalg/controls/arg-control-utils.ts +++ b/src/app/components/polyalg/controls/arg-control-utils.ts @@ -15,7 +15,7 @@ import {AggControl} from './agg-arg/agg-arg.component'; import {LaxAggControl} from './lax-agg/lax-agg-arg.component'; export function getControl(param: Parameter, arg: PlanArgument | null, - isReadOnly: boolean, updateHeight: (height: number) => void, isForOuter = false): ArgControl { + isReadOnly: boolean, isForOuter = false): ArgControl { if (arg == null) { arg = getInitialArg(param, isForOuter); } @@ -44,7 +44,7 @@ export function getControl(param: Parameter, arg: PlanArgument | null, case 'FIELD': return new FieldControl(param, arg.value as FieldArg, isReadOnly); case 'LIST': - return new ListControl(param, arg.value as ListArg, isReadOnly, updateHeight); + return new ListControl(param, arg.value as ListArg, isReadOnly); case 'COLLATION': return new CollationControl(param, arg.value as CollationArg, isReadOnly); case 'CORR_ID': diff --git a/src/app/components/polyalg/controls/arg-control.ts b/src/app/components/polyalg/controls/arg-control.ts index 8fa7c676..879f1f8c 100644 --- a/src/app/components/polyalg/controls/arg-control.ts +++ b/src/app/components/polyalg/controls/arg-control.ts @@ -6,14 +6,13 @@ import {PlanArgument} from '../models/polyalg-plan.model'; export abstract class ArgControl extends ClassicPreset.Control { readonly name: string; isTrivial: Signal = signal(false); + height: Signal; protected constructor(public readonly param: Parameter, public isReadOnly: boolean, isForOuter = false) { super(); this.name = param.isMultiValued && !isForOuter ? null : param.name; } - abstract getHeight(): number; - abstract getArgComponent(): Type; abstract toPolyAlg(): string; diff --git a/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts index 6c23fbb0..1dfae7ca 100644 --- a/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts +++ b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts @@ -1,4 +1,4 @@ -import {Component, Input, Type} from '@angular/core'; +import {Component, Input, signal, Type} from '@angular/core'; import {BooleanArg, PlanArgument} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; import {Parameter, ParamType} from '../../models/polyalg-registry'; @@ -14,14 +14,12 @@ export class BooleanArgComponent { } export class BooleanControl extends ArgControl { + height = signal(50); + constructor(param: Parameter, public value: BooleanArg, isReadOnly: boolean) { super(param, isReadOnly); } - getHeight(): number { - return 50; - } - getArgComponent(): Type { return BooleanArgComponent; } diff --git a/src/app/components/polyalg/controls/collation-arg/collation-arg.component.ts b/src/app/components/polyalg/controls/collation-arg/collation-arg.component.ts index 0c30fe86..1d4ccb70 100644 --- a/src/app/components/polyalg/controls/collation-arg/collation-arg.component.ts +++ b/src/app/components/polyalg/controls/collation-arg/collation-arg.component.ts @@ -1,4 +1,4 @@ -import {Component, Input, Type} from '@angular/core'; +import {Component, Input, signal, Type} from '@angular/core'; import {ArgControl} from '../arg-control'; import {Parameter, ParamType} from '../../models/polyalg-registry'; import {CollationArg, CollDirection, CollNullDirection, defaultNullDirection, PlanArgument} from '../../models/polyalg-plan.model'; @@ -18,6 +18,7 @@ export class CollationArgComponent { } export class CollationControl extends ArgControl { + height = signal(101); constructor(param: Parameter, public value: CollationArg, isReadOnly: boolean) { super(param, isReadOnly); @@ -35,10 +36,6 @@ export class CollationControl extends ArgControl { return str; } - getHeight(): number { - return 101; - } - getArgComponent(): Type { return CollationArgComponent; } diff --git a/src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.ts b/src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.ts index 474b6d9c..3876949e 100644 --- a/src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.ts +++ b/src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.ts @@ -1,4 +1,4 @@ -import {Component, Input, Type} from '@angular/core'; +import {Component, Input, signal, Type} from '@angular/core'; import {ArgControl} from '../arg-control'; import {Parameter, ParamType} from '../../models/polyalg-registry'; import {CorrelationArg, PlanArgument} from '../../models/polyalg-plan.model'; @@ -14,15 +14,12 @@ export class CorrelationArgComponent { } export class CorrelationControl extends ArgControl { + height = signal(this.name ? 55 : 31); constructor(param: Parameter, public value: CorrelationArg, isReadOnly: boolean) { super(param, isReadOnly); } - getHeight(): number { - return this.name ? 55 : 31; - } - getArgComponent(): Type { return CorrelationArgComponent; } diff --git a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts index e00efa36..780ab1c4 100644 --- a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts +++ b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts @@ -1,4 +1,4 @@ -import {Component, Input, Type} from '@angular/core'; +import {Component, Input, signal, Type} from '@angular/core'; import {EntityArg, PlanArgument} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; import {Parameter, ParamType} from '../../models/polyalg-registry'; @@ -13,14 +13,12 @@ export class EntityArgComponent { } export class EntityControl extends ArgControl { + height = signal(this.name ? 55 : 31); + constructor(param: Parameter, public value: EntityArg, isReadOnly: boolean) { super(param, isReadOnly); } - getHeight(): number { - return this.name ? 55 : 31; - } - getArgComponent(): Type { return EntityArgComponent; } diff --git a/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts b/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts index cc43c1a5..91f8a5ed 100644 --- a/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts +++ b/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts @@ -1,4 +1,4 @@ -import {Component, Input, OnInit, Type} from '@angular/core'; +import {Component, Input, OnInit, signal, Type} from '@angular/core'; import {ArgControl} from '../arg-control'; import {EnumArg, PlanArgument} from '../../models/polyalg-plan.model'; import {PolyAlgService} from '../../polyalg.service'; @@ -23,14 +23,12 @@ export class EnumArgComponent implements OnInit { } export class EnumControl extends ArgControl { + height = signal(this.name ? 55 : 31); + constructor(param: Parameter, public type: string, public value: EnumArg, isReadOnly: boolean) { super(param, isReadOnly); } - getHeight(): number { - return this.name ? 55 : 31; - } - getArgComponent(): Type { return EnumArgComponent; } diff --git a/src/app/components/polyalg/controls/field-arg/field-arg.component.ts b/src/app/components/polyalg/controls/field-arg/field-arg.component.ts index 14238a6d..16c0f934 100644 --- a/src/app/components/polyalg/controls/field-arg/field-arg.component.ts +++ b/src/app/components/polyalg/controls/field-arg/field-arg.component.ts @@ -1,4 +1,4 @@ -import {Component, Input, Type} from '@angular/core'; +import {Component, Input, signal, Type} from '@angular/core'; import {ArgControl} from '../arg-control'; import {Parameter, ParamType} from '../../models/polyalg-registry'; import {FieldArg, PlanArgument} from '../../models/polyalg-plan.model'; @@ -14,15 +14,12 @@ export class FieldArgComponent { } export class FieldControl extends ArgControl { + height = signal(this.name ? 55 : 31); constructor(param: Parameter, public value: FieldArg, isReadOnly: boolean) { super(param, isReadOnly); } - getHeight(): number { - return this.name ? 55 : 31; - } - getArgComponent(): Type { return FieldArgComponent; } diff --git a/src/app/components/polyalg/controls/int-arg/int-arg.component.ts b/src/app/components/polyalg/controls/int-arg/int-arg.component.ts index 3b56832b..8886db86 100644 --- a/src/app/components/polyalg/controls/int-arg/int-arg.component.ts +++ b/src/app/components/polyalg/controls/int-arg/int-arg.component.ts @@ -1,4 +1,4 @@ -import {Component, Input, Type} from '@angular/core'; +import {Component, Input, signal, Type} from '@angular/core'; import {ArgControl} from '../arg-control'; import {Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; import {IntArg, PlanArgument} from '../../models/polyalg-plan.model'; @@ -14,6 +14,7 @@ export class IntArgComponent { export class IntControl extends ArgControl { valueRange: { min: number | null; max: number | null; }; + height = signal(this.name ? 55 : 31); constructor(param: Parameter, public value: IntArg, isReadOnly: boolean) { super(param, isReadOnly); @@ -24,10 +25,6 @@ export class IntControl extends ArgControl { } } - getHeight(): number { - return this.name ? 55 : 31; - } - getArgComponent(): Type { return IntArgComponent; } diff --git a/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts b/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts index 8bb98717..14975da7 100644 --- a/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts +++ b/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts @@ -1,4 +1,4 @@ -import {Component, Input, OnInit, Type} from '@angular/core'; +import {Component, Input, OnInit, signal, Type} from '@angular/core'; import {ArgControl} from '../arg-control'; import {Parameter, ParamType} from '../../models/polyalg-registry'; import {LaxAggArg, PlanArgument} from '../../models/polyalg-plan.model'; @@ -23,14 +23,12 @@ export class LaxAggArgComponent implements OnInit { } export class LaxAggControl extends ArgControl { + height = signal(this.name ? 125 : 101); + constructor(param: Parameter, public value: LaxAggArg, isReadOnly: boolean) { super(param, isReadOnly); } - getHeight(): number { - return this.name ? 125 : 101; - } - getArgComponent(): Type { return LaxAggArgComponent; } diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.html b/src/app/components/polyalg/controls/list-arg/list-arg.component.html index 5da1d0b1..34804522 100644 --- a/src/app/components/polyalg/controls/list-arg/list-arg.component.html +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.html @@ -2,13 +2,13 @@
      - -
    • + +
    • @@ -53,7 +53,7 @@
    • -
    • +
    • - + + + +
      + + +
      +
      + + + + +
      + +

      + Open the context menu to add or remove nodes +

      +
      +
      + + \ No newline at end of file diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.scss b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.scss index f763f8a2..2e20c0d2 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.scss +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.scss @@ -10,13 +10,33 @@ } .CHANGED { - border-color: $warning; + border-color: $warning-50; } +button.accordion-button.CHANGED { + background-color: $warning-50 !important; +} + + .SYNCHRONIZED { border-color: lightblue; } .INVALID { - border-color: $danger; + border-color: $danger-50; +} + +button.accordion-button.INVALID { + background-color: $danger-50 !important; +} + +button.accordion-button { + box-shadow: none !important; + outline: none; + color: $body-color !important; + background-color: white !important; +} + +button.accordion-button::after { + background-image: var(--cui-accordion-btn-icon) !important; } \ No newline at end of file diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts index 5688e4b2..e1b24689 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts @@ -5,9 +5,10 @@ import {PolyAlgService} from '../polyalg.service'; import {EditorComponent} from '../../editor/editor.component'; import {AlgValidatorService, trimLines} from './alg-validator.service'; import {ToasterService} from '../../toast-exposer/toaster.service'; -import {Observable, Subscription, timer} from 'rxjs'; +import {Subscription, timer} from 'rxjs'; import {switchMap} from 'rxjs/operators'; import {ActivatedRoute, Router} from '@angular/router'; +import {DataModel} from '../../../models/ui-request.model'; type editorState = 'SYNCHRONIZED' | 'CHANGED' | 'INVALID'; @@ -21,13 +22,20 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { @Input() initialPlan?: string; @Input() planType: 'LOGICAL' | 'ROUTED' | 'PHYSICAL'; @Input() isReadOnly: boolean; - @Output() execute = new EventEmitter(); + @Output() execute = new EventEmitter<[string, DataModel]>(); @ViewChild('rete') container!: ElementRef; @ViewChild('textEditor') textEditor: EditorComponent; private polyAlgPlan = signal(undefined); // null: empty plan textEditorState = signal('SYNCHRONIZED'); + textEditorError = signal(''); nodeEditorState = signal('SYNCHRONIZED'); + nodeEditorError = signal(''); + readonly stateText = { + 'SYNCHRONIZED': '', + 'CHANGED': ' (edited)', + 'INVALID': ' (invalid)' + }; canSyncEditors = computed(() => (this.textEditorState() === 'SYNCHRONIZED' && this.nodeEditorState() === 'INVALID') || (this.textEditorState() === 'INVALID' && this.nodeEditorState() === 'SYNCHRONIZED') @@ -36,7 +44,7 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { showEditButton: boolean; private modifySubscription: Subscription; - nodeEditor: { toPolyAlg: any; layout: () => Promise; destroy: () => void; onModify: Observable; }; + nodeEditor: { onModify: any; destroy: any; toPolyAlg: any; layout?: () => Promise; }; showNodeEditor = computed(() => this._registry.registryLoaded()); private isNodeFocused = false; // If a node is focused we must assume that a control has changed. Thus, the nodeEditor cannot be 'SYNCHRONIZED'. @@ -118,7 +126,7 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { } generateTextFromNodeEditor(validatePlanWithBackend = false) { - this.getPolyAlgFromTree().then(str => { + this.getPolyAlgFromTree().then(([str, model]) => { if (str != null) { if (!this.initialPolyAlg) { this.initialPolyAlg = str; @@ -128,8 +136,7 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { next: () => this.updateTextEditor(str), error: (err) => { this.nodeEditorState.set('INVALID'); - console.log('Invalid PolyAlgebra: ' + err.error.errorMsg); - //this._toast.warn(err.error.errorMsg, 'Invalid PolyAlgebra'); + this.nodeEditorError.set(err.error.errorMsg); } }); } else { @@ -137,6 +144,7 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { } } else { this.nodeEditorState.set('INVALID'); + this.nodeEditorError.set('Invalid plan structure'); } }); } @@ -157,13 +165,13 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { }, error: (err) => { this.textEditorState.set('INVALID'); - this._toast.warn(err.error.errorMsg, 'Invalid PolyAlgebra'); + this.textEditorError.set(err.error.errorMsg); } }); } - getPolyAlgFromTree() { + getPolyAlgFromTree(): Promise<[string, DataModel]> { return this.nodeEditor.toPolyAlg(); } @@ -236,9 +244,10 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { } executePlan() { - const code = this.textEditor.getCode(); - if (code.length > 0) { - this.execute.emit(code); - } + this.getPolyAlgFromTree().then(([str, model]) => { + if (str.length > 0 && model) { + this.execute.emit([str, model]); + } + }); } } diff --git a/src/app/components/polyalg/polyalg-viewer/magnetic-connection/index.ts b/src/app/components/polyalg/polyalg-viewer/magnetic-connection/index.ts index d6bde64d..82203ef5 100644 --- a/src/app/components/polyalg/polyalg-viewer/magnetic-connection/index.ts +++ b/src/app/components/polyalg/polyalg-viewer/magnetic-connection/index.ts @@ -1,10 +1,6 @@ import {NodeEditor} from 'rete'; import {Area2D, AreaPlugin} from 'rete-area-plugin'; -import { - ConnectionPlugin, - SocketData, - createPseudoconnection -} from 'rete-connection-plugin'; +import {ConnectionPlugin, createPseudoconnection, SocketData} from 'rete-connection-plugin'; import {getElementCenter} from 'rete-render-utils'; import {Position, Schemes} from './types'; import {findNearestPoint, isInsideRect} from './math'; @@ -87,7 +83,6 @@ export function useMagneticConnection(connection: ); nearestSocket = findNearestPoint(socketsPositions, point, distance) || null; - if (nearestSocket && props.display(picked, nearestSocket)) { if (!magneticConnection.isMounted()) { magneticConnection.mount(area); diff --git a/src/app/components/polyalg/polyalg-viewer/magnetic-connection/magnetic-connection.component.scss b/src/app/components/polyalg/polyalg-viewer/magnetic-connection/magnetic-connection.component.scss index 84bf40c4..a484b50f 100644 --- a/src/app/components/polyalg/polyalg-viewer/magnetic-connection/magnetic-connection.component.scss +++ b/src/app/components/polyalg/polyalg-viewer/magnetic-connection/magnetic-connection.component.scss @@ -1,3 +1,5 @@ +@import "scss/style.scss"; + svg { overflow: visible !important; position: absolute; @@ -8,7 +10,7 @@ svg { path { fill: none; stroke-width: 5px; - stroke: lightblue; + stroke: rgba($primary, 0.5); stroke-dasharray: 10 10; pointer-events: auto; } diff --git a/src/app/models/ui-request.model.ts b/src/app/models/ui-request.model.ts index cb439675..d7bde375 100644 --- a/src/app/models/ui-request.model.ts +++ b/src/app/models/ui-request.model.ts @@ -62,14 +62,16 @@ export class RelAlgRequest extends UIRequest { } } -export class PolyAlgRequest { +export class PolyAlgRequest extends UIRequest { type = 'PolyAlgRequest'; polyAlg: string; - runQuery: boolean; // false if the parsed tree should only be generated, but not executed + model: DataModel; + noLimit = false; // TODO: handle queries with large results - constructor(polyAlg: string, runQuery: boolean) { + constructor(polyAlg: string, model: DataModel) { + super(); this.polyAlg = polyAlg; - this.runQuery = runQuery; + this.model = model; } } diff --git a/src/app/services/crud.service.ts b/src/app/services/crud.service.ts index b741871e..eef57515 100644 --- a/src/app/services/crud.service.ts +++ b/src/app/services/crud.service.ts @@ -3,7 +3,7 @@ import {HttpClient, HttpHeaders} from '@angular/common/http'; import {WebuiSettingsService} from './webui-settings.service'; import {EntityMeta, IndexModel, ModifyPartitionRequest, PartitionFunctionModel, PartitioningRequest, PathAccessRequest, PlacementFieldsModel} from '../components/data-view/models/result-set.model'; import {webSocket} from 'rxjs/webSocket'; -import {ColumnRequest, ConstraintRequest, DeleteRequest, EditCollectionRequest, EditTableRequest, EntityRequest, ExploreTable, GraphRequest, MaterializedRequest, Method, MonitoringRequest, Namespace, PolyAlgRequest, QueryRequest, RelAlgRequest, StatisticRequest} from '../models/ui-request.model'; +import {ColumnRequest, ConstraintRequest, DataModel, DeleteRequest, EditCollectionRequest, EditTableRequest, EntityRequest, ExploreTable, GraphRequest, MaterializedRequest, Method, MonitoringRequest, Namespace, PolyAlgRequest, QueryRequest, RelAlgRequest, StatisticRequest} from '../models/ui-request.model'; import {AutoDockerResult, AutoDockerStatus, CreateDockerResponse, DockerInstanceInfo, DockerSettings, HandshakeInfo, InstancesAndAutoDocker, UpdateDockerResponse} from '../models/docker.model'; import {ForeignKey, Uml} from '../views/uml/uml.model'; import {Validators} from '@angular/forms'; @@ -661,13 +661,13 @@ export class CrudService { return this._http.get(`${this.httpUrl}/getPolyAlgRegistry`); } - executePolyAlg(socket: WebSocket, polyAlg: string) { - const request = new PolyAlgRequest(polyAlg, true); + executePolyAlg(socket: WebSocket, polyAlg: string, model: DataModel) { + const request = new PolyAlgRequest(polyAlg, model); return socket.sendMessage(request); } buildTreeFromPolyAlg(polyAlg: string) { - const request = new PolyAlgRequest(polyAlg, false); + const request = new PolyAlgRequest(polyAlg, DataModel.RELATIONAL); // datamodel doesn't matter when building the plan return this._http.post(`${this.httpUrl}/buildPolyPlan`, request, this.httpOptions); } diff --git a/src/app/views/querying/polyalg/polyalg.component.ts b/src/app/views/querying/polyalg/polyalg.component.ts index fac524f6..dbf8aaed 100644 --- a/src/app/views/querying/polyalg/polyalg.component.ts +++ b/src/app/views/querying/polyalg/polyalg.component.ts @@ -11,6 +11,7 @@ import {WebuiSettingsService} from '../../../services/webui-settings.service'; import {InformationObject, InformationPage} from '../../../models/information-page.model'; import {SidebarNode} from '../../../models/sidebar-node.model'; import {BreadcrumbItem} from '../../../components/breadcrumb/breadcrumb-item'; +import {DataModel} from '../../../models/ui-request.model'; @Component({ selector: 'app-polyalg', @@ -28,8 +29,7 @@ export class PolyalgComponent implements OnInit, OnDestroy { showingAnalysis = false; queryAnalysis: InformationPage; - polyAlg = ` - PROJECT[employeeno, relationshipjoy AS happiness]( + polyAlg = `PROJECT[employeeno, relationshipjoy AS happiness]( FILTER[<(age0, 30)]( JOIN[=(employeeno, employeeno0)]( SCAN[public.emp], @@ -65,7 +65,7 @@ export class PolyalgComponent implements OnInit, OnDestroy { this.websocket.close(); } - executePolyAlg(polyAlg: string) { + executePolyAlg([polyAlg, model]: [string, DataModel]) { if (polyAlg == null) { this._toast.warn('Plan is invalid'); return; @@ -74,7 +74,7 @@ export class PolyalgComponent implements OnInit, OnDestroy { this._leftSidebar.open(); this.loading.set(true); - if (!this._crud.executePolyAlg(this.websocket, polyAlg)) { + if (!this._crud.executePolyAlg(this.websocket, polyAlg, model)) { this.loading.set(false); this.result.set(new RelationalResult('Could not establish a connection with the server.')); } From 8904986c57e7b7dccc896455171cd23fb3a1bea6 Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Fri, 10 May 2024 19:34:53 +0200 Subject: [PATCH 19/54] Add support for nested ListArgs --- .../polyalg/algnode/alg-node.component.ts | 3 +- .../polyalg/controls/arg-control-utils.ts | 30 +++++++++++-------- .../polyalg/controls/arg-control.ts | 4 +-- .../controls/list-arg/list-arg.component.html | 21 ++++++++----- .../controls/list-arg/list-arg.component.ts | 11 +++---- .../polyalg/models/polyalg-registry.ts | 2 +- .../polyalg/polyalg-viewer/alg-editor.ts | 6 ++++ 7 files changed, 48 insertions(+), 29 deletions(-) diff --git a/src/app/components/polyalg/algnode/alg-node.component.ts b/src/app/components/polyalg/algnode/alg-node.component.ts index 2585d228..214bb000 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.ts +++ b/src/app/components/polyalg/algnode/alg-node.component.ts @@ -77,10 +77,9 @@ export class AlgNode extends ClassicPreset.Node { output.multipleConnections = false; this.addOutput('out', output); - //const heights = {}; for (const p of decl.posParams.concat(decl.kwParams)) { const arg = args?.[p.name] || null; - const c = getControl(p, arg, isReadOnly, p.isMultiValued); + const c = getControl(p, arg, isReadOnly, 0); this.controlHeights.push(c.height); diff --git a/src/app/components/polyalg/controls/arg-control-utils.ts b/src/app/components/polyalg/controls/arg-control-utils.ts index 3c3499ca..0edd1605 100644 --- a/src/app/components/polyalg/controls/arg-control-utils.ts +++ b/src/app/components/polyalg/controls/arg-control-utils.ts @@ -15,9 +15,9 @@ import {AggControl} from './agg-arg/agg-arg.component'; import {LaxAggControl} from './lax-agg/lax-agg-arg.component'; export function getControl(param: Parameter, arg: PlanArgument | null, - isReadOnly: boolean, isForOuter = false): ArgControl { + isReadOnly: boolean, depth: number): ArgControl { if (arg == null) { - arg = getInitialArg(param, isForOuter); + arg = getInitialArg(param, depth); } if (arg.isEnum) { @@ -44,7 +44,7 @@ export function getControl(param: Parameter, arg: PlanArgument | null, case 'FIELD': return new FieldControl(param, arg.value as FieldArg, isReadOnly); case 'LIST': - return new ListControl(param, arg.value as ListArg, isReadOnly); + return new ListControl(param, arg.value as ListArg, depth, isReadOnly); case 'COLLATION': return new CollationControl(param, arg.value as CollationArg, isReadOnly); case 'CORR_ID': @@ -53,24 +53,30 @@ export function getControl(param: Parameter, arg: PlanArgument | null, return new StringControl(param, {'arg': JSON.stringify(arg)}, isReadOnly); } -export function getInitialArg(p: Parameter, isForOuter: boolean): PlanArgument { - if (p.defaultValue && !p.isMultiValued) { + +export function getInitialArg(p: Parameter, depth: number): PlanArgument { + if (p.defaultValue && p.multiValued === 0) { return p.defaultValue; // kwParams always have a defaultValue } - const isListArg = p.isMultiValued && isForOuter; + const isListArg = depth < p.multiValued; // not yet on the depth of the actual argument - if (isListArg && p.defaultValue) { - if ((p.defaultValue?.value as ListArg).args.length === 0) { - // Handle case of EMPTY_LIST - return {type: ParamType.LIST, value: {innerType: p.type, args: []}}; + if (isListArg && depth === 0) { + // we're at the outermost list + if (p.defaultValue) { + if ((p.defaultValue?.value as ListArg).args.length === 0) { + // Handle case of EMPTY_LIST + const innerType = p.multiValued > 1 ? ParamType.LIST : p.type; + return {type: ParamType.LIST, value: {innerType: innerType, args: []}}; + } + return p.defaultValue; } - return p.defaultValue; } return { type: isListArg ? ParamType.LIST : p.type, value: (function () { if (isListArg) { - return {innerType: p.type, args: [getInitialArg(p, false)]}; + const innerType = p.multiValued > depth + 1 ? ParamType.LIST : p.type; + return {innerType: innerType, args: [getInitialArg(p, depth + 1)]}; } if (p.isEnum) { return {arg: ''}; diff --git a/src/app/components/polyalg/controls/arg-control.ts b/src/app/components/polyalg/controls/arg-control.ts index 879f1f8c..9de5aff3 100644 --- a/src/app/components/polyalg/controls/arg-control.ts +++ b/src/app/components/polyalg/controls/arg-control.ts @@ -8,9 +8,9 @@ export abstract class ArgControl extends ClassicPreset.Control { isTrivial: Signal = signal(false); height: Signal; - protected constructor(public readonly param: Parameter, public isReadOnly: boolean, isForOuter = false) { + protected constructor(public readonly param: Parameter, public isReadOnly: boolean, enforceName = false) { super(); - this.name = param.isMultiValued && !isForOuter ? null : param.name; + this.name = param.multiValued > 0 && !enforceName ? null : param.name; } abstract getArgComponent(): Type; diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.html b/src/app/components/polyalg/controls/list-arg/list-arg.component.html index 7a0a2eb9..d5c892bf 100644 --- a/src/app/components/polyalg/controls/list-arg/list-arg.component.html +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.html @@ -1,4 +1,4 @@ - + -
        +
          -
        • +
        • @@ -42,6 +42,10 @@ + + + +

          {{data.value.innerType}}

          {{child}}

          @@ -53,13 +57,16 @@
        • -
        • +
        • - +
          -
        diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts index 2fa1ca93..46c3a99f 100644 --- a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts @@ -22,10 +22,10 @@ export class ListControl extends ArgControl { hideTrivial: WritableSignal; height = computed(() => this.computeHeight()); - constructor(param: Parameter, public value: ListArg, + constructor(param: Parameter, public value: ListArg, public depth: number, isReadOnly: boolean) { - super(param, isReadOnly, true); - this.children = signal(value.args.map(arg => getControl(param, arg, isReadOnly))); + super(param, isReadOnly, depth === 0); + this.children = signal(value.args.map(arg => getControl(param, arg, isReadOnly, depth + 1))); if (this.children().length === 0 && value.innerType === ParamType.LIST) { value.innerType = param.type; // TODO: handle nested lists } @@ -47,7 +47,8 @@ export class ListControl extends ArgControl { addElement() { this.hideTrivial.set(false); - this.children.update(values => [...values, getControl(this.param, null, this.isReadOnly, false)]); + this.children.update(values => + [...values, getControl(this.param, null, this.isReadOnly, this.depth + 1)]); } removeElement(child: ArgControl) { @@ -69,7 +70,7 @@ export class ListControl extends ArgControl { } const args = this.children().map(arg => arg.toPolyAlg()).filter(s => s.length > 0).join(', '); - if (this.children().length === 1 || this.param.canUnpackValues) { + if (this.param.multiValued === 1 && (this.children().length === 1 || this.param.canUnpackValues)) { return args; } return `[${args}]`; diff --git a/src/app/components/polyalg/models/polyalg-registry.ts b/src/app/components/polyalg/models/polyalg-registry.ts index 32199a70..dcd136bc 100644 --- a/src/app/components/polyalg/models/polyalg-registry.ts +++ b/src/app/components/polyalg/models/polyalg-registry.ts @@ -22,7 +22,7 @@ export interface Parameter { tags: ParamTag[]; type: ParamType; // if isEnum, then type identifies the type of enum and is not a ParamType isEnum: boolean; - isMultiValued: boolean; + multiValued: number; // how deeply nested arguments can be in lists (0 = not nested at all) requiresAlias: boolean; defaultValue?: PlanArgument; defaultPolyAlg?: string; diff --git a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts index 33e5ed22..c8f97a00 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts @@ -208,6 +208,12 @@ export async function createEditor(container: HTMLElement, injector: Injector, r } return context; }); + area.addPipe(context => { + if (context.type === 'zoom' && context.data.source === 'dblclick') { + return; // https://github.com/retejs/rete/issues/204 + } + return context; + }); if (isReadOnly) { readonlyPlugin.enable(); // disable interaction with nodes (control interaction is deactivated separately) From cfa6b62e838d0afa0b2ad8e2ef058bc88fcea409 Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Sat, 11 May 2024 17:42:31 +0200 Subject: [PATCH 20/54] Introduce simple mode that hides advanced operators --- src/app/components/components.module.ts | 3 +- .../polyalg/algnode/alg-node.component.ts | 2 +- .../controls/agg-arg/agg-arg.component.ts | 5 +- .../polyalg/controls/arg-control-utils.ts | 29 ++++---- .../polyalg/controls/arg-control.ts | 3 +- .../boolean-arg/boolean-arg.component.ts | 5 +- .../collation-arg/collation-arg.component.ts | 5 +- .../correlation-arg.component.ts | 5 +- .../entity-arg/entity-arg.component.html | 2 +- .../entity-arg/entity-arg.component.ts | 16 +++-- .../controls/enum-arg/enum-arg.component.ts | 5 +- .../controls/field-arg/field-arg.component.ts | 5 +- .../controls/int-arg/int-arg.component.ts | 5 +- .../controls/lax-agg/lax-agg-arg.component.ts | 5 +- .../controls/list-arg/list-arg.component.ts | 9 +-- .../controls/rex-arg/rex-arg.component.ts | 5 +- .../string-arg/string-arg.component.ts | 5 +- .../polyalg/polyalg-viewer/alg-editor.ts | 21 +++++- .../polyalg-viewer/alg-viewer.component.html | 36 +++++++--- .../polyalg-viewer/alg-viewer.component.ts | 68 +++++++++++-------- src/app/components/polyalg/polyalg.service.ts | 7 +- src/scss/style.scss | 14 ++-- 22 files changed, 161 insertions(+), 99 deletions(-) diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts index ed514781..d6bad922 100644 --- a/src/app/components/components.module.ts +++ b/src/app/components/components.module.ts @@ -12,6 +12,7 @@ import { ButtonCloseDirective, ButtonDirective, ButtonGroupComponent, + ButtonToolbarComponent, CardBodyComponent, CardComponent, CardFooterComponent, @@ -207,7 +208,7 @@ import {PopoverModule} from 'ngx-bootstrap/popover'; ReteModule, AutocompleteLibModule, FormCheckComponent, - FormCheckLabelDirective, AccordionComponent, AccordionItemComponent, AccordionButtonDirective, TemplateIdDirective, CollapseDirective, PopoverDirective, PopoverModule + FormCheckLabelDirective, AccordionComponent, AccordionItemComponent, AccordionButtonDirective, TemplateIdDirective, CollapseDirective, PopoverDirective, PopoverModule, ButtonToolbarComponent ], declarations: [ BreadcrumbComponent, diff --git a/src/app/components/polyalg/algnode/alg-node.component.ts b/src/app/components/polyalg/algnode/alg-node.component.ts index 214bb000..b11e9522 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.ts +++ b/src/app/components/polyalg/algnode/alg-node.component.ts @@ -79,7 +79,7 @@ export class AlgNode extends ClassicPreset.Node { for (const p of decl.posParams.concat(decl.kwParams)) { const arg = args?.[p.name] || null; - const c = getControl(p, arg, isReadOnly, 0); + const c = getControl(p, arg, isReadOnly, 0, decl.model); this.controlHeights.push(c.height); diff --git a/src/app/components/polyalg/controls/agg-arg/agg-arg.component.ts b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.ts index dc932f95..dc6ecb3a 100644 --- a/src/app/components/polyalg/controls/agg-arg/agg-arg.component.ts +++ b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.ts @@ -4,6 +4,7 @@ import {Parameter, ParamType} from '../../models/polyalg-registry'; import {AggArg, PlanArgument} from '../../models/polyalg-plan.model'; import {CollationControl} from '../collation-arg/collation-arg.component'; import {PolyAlgService} from '../../polyalg.service'; +import {DataModel} from '../../../../models/ui-request.model'; @Component({ selector: 'app-agg-arg', @@ -27,8 +28,8 @@ export class AggArgComponent implements OnInit { export class AggControl extends ArgControl { height = signal(188); - constructor(param: Parameter, public value: AggArg, isReadOnly: boolean) { - super(param, isReadOnly); + constructor(param: Parameter, public value: AggArg, model: DataModel, isReadOnly: boolean) { + super(param, model, isReadOnly); } getArgComponent(): Type { diff --git a/src/app/components/polyalg/controls/arg-control-utils.ts b/src/app/components/polyalg/controls/arg-control-utils.ts index 0edd1605..669d6fe9 100644 --- a/src/app/components/polyalg/controls/arg-control-utils.ts +++ b/src/app/components/polyalg/controls/arg-control-utils.ts @@ -13,44 +13,45 @@ import {CorrelationControl} from './correlation-arg/correlation-arg.component'; import {CollationControl} from './collation-arg/collation-arg.component'; import {AggControl} from './agg-arg/agg-arg.component'; import {LaxAggControl} from './lax-agg/lax-agg-arg.component'; +import {DataModel} from '../../../models/ui-request.model'; export function getControl(param: Parameter, arg: PlanArgument | null, - isReadOnly: boolean, depth: number): ArgControl { + isReadOnly: boolean, depth: number, model: DataModel): ArgControl { if (arg == null) { arg = getInitialArg(param, depth); } if (arg.isEnum) { - return new EnumControl(param, arg.type, arg.value as EnumArg, isReadOnly); + return new EnumControl(param, arg.type, arg.value as EnumArg, model, isReadOnly); } switch (arg.type) { case 'ANY': break; case 'INTEGER': - return new IntControl(param, arg.value as IntArg, isReadOnly); + return new IntControl(param, arg.value as IntArg, model, isReadOnly); case 'STRING': - return new StringControl(param, arg.value as StringArg, isReadOnly); + return new StringControl(param, arg.value as StringArg, model, isReadOnly); case 'BOOLEAN': - return new BooleanControl(param, arg.value as BooleanArg, isReadOnly); + return new BooleanControl(param, arg.value as BooleanArg, model, isReadOnly); case 'REX': - return new RexControl(param, arg.value as RexArg, isReadOnly); + return new RexControl(param, arg.value as RexArg, model, isReadOnly); case 'AGGREGATE': - return new AggControl(param, arg.value as AggArg, isReadOnly); + return new AggControl(param, arg.value as AggArg, model, isReadOnly); case 'LAX_AGGREGATE': - return new LaxAggControl(param, arg.value as LaxAggArg, isReadOnly); + return new LaxAggControl(param, arg.value as LaxAggArg, model, isReadOnly); case 'ENTITY': - return new EntityControl(param, arg.value as EntityArg, isReadOnly); + return new EntityControl(param, arg.value as EntityArg, model, isReadOnly); case 'FIELD': - return new FieldControl(param, arg.value as FieldArg, isReadOnly); + return new FieldControl(param, arg.value as FieldArg, model, isReadOnly); case 'LIST': - return new ListControl(param, arg.value as ListArg, depth, isReadOnly); + return new ListControl(param, arg.value as ListArg, depth, model, isReadOnly); case 'COLLATION': - return new CollationControl(param, arg.value as CollationArg, isReadOnly); + return new CollationControl(param, arg.value as CollationArg, model, isReadOnly); case 'CORR_ID': - return new CorrelationControl(param, arg.value as CorrelationArg, isReadOnly); + return new CorrelationControl(param, arg.value as CorrelationArg, model, isReadOnly); } - return new StringControl(param, {'arg': JSON.stringify(arg)}, isReadOnly); + return new StringControl(param, {'arg': JSON.stringify(arg)}, model, isReadOnly); } diff --git a/src/app/components/polyalg/controls/arg-control.ts b/src/app/components/polyalg/controls/arg-control.ts index 9de5aff3..2535b338 100644 --- a/src/app/components/polyalg/controls/arg-control.ts +++ b/src/app/components/polyalg/controls/arg-control.ts @@ -2,13 +2,14 @@ import {ClassicPreset} from 'rete'; import {Signal, signal, Type} from '@angular/core'; import {Parameter} from '../models/polyalg-registry'; import {PlanArgument} from '../models/polyalg-plan.model'; +import {DataModel} from '../../../models/ui-request.model'; export abstract class ArgControl extends ClassicPreset.Control { readonly name: string; isTrivial: Signal = signal(false); height: Signal; - protected constructor(public readonly param: Parameter, public isReadOnly: boolean, enforceName = false) { + protected constructor(public readonly param: Parameter, public readonly model: DataModel, public isReadOnly: boolean, enforceName = false) { super(); this.name = param.multiValued > 0 && !enforceName ? null : param.name; } diff --git a/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts index 1dfae7ca..1dfd4a54 100644 --- a/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts +++ b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts @@ -2,6 +2,7 @@ import {Component, Input, signal, Type} from '@angular/core'; import {BooleanArg, PlanArgument} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; import {Parameter, ParamType} from '../../models/polyalg-registry'; +import {DataModel} from '../../../../models/ui-request.model'; @Component({ selector: 'app-boolean-arg', @@ -16,8 +17,8 @@ export class BooleanArgComponent { export class BooleanControl extends ArgControl { height = signal(50); - constructor(param: Parameter, public value: BooleanArg, isReadOnly: boolean) { - super(param, isReadOnly); + constructor(param: Parameter, public value: BooleanArg, model: DataModel, isReadOnly: boolean) { + super(param, model, isReadOnly); } getArgComponent(): Type { diff --git a/src/app/components/polyalg/controls/collation-arg/collation-arg.component.ts b/src/app/components/polyalg/controls/collation-arg/collation-arg.component.ts index 1d4ccb70..734c262a 100644 --- a/src/app/components/polyalg/controls/collation-arg/collation-arg.component.ts +++ b/src/app/components/polyalg/controls/collation-arg/collation-arg.component.ts @@ -2,6 +2,7 @@ import {Component, Input, signal, Type} from '@angular/core'; import {ArgControl} from '../arg-control'; import {Parameter, ParamType} from '../../models/polyalg-registry'; import {CollationArg, CollDirection, CollNullDirection, defaultNullDirection, PlanArgument} from '../../models/polyalg-plan.model'; +import {DataModel} from '../../../../models/ui-request.model'; @Component({ selector: 'app-collation-arg', @@ -20,8 +21,8 @@ export class CollationArgComponent { export class CollationControl extends ArgControl { height = signal(101); - constructor(param: Parameter, public value: CollationArg, isReadOnly: boolean) { - super(param, isReadOnly); + constructor(param: Parameter, public value: CollationArg, model: DataModel, isReadOnly: boolean) { + super(param, model, isReadOnly); } static collToPolyAlg(value: CollationArg): string { diff --git a/src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.ts b/src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.ts index 3876949e..beee0096 100644 --- a/src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.ts +++ b/src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.ts @@ -2,6 +2,7 @@ import {Component, Input, signal, Type} from '@angular/core'; import {ArgControl} from '../arg-control'; import {Parameter, ParamType} from '../../models/polyalg-registry'; import {CorrelationArg, PlanArgument} from '../../models/polyalg-plan.model'; +import {DataModel} from '../../../../models/ui-request.model'; @Component({ selector: 'app-correlation-arg', @@ -16,8 +17,8 @@ export class CorrelationArgComponent { export class CorrelationControl extends ArgControl { height = signal(this.name ? 55 : 31); - constructor(param: Parameter, public value: CorrelationArg, isReadOnly: boolean) { - super(param, isReadOnly); + constructor(param: Parameter, public value: CorrelationArg, model: DataModel, isReadOnly: boolean) { + super(param, model, isReadOnly); } getArgComponent(): Type { diff --git a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.html b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.html index 0aad0e3f..92613e05 100644 --- a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.html +++ b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.html @@ -1,6 +1,6 @@
        \ No newline at end of file diff --git a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts index 780ab1c4..52ba0141 100644 --- a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts +++ b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts @@ -1,22 +1,30 @@ -import {Component, Input, signal, Type} from '@angular/core'; +import {Component, Input, OnInit, signal, Type} from '@angular/core'; import {EntityArg, PlanArgument} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; import {Parameter, ParamType} from '../../models/polyalg-registry'; +import {DataModel} from '../../../../models/ui-request.model'; @Component({ selector: 'app-entity-arg', templateUrl: './entity-arg.component.html', styleUrl: './entity-arg.component.scss' }) -export class EntityArgComponent { +export class EntityArgComponent implements OnInit { @Input() data: EntityControl; + placeholder = 'entity.field'; + + ngOnInit(): void { + if (this.data.model === DataModel.GRAPH) { + this.placeholder = 'entity'; + } + } } export class EntityControl extends ArgControl { height = signal(this.name ? 55 : 31); - constructor(param: Parameter, public value: EntityArg, isReadOnly: boolean) { - super(param, isReadOnly); + constructor(param: Parameter, public value: EntityArg, model: DataModel, isReadOnly: boolean) { + super(param, model, isReadOnly); } getArgComponent(): Type { diff --git a/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts b/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts index 4ebe5bc6..65a80335 100644 --- a/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts +++ b/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts @@ -3,6 +3,7 @@ import {ArgControl} from '../arg-control'; import {EnumArg, PlanArgument} from '../../models/polyalg-plan.model'; import {PolyAlgService} from '../../polyalg.service'; import {Parameter} from '../../models/polyalg-registry'; +import {DataModel} from '../../../../models/ui-request.model'; @Component({ selector: 'app-enum-arg', @@ -28,8 +29,8 @@ export class EnumArgComponent implements OnInit { export class EnumControl extends ArgControl { height = signal(this.name ? 55 : 31); - constructor(param: Parameter, public type: string, public value: EnumArg, isReadOnly: boolean) { - super(param, isReadOnly); + constructor(param: Parameter, public type: string, public value: EnumArg, model: DataModel, isReadOnly: boolean) { + super(param, model, isReadOnly); } getArgComponent(): Type { diff --git a/src/app/components/polyalg/controls/field-arg/field-arg.component.ts b/src/app/components/polyalg/controls/field-arg/field-arg.component.ts index 16c0f934..288fe0c9 100644 --- a/src/app/components/polyalg/controls/field-arg/field-arg.component.ts +++ b/src/app/components/polyalg/controls/field-arg/field-arg.component.ts @@ -2,6 +2,7 @@ import {Component, Input, signal, Type} from '@angular/core'; import {ArgControl} from '../arg-control'; import {Parameter, ParamType} from '../../models/polyalg-registry'; import {FieldArg, PlanArgument} from '../../models/polyalg-plan.model'; +import {DataModel} from '../../../../models/ui-request.model'; @Component({ selector: 'app-field-arg', @@ -16,8 +17,8 @@ export class FieldArgComponent { export class FieldControl extends ArgControl { height = signal(this.name ? 55 : 31); - constructor(param: Parameter, public value: FieldArg, isReadOnly: boolean) { - super(param, isReadOnly); + constructor(param: Parameter, public value: FieldArg, model: DataModel, isReadOnly: boolean) { + super(param, model, isReadOnly); } getArgComponent(): Type { diff --git a/src/app/components/polyalg/controls/int-arg/int-arg.component.ts b/src/app/components/polyalg/controls/int-arg/int-arg.component.ts index 8886db86..86a7cf42 100644 --- a/src/app/components/polyalg/controls/int-arg/int-arg.component.ts +++ b/src/app/components/polyalg/controls/int-arg/int-arg.component.ts @@ -2,6 +2,7 @@ import {Component, Input, signal, Type} from '@angular/core'; import {ArgControl} from '../arg-control'; import {Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; import {IntArg, PlanArgument} from '../../models/polyalg-plan.model'; +import {DataModel} from '../../../../models/ui-request.model'; @Component({ selector: 'app-int-arg', @@ -16,8 +17,8 @@ export class IntControl extends ArgControl { valueRange: { min: number | null; max: number | null; }; height = signal(this.name ? 55 : 31); - constructor(param: Parameter, public value: IntArg, isReadOnly: boolean) { - super(param, isReadOnly); + constructor(param: Parameter, public value: IntArg, model: DataModel, isReadOnly: boolean) { + super(param, model, isReadOnly); this.valueRange = {min: null, max: null}; if (param.tags.includes(ParamTag.NON_NEGATIVE)) { diff --git a/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts b/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts index 14975da7..ccd47074 100644 --- a/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts +++ b/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts @@ -3,6 +3,7 @@ import {ArgControl} from '../arg-control'; import {Parameter, ParamType} from '../../models/polyalg-registry'; import {LaxAggArg, PlanArgument} from '../../models/polyalg-plan.model'; import {PolyAlgService} from '../../polyalg.service'; +import {DataModel} from '../../../../models/ui-request.model'; @Component({ selector: 'app-lax-agg-arg', @@ -25,8 +26,8 @@ export class LaxAggArgComponent implements OnInit { export class LaxAggControl extends ArgControl { height = signal(this.name ? 125 : 101); - constructor(param: Parameter, public value: LaxAggArg, isReadOnly: boolean) { - super(param, isReadOnly); + constructor(param: Parameter, public value: LaxAggArg, model: DataModel, isReadOnly: boolean) { + super(param, model, isReadOnly); } getArgComponent(): Type { diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts index 46c3a99f..a48d08b2 100644 --- a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts @@ -3,6 +3,7 @@ import {ListArg, PlanArgument} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; import {getControl} from '../arg-control-utils'; import {Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; +import {DataModel} from '../../../../models/ui-request.model'; @Component({ selector: 'app-list-arg', @@ -22,10 +23,10 @@ export class ListControl extends ArgControl { hideTrivial: WritableSignal; height = computed(() => this.computeHeight()); - constructor(param: Parameter, public value: ListArg, public depth: number, + constructor(param: Parameter, public value: ListArg, public depth: number, model: DataModel, isReadOnly: boolean) { - super(param, isReadOnly, depth === 0); - this.children = signal(value.args.map(arg => getControl(param, arg, isReadOnly, depth + 1))); + super(param, model, isReadOnly, depth === 0); + this.children = signal(value.args.map(arg => getControl(param, arg, isReadOnly, depth + 1, model))); if (this.children().length === 0 && value.innerType === ParamType.LIST) { value.innerType = param.type; // TODO: handle nested lists } @@ -48,7 +49,7 @@ export class ListControl extends ArgControl { addElement() { this.hideTrivial.set(false); this.children.update(values => - [...values, getControl(this.param, null, this.isReadOnly, this.depth + 1)]); + [...values, getControl(this.param, null, this.isReadOnly, this.depth + 1, this.model)]); } removeElement(child: ArgControl) { diff --git a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts index a0f6ec4b..708dd590 100644 --- a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts +++ b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts @@ -2,6 +2,7 @@ import {Component, computed, Input, signal, Type} from '@angular/core'; import {PlanArgument, RexArg} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; import {Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; +import {DataModel} from '../../../../models/ui-request.model'; @Component({ selector: 'app-rex-arg', @@ -26,8 +27,8 @@ export class RexControl extends ArgControl { return hasTrivialAlias && hasTrivialRex; }); - constructor(param: Parameter, private value: RexArg, isReadOnly: boolean) { - super(param, isReadOnly); + constructor(param: Parameter, private value: RexArg, model: DataModel, isReadOnly: boolean) { + super(param, model, isReadOnly); this.showAlias = param.tags.includes(ParamTag.ALIAS); } diff --git a/src/app/components/polyalg/controls/string-arg/string-arg.component.ts b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts index cd9b7a0a..3d870b59 100644 --- a/src/app/components/polyalg/controls/string-arg/string-arg.component.ts +++ b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts @@ -2,6 +2,7 @@ import {Component, computed, Input, signal, Type} from '@angular/core'; import {PlanArgument, StringArg} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; import {Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; +import {DataModel} from '../../../../models/ui-request.model'; @Component({ selector: 'app-string-arg', @@ -26,8 +27,8 @@ export class StringControl extends ArgControl { return hasTrivialAlias && hasTrivialArg; }); - constructor(param: Parameter, private value: StringArg, isReadOnly: boolean) { - super(param, isReadOnly); + constructor(param: Parameter, private value: StringArg, model: DataModel, isReadOnly: boolean) { + super(param, model, isReadOnly); this.showAlias = param.tags.includes(ParamTag.ALIAS); } diff --git a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts index c8f97a00..5227af01 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts @@ -1,4 +1,4 @@ -import {Injector} from '@angular/core'; +import {Injector, WritableSignal} from '@angular/core'; import {GetSchemes, NodeEditor} from 'rete'; import {AreaExtensions, AreaPlugin, BaseAreaPlugin} from 'rete-area-plugin'; import {ConnectionPlugin, Presets as ConnectionPresets} from 'rete-connection-plugin'; @@ -28,7 +28,8 @@ export type Schemes = GetSchemes>; type AreaExtra = AngularArea2D | ContextMenuExtra; export async function createEditor(container: HTMLElement, injector: Injector, registry: PolyAlgService, node: PlanNode | null, - isReadOnly: boolean) { + isReadOnly: boolean, userMode: WritableSignal) { + const readonlyPlugin = new ReadonlyPlugin(); //const socket = new ClassicPreset.Socket('socket'); @@ -202,7 +203,7 @@ export async function createEditor(container: HTMLElement, injector: Injector, r } } if (modifyingEventTypes.has(context.type)) { - if (context.type !== 'nodecreated' || editor.getNodes().length === 1) { + if (!(context.type === 'nodecreated' || context.type === 'noderemoved') || editor.getNodes().length === 1) { $modifyEvent.next(); } } @@ -219,6 +220,15 @@ export async function createEditor(container: HTMLElement, injector: Injector, r readonlyPlugin.enable(); // disable interaction with nodes (control interaction is deactivated separately) } + contextMenu.addPipe(context => { + if (context.type === 'render' && context.data.type === 'contextmenu' && userMode() === UserMode.SIMPLE) { + context.data.items.forEach(item => + item.subitems = item.subitems?.filter(sub => registry.isSimpleOperator(sub.label)) + ); + } + return context; + }); + return { layout: async () => { @@ -328,3 +338,8 @@ function updateSize(algNode: AlgNode, {x, y}: Position, area: AreaPlugin - - - + +
        + + + + + + + + + + +
        + + + +
        diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts index e1b24689..809f0f3d 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts @@ -1,5 +1,5 @@ import {AfterViewInit, Component, computed, effect, ElementRef, EventEmitter, Injector, Input, OnChanges, OnDestroy, Output, signal, SimpleChanges, untracked, ViewChild} from '@angular/core'; -import {createEditor} from './alg-editor'; +import {createEditor, UserMode} from './alg-editor'; import {PlanNode} from '../models/polyalg-plan.model'; import {PolyAlgService} from '../polyalg.service'; import {EditorComponent} from '../../editor/editor.component'; @@ -18,6 +18,36 @@ type editorState = 'SYNCHRONIZED' | 'CHANGED' | 'INVALID'; styleUrl: './alg-viewer.component.scss' }) export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { + + constructor(private injector: Injector, + private _registry: PolyAlgService, + private _toast: ToasterService, + private _validator: AlgValidatorService, + private _router: Router, + private _route: ActivatedRoute) { + + effect(() => { + const el = this.container.nativeElement; + + if (this.showNodeEditor() && this.polyAlgPlan() !== undefined && el) { + untracked(() => { + this.modifySubscription?.unsubscribe(); + createEditor(el, this.injector, _registry, this.polyAlgPlan(), this.isReadOnly, this.userMode) + .then(editor => { + this.nodeEditor = editor; + this.generateTextFromNodeEditor(); + this.modifySubscription = this.nodeEditor.onModify.pipe( + switchMap(() => { + this.nodeEditorState.set('CHANGED'); + return timer(500); + }) + ).subscribe(() => this.generateTextFromNodeEditor(true)); + }); + }); + } + }); + } + @Input() polyAlg?: string; @Input() initialPlan?: string; @Input() planType: 'LOGICAL' | 'ROUTED' | 'PHYSICAL'; @@ -42,6 +72,8 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { ); isSynchronized = computed(() => this.nodeEditorState() === 'SYNCHRONIZED' && this.textEditorState() === 'SYNCHRONIZED'); showEditButton: boolean; + //isSimpleMode = signal(false); + userMode = signal(UserMode.ADVANCED); private modifySubscription: Subscription; nodeEditor: { onModify: any; destroy: any; toPolyAlg: any; layout?: () => Promise; }; @@ -60,40 +92,12 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { tabSize: 2 }; - constructor(private injector: Injector, - private _registry: PolyAlgService, - private _toast: ToasterService, - private _validator: AlgValidatorService, - private _router: Router, - private _route: ActivatedRoute) { - - effect(() => { - const el = this.container.nativeElement; - - if (this.showNodeEditor() && this.polyAlgPlan() !== undefined && el) { - untracked(() => { - this.modifySubscription?.unsubscribe(); - createEditor(el, this.injector, _registry, this.polyAlgPlan(), this.isReadOnly) - .then(editor => { - this.nodeEditor = editor; - this.generateTextFromNodeEditor(); - this.modifySubscription = this.nodeEditor.onModify.pipe( - switchMap(() => { - this.nodeEditorState.set('CHANGED'); - return timer(500); - }) - ).subscribe(() => this.generateTextFromNodeEditor(true)); - }); - }); - } - }); - } + protected readonly UserMode = UserMode; ngAfterViewInit(): void { this.showEditButton = this.isReadOnly && !(this.planType === 'LOGICAL' && this._route.snapshot.params.route === 'polyalg'); this.textEditor.setScrollMargin(5, 5); - //this.textEditor.onSelectionChange((f) => console.log(f)); if (!this.isReadOnly) { this.textEditor.onBlur(() => this.onTextEditorBlur()); this.textEditor.onChange(() => this.onTextEditorChange()); @@ -250,4 +254,8 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { } }); } + + setUserMode(mode: UserMode) { + this.userMode.set(mode); + } } diff --git a/src/app/components/polyalg/polyalg.service.ts b/src/app/components/polyalg/polyalg.service.ts index 4bf4023f..abc5ab34 100644 --- a/src/app/components/polyalg/polyalg.service.ts +++ b/src/app/components/polyalg/polyalg.service.ts @@ -1,6 +1,6 @@ import {Injectable, signal} from '@angular/core'; import {CrudService} from '../../services/crud.service'; -import {Declaration, Parameter, ParamTag, PolyAlgRegistry} from './models/polyalg-registry'; +import {Declaration, OperatorTag, Parameter, PolyAlgRegistry} from './models/polyalg-registry'; import {DataModel} from '../../models/ui-request.model'; @Injectable({ @@ -68,8 +68,7 @@ export class PolyAlgService { return this.parameters.get(decl)?.get(pName); } - showAlias(opName: string, pName: string) { - const p = this.getParameter(opName, pName); - return p.tags.includes(ParamTag.ALIAS); + isSimpleOperator(opName: string) { + return !this.declarations.get(opName).tags.includes(OperatorTag.ADVANCED); } } diff --git a/src/scss/style.scss b/src/scss/style.scss index f02e2906..0bc48d1e 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -83,19 +83,19 @@ [rete-context-menu] { // TODO: adjust style of context menu - width: 320px !important; + width: 200px !important; border-radius: 4px; .block { - background: $dark !important; - //color: black !important; - border-bottom: 1px solid white !important; - //border-radius: 4px; - //border: none !important; + color: $body-color !important; + background: $light !important; + border-bottom: 1px solid #c8c9cb !important; + padding-top: 2px !important; + padding-bottom: 2px !important; } .block:hover { - background: $secondary !important; + background: #c8c9cb !important; } .block:last-child { From 55cfa3d9a3735463b0eda045d5fdf3e593bc74cf Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Fri, 17 May 2024 20:05:50 +0200 Subject: [PATCH 21/54] Add metadata to PolyPlans --- src/app/components/components.module.ts | 7 +- .../alg-metadata/alg-metadata.component.html | 13 ++++ .../alg-metadata/alg-metadata.component.scss | 0 .../alg-metadata/alg-metadata.component.ts | 59 +++++++++++++++ .../polyalg/algnode/alg-node.component.html | 25 +++++-- .../polyalg/algnode/alg-node.component.scss | 40 +++++++++-- .../polyalg/algnode/alg-node.component.ts | 56 ++++++++++++--- .../polyalg/models/polyalg-plan.model.ts | 9 +++ .../polyalg-viewer/alg-editor-utils.ts | 38 ++++++++++ .../polyalg/polyalg-viewer/alg-editor.ts | 71 +++++++++++++------ .../polyalg-viewer/alg-viewer.component.html | 12 +++- .../polyalg-viewer/alg-viewer.component.ts | 13 +++- 12 files changed, 293 insertions(+), 50 deletions(-) create mode 100644 src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.html create mode 100644 src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.scss create mode 100644 src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.ts diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts index d6bad922..08aa57e2 100644 --- a/src/app/components/components.module.ts +++ b/src/app/components/components.module.ts @@ -7,6 +7,7 @@ import { AccordionButtonDirective, AccordionComponent, AccordionItemComponent, + BadgeComponent, BgColorDirective, BreadcrumbComponent as BreadCrumb, ButtonCloseDirective, @@ -128,6 +129,7 @@ import {AggArgComponent} from './polyalg/controls/agg-arg/agg-arg.component'; import {LaxAggArgComponent} from './polyalg/controls/lax-agg/lax-agg-arg.component'; import {MagneticConnectionComponent} from './polyalg/polyalg-viewer/magnetic-connection/magnetic-connection.component'; import {PopoverModule} from 'ngx-bootstrap/popover'; +import {AlgMetadataComponent} from './polyalg/algnode/alg-metadata/alg-metadata.component'; //import 'hammerjs'; @@ -208,7 +210,7 @@ import {PopoverModule} from 'ngx-bootstrap/popover'; ReteModule, AutocompleteLibModule, FormCheckComponent, - FormCheckLabelDirective, AccordionComponent, AccordionItemComponent, AccordionButtonDirective, TemplateIdDirective, CollapseDirective, PopoverDirective, PopoverModule, ButtonToolbarComponent + FormCheckLabelDirective, AccordionComponent, AccordionItemComponent, AccordionButtonDirective, TemplateIdDirective, CollapseDirective, PopoverDirective, PopoverModule, ButtonToolbarComponent, BadgeComponent ], declarations: [ BreadcrumbComponent, @@ -254,7 +256,8 @@ import {PopoverModule} from 'ngx-bootstrap/popover'; CollationArgComponent, AggArgComponent, LaxAggArgComponent, - MagneticConnectionComponent + MagneticConnectionComponent, + AlgMetadataComponent ], exports: [ BreadcrumbComponent, diff --git a/src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.html b/src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.html new file mode 100644 index 00000000..5a725bd1 --- /dev/null +++ b/src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.html @@ -0,0 +1,13 @@ +
        + Most CPU + Most Rows + Most IO + + + + + + + +
        {{row.key}}{{row.value}}
        +
        diff --git a/src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.scss b/src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.ts b/src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.ts new file mode 100644 index 00000000..f0e16536 --- /dev/null +++ b/src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.ts @@ -0,0 +1,59 @@ +import {Component, Input, signal} from '@angular/core'; +import {PlanMetadata} from '../../models/polyalg-plan.model'; +import {GlobalStats} from '../../polyalg-viewer/alg-editor-utils'; + +@Component({ + selector: 'app-alg-metadata', + templateUrl: './alg-metadata.component.html', + styleUrl: './alg-metadata.component.scss' +}) +export class AlgMetadataComponent { + @Input() data: AlgMetadata; + +} + +export class AlgMetadata { + height = signal(0); + rows = new Map(); + isAuxiliary = false; + isHighestIoCost: boolean; + isHighestRowsCost: boolean; + isHighestCpuCost: boolean; + + constructor(meta: PlanMetadata, globalStats: GlobalStats) { + this.computeStats(meta, globalStats); + this.recomputeHeight(); + } + + private computeStats(meta: PlanMetadata, stats: GlobalStats) { + this.isAuxiliary = meta.isAuxiliary; + this.rows.set('Row Count', this.round(meta.rowCount, 2)); + this.rows.set('Rows Cost', this.round(meta.rowsCost, 2)); + this.rows.set('CPU Cost', this.round(meta.cpuCost, 2)); + this.rows.set('IO Cost', this.round(meta.ioCost, 2)); + + this.isHighestIoCost = stats.maxIo > 0 && stats.maxIo === meta.ioCost; + this.isHighestRowsCost = stats.maxRows > 0 && stats.maxRows === meta.rowsCost; + this.isHighestCpuCost = stats.maxCpu > 0 && stats.maxCpu === meta.cpuCost; + } + + private round(number: number, d: number) { + if (number == null) { + return null; + } + if (Number.isInteger(number)) { + return number; + } + const factor = Math.pow(10, d); + return Math.round(number * factor) / factor; + } + + private recomputeHeight() { + let height = 2 * 8; // padding + if (this.isHighestIoCost || this.isHighestRowsCost || this.isHighestCpuCost) { + height += 20; + } + height += this.rows.size * 33; + this.height.set(height); + } +} diff --git a/src/app/components/polyalg/algnode/alg-node.component.html b/src/app/components/polyalg/algnode/alg-node.component.html index f1c95f8f..a1190771 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.html +++ b/src/app/components/polyalg/algnode/alg-node.component.html @@ -9,15 +9,28 @@
    -
    -

    {{ data.label }}

    +
    +

    {{ data.label }}

    +
    + {{data.modelBadge}} +
    -
    +
    +
    +
    +
    + +
    + +
    +
    = (N['controls'] | N['inputs'] | N['outputs'])[string]; @@ -30,7 +32,7 @@ export class AlgNodeComponent implements OnChanges { constructor(private cdr: ChangeDetectorRef) { this.cdr.detach(); - effect(() => this.data.recomputeHeight()); + effect(() => this.data.recomputeSize()); } ngOnChanges(): void { @@ -45,9 +47,15 @@ export class AlgNodeComponent implements OnChanges { return ai - bi; } + + toggleCollapse() { + this.data.isMetaVisible.update(b => !b); + } } const BASE_WIDTH = 350; +const METADATA_WIDTH = 200; +const METADATA_COLLAPSE_WIDTH = 16; const BASE_HEIGHT = 110; const TAB_SIZE = 2; // the indentation width when generating PolyAlg @@ -61,17 +69,32 @@ const MULTI_SOCKET_PRESETS = { [DataModel.RELATIONAL]: new AlgNodeSocket(DataModel.RELATIONAL, true), [DataModel.GRAPH]: new AlgNodeSocket(DataModel.GRAPH, true), }; +const MODEL_COLORS = new Map([ + [DataModel.RELATIONAL, 'warning'], + [DataModel.DOCUMENT, 'warning'], + [DataModel.GRAPH, 'warning'] +]); export class AlgNode extends ClassicPreset.Node { - width = BASE_WIDTH; + width: number; height: number; + isMetaVisible = signal(false); private readonly tabIndent = ' '.repeat(TAB_SIZE); private controlHeights: Signal[] = []; readonly hasVariableInputs: boolean; + readonly modelBadge: string; + readonly modelColor: string; - constructor(public decl: Declaration, args: { [key: string]: PlanArgument } | null, private isReadOnly: boolean, - private updateArea: (a: AlgNode, delta: Position) => void) { - super(decl.name); + constructor(public decl: Declaration, args: { [key: string]: PlanArgument } | null, public readonly metadata: AlgMetadata | null, + public isReadOnly: boolean, private updateArea: (a: AlgNode, delta: Position) => void) { + super(decl.name.substring(decl.name.indexOf('_') + 1)); + this.modelBadge = getModelPrefix(decl.model); + this.modelColor = MODEL_COLORS.get(decl.model); + + if (metadata) { + this.isMetaVisible.set(true); + } + this.width = isReadOnly ? BASE_WIDTH + METADATA_WIDTH + METADATA_COLLAPSE_WIDTH : BASE_WIDTH; const output = new ClassicPreset.Output(SINGLE_SOCKET_PRESETS[decl.model]); output.multipleConnections = false; @@ -99,15 +122,26 @@ export class AlgNode extends ClassicPreset.Node { } } - recomputeHeight() { + recomputeSize() { + const oldWidth = this.width; + this.width = BASE_WIDTH; + if (this.isReadOnly) { + this.width += METADATA_COLLAPSE_WIDTH; + } + if (this.isMetaVisible()) { + this.width += METADATA_WIDTH; + } + const deltaX = this.width - oldWidth; + const oldHeight = this.height; const sum = Object.values(this.controlHeights).reduce((total, value) => total + value() + 12, 0); - this.height = BASE_HEIGHT + sum; + this.height = BASE_HEIGHT + (this.isMetaVisible() ? Math.max(sum, this.metadata.height()) : sum); + let deltaY = 0; if (oldHeight) { - let deltaY = this.height - oldHeight; + deltaY = this.height - oldHeight; deltaY += deltaY > 0 ? 1 : -1; // slight adjustment is required for smooth behavior - this.updateArea(this, {x: 0, y: -deltaY}); } + this.updateArea(this, {x: -deltaX / 2, y: -deltaY}); } data(inputs: { [key: string]: string } = {}) { @@ -150,7 +184,7 @@ export class AlgNode extends ClassicPreset.Node { for (const p of this.decl.posParams.concat(this.decl.kwParams)) { args[p.name] = (this.controls[p.name] as ArgControl).copyArg(); } - return new AlgNode(this.decl, args, this.isReadOnly, this.updateArea); + return new AlgNode(this.decl, args, null, this.isReadOnly, this.updateArea); } } diff --git a/src/app/components/polyalg/models/polyalg-plan.model.ts b/src/app/components/polyalg/models/polyalg-plan.model.ts index 9c2c583f..fe5951d6 100644 --- a/src/app/components/polyalg/models/polyalg-plan.model.ts +++ b/src/app/components/polyalg/models/polyalg-plan.model.ts @@ -5,10 +5,19 @@ export interface PlanNode { arguments: { [key: string]: PlanArgument }; + metadata: PlanMetadata; inputs: PlanNode[]; defaultValue: string; } +export interface PlanMetadata { + isAuxiliary?: boolean; + rowCount?: number; + rowsCost?: number; + cpuCost?: number; + ioCost?: number; +} + export interface PlanArgument { type: ParamType | string; // if isEnum, then type identifies the type of enum and is not a ParamType value: ArgType; diff --git a/src/app/components/polyalg/polyalg-viewer/alg-editor-utils.ts b/src/app/components/polyalg/polyalg-viewer/alg-editor-utils.ts index af5ce24a..dea29834 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-editor-utils.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-editor-utils.ts @@ -3,6 +3,8 @@ import {ClassicPreset, NodeEditor} from 'rete'; import {Schemes} from './alg-editor'; import {AlgNode} from '../algnode/alg-node.component'; import {CustomConnection} from '../custom-connection/custom-connection.component'; +import {PlanNode} from '../models/polyalg-plan.model'; +import {DataModel} from '../../../models/ui-request.model'; type Sockets = AlgNodeSocket; type Input = ClassicPreset.Input; @@ -82,6 +84,42 @@ export function findRootNodeId(nodes: AlgNode[], connections: CustomConnection stats.maxRows) { + stats.maxRows = meta.rowsCost; + } + if (meta.ioCost > stats.maxIo) { + stats.maxIo = meta.ioCost; + } + if (meta.cpuCost > stats.maxCpu) { + stats.maxCpu = meta.cpuCost; + } + + for (const child of plan.inputs) { + computeGlobalStats(child, stats); + } + return stats; +} + +export interface GlobalStats { + maxRows: number; + maxIo: number; + //maxDuration: number; + maxCpu: number; +} + function getSuccessor(nodeId: string, connections: CustomConnection[]): string | null { const outgoing = connections.filter(c => c.source === nodeId); if (outgoing.length === 0) { diff --git a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts index 5227af01..2b55e727 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts @@ -19,16 +19,18 @@ import {DataModel} from '../../../models/ui-request.model'; import {DataflowEngine} from 'rete-engine'; import {Position} from 'rete-angular-plugin/17/types'; import {Subject} from 'rxjs'; -import {canCreateConnection, findRootNodeId} from './alg-editor-utils'; +import {canCreateConnection, computeGlobalStats, findRootNodeId, getModelPrefix, GlobalStats} from './alg-editor-utils'; import {setupPanningBoundary} from './panning-boundary'; import {useMagneticConnection} from './magnetic-connection'; import {MagneticConnectionComponent} from './magnetic-connection/magnetic-connection.component'; +import {AlgMetadata} from '../algnode/alg-metadata/alg-metadata.component'; +import {Transform} from 'rete-area-plugin/_types/area'; export type Schemes = GetSchemes>; type AreaExtra = AngularArea2D | ContextMenuExtra; export async function createEditor(container: HTMLElement, injector: Injector, registry: PolyAlgService, node: PlanNode | null, - isReadOnly: boolean, userMode: WritableSignal) { + isReadOnly: boolean, userMode: WritableSignal, oldTransform: Transform | null) { const readonlyPlugin = new ReadonlyPlugin(); @@ -106,7 +108,7 @@ export async function createEditor(container: HTMLElement, injector: Injector, r }; const $modifyEvent = new Subject(); - const updateSizeFct = (a: AlgNode, delta: Position) => updateSize(a, delta, area, readonlyPlugin, + const updateSizeFct = (a: AlgNode, delta: Position) => updateSize(a, delta, area, isReadOnly ? readonlyPlugin : null, () => arrange.layout({applier: undefined, options: layoutOpts}), $modifyEvent); const contextMenu = new ContextMenuPlugin({ @@ -179,7 +181,8 @@ export async function createEditor(container: HTMLElement, injector: Injector, r panningBoundary = setupPanningBoundary({area, selector, padding: 40, intensity: 2}); } - const [nodes, connections] = addNode(registry, node, isReadOnly, updateSizeFct); + const globalStats = computeGlobalStats(node); + const [nodes, connections] = addNode(registry, node, globalStats, isReadOnly, updateSizeFct); for (const n of nodes) { await editor.addNode(n); } @@ -192,7 +195,11 @@ export async function createEditor(container: HTMLElement, injector: Injector, r applier: undefined, options: layoutOpts }); - AreaExtensions.zoomAt(area, editor.getNodes()); + if (oldTransform) { + await area.area.zoom(oldTransform.k, oldTransform.x, oldTransform.y); + } else { + AreaExtensions.zoomAt(area, editor.getNodes()); + } const modifyingEventTypes = new Set(['nodecreated', 'noderemoved', 'connectioncreated', 'connectionremoved']); editor.addPipe(context => { @@ -222,9 +229,13 @@ export async function createEditor(container: HTMLElement, injector: Injector, r contextMenu.addPipe(context => { if (context.type === 'render' && context.data.type === 'contextmenu' && userMode() === UserMode.SIMPLE) { - context.data.items.forEach(item => - item.subitems = item.subitems?.filter(sub => registry.isSimpleOperator(sub.label)) - ); + context.data.items.forEach(item => { + const model = getModelPrefix(item.label as DataModel); + item.subitems = item.subitems?.filter(sub => { + const opName = `${model}_${sub.label}`; + return registry.isSimpleOperator(opName); + }); + }); } return context; }); @@ -252,24 +263,27 @@ export async function createEditor(container: HTMLElement, injector: Injector, r } return [null, null]; }, - onModify: $modifyEvent.asObservable() + onModify: $modifyEvent.asObservable(), + showMetadata: (b: boolean) => showMetadata(editor, b), + getTransform: () => area.area.transform }; } -function addNode(registry: PolyAlgService, node: PlanNode | null, isReadOnly: boolean, updateSize: (a: AlgNode, delta: Position) => void): [AlgNode[], CustomConnection[]] { +function addNode(registry: PolyAlgService, node: PlanNode | null, globalStats: GlobalStats, isReadOnly: boolean, updateSize: (a: AlgNode, delta: Position) => void): [AlgNode[], CustomConnection[]] { const nodes = []; const connections = []; if (!node) { return [nodes, connections]; } - const algNode = new AlgNode(registry.getDeclaration(node.opName), node.arguments, isReadOnly, updateSize); + const metadata = new AlgMetadata(node.metadata, globalStats); + const algNode = new AlgNode(registry.getDeclaration(node.opName), node.arguments, metadata, isReadOnly, updateSize); if (node.opName.endsWith('#')) { // TODO: handle implicit project correctly algNode.label = 'PROJECT#'; } for (let i = 0; i < node.inputs.length; i++) { - const [childNodes, childConnections] = addNode(registry, node.inputs[i], isReadOnly, updateSize); + const [childNodes, childConnections] = addNode(registry, node.inputs[i], globalStats, isReadOnly, updateSize); const childNode = childNodes[childNodes.length - 1]; nodes.push(...childNodes); connections.push(...childConnections); @@ -287,8 +301,8 @@ function getContextMenuItems(registry: PolyAlgService, isReadOnly: boolean, upda const innerNodes = []; for (const decl of registry.getSortedDeclarations(model)) { innerNodes.push([ - decl.name, - () => new AlgNode(decl, null, isReadOnly, updateSize) + decl.name.substring(decl.name.indexOf('_') + 1), + () => new AlgNode(decl, null, null, isReadOnly, updateSize) ]); } nodes.push([model, innerNodes]); @@ -320,25 +334,42 @@ function getContextMenuItems(registry: PolyAlgService, isReadOnly: boolean, upda } function updateSize(algNode: AlgNode, {x, y}: Position, area: AreaPlugin, - readonlyPlugin: ReadonlyPlugin, + readonlyPlugin: ReadonlyPlugin | null, arrange: () => Promise, $modifyEvent: Subject) { const oldPos = area.nodeViews.get(algNode.id).position; // update location of sockets area.update('node', algNode.id).then( () => { - const isReadOnly = readonlyPlugin.enabled; + const isReadOnly = readonlyPlugin != null; if (isReadOnly) { - readonlyPlugin.disable(); - arrange().then(() => readonlyPlugin.enable()); - } else { + if (readonlyPlugin.enabled) { // disabled if another node is currently arranging + readonlyPlugin.disable(); + arrange().then(() => readonlyPlugin.enable()); + } + + } else if (x || y) { area.translate(algNode.id, {x: oldPos.x + x, y: oldPos.y + y}).then(); - $modifyEvent.next(); // size has changed, so the content has probably also changed (e.g. when list item is deleted) + if (y) { + $modifyEvent.next(); // height has changed, so the content has probably also changed (e.g. when list item is deleted) + } } } ); } +function showMetadata(editor: NodeEditor, b: boolean): boolean { + const nodes = editor.getNodes(); + if (nodes.some(n => n.isMetaVisible() !== b)) { + nodes.forEach(n => n.isMetaVisible.set(b)); + return b; + } + // if setting the metadata to b would have no effect, we toggle all by inverting b + nodes.forEach(n => n.isMetaVisible.set(!b)); + return !b; + +} + export enum UserMode { SIMPLE = 'SIMPLE', ADVANCED = 'ADVANCED' diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html index dfe61a25..1864eaac 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html @@ -1,7 +1,7 @@ - + + + - + -
    - + diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts index a655f122..0fdd5c2e 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts @@ -10,6 +10,7 @@ import {switchMap} from 'rxjs/operators'; import {ActivatedRoute, Router} from '@angular/router'; import {DataModel} from '../../../models/ui-request.model'; import {Transform} from 'rete-area-plugin/_types/area'; +import {PlanType} from '../../../models/information-page.model'; type editorState = 'SYNCHRONIZED' | 'CHANGED' | 'INVALID'; @@ -30,11 +31,11 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { effect(() => { const el = this.container.nativeElement; - if (this.showNodeEditor() && this.polyAlgPlan() !== undefined && el) { + if (this.showNodeEditor() && this.polyAlgPlan() !== undefined && this.planTypeSignal() !== undefined && el) { untracked(() => { this.modifySubscription?.unsubscribe(); const oldTransform = this.nodeEditor ? this.nodeEditor.getTransform() : null; - createEditor(el, this.injector, _registry, this.polyAlgPlan(), this.isReadOnly, this.userMode, oldTransform) + createEditor(el, this.injector, _registry, this.polyAlgPlan(), this.planTypeSignal(), this.isReadOnly, this.userMode, oldTransform) .then(editor => { this.nodeEditor = editor; this.generateTextFromNodeEditor(); @@ -52,13 +53,14 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { @Input() polyAlg?: string; @Input() initialPlan?: string; - @Input() planType: 'LOGICAL' | 'ROUTED' | 'PHYSICAL'; + @Input() planType: PlanType; @Input() isReadOnly: boolean; @Output() execute = new EventEmitter<[string, DataModel]>(); @ViewChild('rete') container!: ElementRef; @ViewChild('textEditor') textEditor: EditorComponent; private polyAlgPlan = signal(undefined); // null: empty plan + private planTypeSignal = signal(undefined); // we need an additional signal to automatically execute the effect textEditorState = signal('SYNCHRONIZED'); textEditorError = signal(''); nodeEditorState = signal('SYNCHRONIZED'); @@ -97,7 +99,7 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { protected readonly UserMode = UserMode; ngAfterViewInit(): void { - this.showEditButton = this.isReadOnly && !(this.planType === 'LOGICAL' && this._route.snapshot.params.route === 'polyalg'); + this.showEditButton = this.isReadOnly && !(this._route.snapshot.params.route === 'polyalg'); this.showMetadata = this.isReadOnly; this.textEditor.setScrollMargin(5, 5); @@ -110,10 +112,10 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { ngOnChanges(changes: SimpleChanges) { if (changes.polyAlg) { - if (!this.polyAlg) { + if (this.polyAlg == null) { return; } - this._validator.buildPlan(this.polyAlg).subscribe({ + this._validator.buildPlan(this.polyAlg, this.planType).subscribe({ next: (plan) => this.polyAlgPlan.set(plan), error: () => { this.nodeEditorState.set('INVALID'); @@ -123,6 +125,10 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { }); } + if (changes.planType) { + this.planTypeSignal.set(this.planType); + } + if (changes.initialPlan && !this.polyAlg && !this.polyAlgPlan()) { this.polyAlgPlan.set(JSON.parse(this.initialPlan)); } @@ -140,7 +146,7 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { this.initialPolyAlg = str; } if (validatePlanWithBackend && !this._validator.isConfirmedValid(str)) { - this._validator.buildPlan(str).subscribe({ + this._validator.buildPlan(str, this.planTypeSignal()).subscribe({ next: () => this.updateTextEditor(str), error: (err) => { this.nodeEditorState.set('INVALID'); @@ -167,7 +173,7 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { } generateNodesFromTextEditor(updatedPolyAlg = this.textEditor.getCode()) { - this._validator.buildPlan(updatedPolyAlg).subscribe({ + this._validator.buildPlan(updatedPolyAlg, this.planTypeSignal()).subscribe({ next: (plan) => { this.polyAlgPlan.set(plan); // this has the effect of calling generateTextFromNodeEditor() since the nodeEditor is the sync authority }, @@ -247,6 +253,7 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { return; } localStorage.setItem('polyalg.polyAlg', this.textEditor.getCode()); + localStorage.setItem('polyalg.planType', this.planTypeSignal()); this._router.navigate(['/views/querying/polyalg']).then(null); } diff --git a/src/app/models/information-page.model.ts b/src/app/models/information-page.model.ts index c1f2bf75..c183c354 100644 --- a/src/app/models/information-page.model.ts +++ b/src/app/models/information-page.model.ts @@ -43,10 +43,9 @@ export interface InformationObject extends Duration { graphType?: string; //debugger queryPlan: string; - //polyalg - polyAlg: string; + //PolyAlg plans jsonPolyAlg: string; - planType: "LOGICAL" | "ROUTED" | "PHYSICAL"; + planType: PlanType; //code code?: string; language?: string; @@ -64,6 +63,8 @@ export interface InformationObject extends Duration { text: string; } +export type PlanType = 'LOGICAL' | 'ALLOCATION' | 'PHYSICAL'; + export interface InformationResponse { errorMsg?: string; successMsg?: string; diff --git a/src/app/models/ui-request.model.ts b/src/app/models/ui-request.model.ts index d7bde375..82ba023a 100644 --- a/src/app/models/ui-request.model.ts +++ b/src/app/models/ui-request.model.ts @@ -2,6 +2,7 @@ import {SortState} from '../components/data-view/models/sort-state.model'; import {TableConstraint, UiColumnDefinition} from '../components/data-view/models/result-set.model'; import {Node} from '../views/querying/algebra/algebra.model'; import {EntityType} from './catalog.model'; +import {PlanType} from './information-page.model'; export class RequestModel { type: string; @@ -66,12 +67,14 @@ export class PolyAlgRequest extends UIRequest { type = 'PolyAlgRequest'; polyAlg: string; model: DataModel; + planType: PlanType; noLimit = false; // TODO: handle queries with large results - constructor(polyAlg: string, model: DataModel) { + constructor(polyAlg: string, model: DataModel, planType: PlanType) { super(); this.polyAlg = polyAlg; this.model = model; + this.planType = planType; } } diff --git a/src/app/services/crud.service.ts b/src/app/services/crud.service.ts index eef57515..f8f61764 100644 --- a/src/app/services/crud.service.ts +++ b/src/app/services/crud.service.ts @@ -15,6 +15,7 @@ import {Observable} from 'rxjs'; import {map} from 'rxjs/operators'; import {PolyAlgRegistry} from '../components/polyalg/models/polyalg-registry'; import {PlanNode} from '../components/polyalg/models/polyalg-plan.model'; +import {PlanType} from '../models/information-page.model'; @Injectable({ @@ -661,13 +662,13 @@ export class CrudService { return this._http.get(`${this.httpUrl}/getPolyAlgRegistry`); } - executePolyAlg(socket: WebSocket, polyAlg: string, model: DataModel) { - const request = new PolyAlgRequest(polyAlg, model); + executePolyAlg(socket: WebSocket, polyAlg: string, model: DataModel, planType: PlanType) { + const request = new PolyAlgRequest(polyAlg, model, planType); return socket.sendMessage(request); } - buildTreeFromPolyAlg(polyAlg: string) { - const request = new PolyAlgRequest(polyAlg, DataModel.RELATIONAL); // datamodel doesn't matter when building the plan + buildTreeFromPolyAlg(polyAlg: string, planType: PlanType) { + const request = new PolyAlgRequest(polyAlg, DataModel.RELATIONAL, planType); // datamodel doesn't matter when building the plan return this._http.post(`${this.httpUrl}/buildPolyPlan`, request, this.httpOptions); } diff --git a/src/app/views/querying/polyalg/polyalg.component.html b/src/app/views/querying/polyalg/polyalg.component.html index 1027bbb8..1467f031 100644 --- a/src/app/views/querying/polyalg/polyalg.component.html +++ b/src/app/views/querying/polyalg/polyalg.component.html @@ -1,7 +1,20 @@ -
    - +
    + + + +
    + +
    @@ -44,4 +57,48 @@ -
    \ No newline at end of file + + + + +
    Choose a Plan Type
    + +
    + +

    + {{planType ? 'Warning: Changing the plan type will create a completely new plan!' : 'Please select the type of plan you want to create:'}} +

    + + + +
    + + + + +
    + + + +
    Choose a Plan Type
    + +
    + + The PolyPlan Builder can be used to create or edit plans using either +
      +
    • the PolyAlgebra Editor
    • +
    • or the Node Editor.
    • +
    + + +
    + + + +
    \ No newline at end of file diff --git a/src/app/views/querying/polyalg/polyalg.component.scss b/src/app/views/querying/polyalg/polyalg.component.scss index b6c65575..e69de29b 100644 --- a/src/app/views/querying/polyalg/polyalg.component.scss +++ b/src/app/views/querying/polyalg/polyalg.component.scss @@ -1,7 +0,0 @@ -.rete { - min-height: 800px; - border: 2px solid lightblue; -} - -.rete-wrapper { -} \ No newline at end of file diff --git a/src/app/views/querying/polyalg/polyalg.component.ts b/src/app/views/querying/polyalg/polyalg.component.ts index dbf8aaed..bd3ec6e2 100644 --- a/src/app/views/querying/polyalg/polyalg.component.ts +++ b/src/app/views/querying/polyalg/polyalg.component.ts @@ -8,7 +8,7 @@ import {ToasterService} from '../../../components/toast-exposer/toaster.service' import {Subscription} from 'rxjs'; import {BreadcrumbService} from '../../../components/breadcrumb/breadcrumb.service'; import {WebuiSettingsService} from '../../../services/webui-settings.service'; -import {InformationObject, InformationPage} from '../../../models/information-page.model'; +import {InformationObject, InformationPage, PlanType} from '../../../models/information-page.model'; import {SidebarNode} from '../../../models/sidebar-node.model'; import {BreadcrumbItem} from '../../../components/breadcrumb/breadcrumb-item'; import {DataModel} from '../../../models/ui-request.model'; @@ -29,6 +29,10 @@ export class PolyalgComponent implements OnInit, OnDestroy { showingAnalysis = false; queryAnalysis: InformationPage; + planType: PlanType = 'LOGICAL'; + selectedPlanType: PlanType = 'LOGICAL'; + showPlanTypeModal = signal(false); + showHelpModal = signal(false); polyAlg = `PROJECT[employeeno, relationshipjoy AS happiness]( FILTER[<(age0, 30)]( JOIN[=(employeeno, employeeno0)]( @@ -49,6 +53,14 @@ export class PolyalgComponent implements OnInit, OnDestroy { if (polyAlgToEdit) { this.polyAlg = polyAlgToEdit; + this.planType = localStorage.getItem('polyalg.planType') as PlanType; + console.log(this.planType); + this.selectedPlanType = this.planType; + localStorage.removeItem('polyalg.planType'); + } + + if (!this.planType) { + this.showPlanTypeModal.set(true); } this.websocket = new WebSocket(); @@ -74,7 +86,7 @@ export class PolyalgComponent implements OnInit, OnDestroy { this._leftSidebar.open(); this.loading.set(true); - if (!this._crud.executePolyAlg(this.websocket, polyAlg, model)) { + if (!this._crud.executePolyAlg(this.websocket, polyAlg, model, this.planType)) { this.loading.set(false); this.result.set(new RelationalResult('Could not establish a connection with the server.')); } @@ -165,4 +177,28 @@ export class PolyalgComponent implements OnInit, OnDestroy { this.subscriptions.add(sub); } + handlePlanTypeModalChange($event: boolean) { + this.showPlanTypeModal.set($event); + } + + togglePlanTypeModal() { + this.showPlanTypeModal.update(b => !b); + } + + choosePlanType() { + const hasChanged = this.planType !== this.selectedPlanType; + this.planType = this.selectedPlanType; + this.showPlanTypeModal.set(false); + if (hasChanged) { + this.polyAlg = ''; + } + } + + handleHelpModalChange($event: boolean) { + this.showHelpModal.set($event); + } + + toggleHelpModal() { + this.showHelpModal.update(b => !b); + } } diff --git a/src/app/views/views.module.ts b/src/app/views/views.module.ts index 6b83e6b9..e0d6fc99 100644 --- a/src/app/views/views.module.ts +++ b/src/app/views/views.module.ts @@ -45,6 +45,7 @@ import { ButtonCloseDirective, ButtonDirective, ButtonGroupComponent, + ButtonToolbarComponent, CardBodyComponent, CardComponent, CardFooterComponent, @@ -89,7 +90,7 @@ import { } from '@coreui/angular'; import {EditEntityComponent} from './schema-editing/edit-entity/edit-entity.component'; import {TreeModule} from '@ali-hm/angular-tree-component'; -import {PolyalgComponent} from "./querying/polyalg/polyalg.component"; +import {PolyalgComponent} from './querying/polyalg/polyalg.component'; @NgModule({ @@ -158,7 +159,8 @@ import {PolyalgComponent} from "./querying/polyalg/polyalg.component"; PlaceholderDirective, ProgressComponent, ProgressBarComponent, - CollapseDirective + CollapseDirective, + ButtonToolbarComponent ], declarations: [ EditColumnsComponent, From e4d31c3f09cc9dbc14c48fad3991384d61cb6abb Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Wed, 29 May 2024 20:55:23 +0200 Subject: [PATCH 26/54] Various UI improvements --- .../alg-metadata/alg-metadata.component.html | 2 +- .../alg-metadata/alg-metadata.component.ts | 8 +++- .../polyalg/algnode/alg-node.component.html | 1 + .../polyalg/algnode/alg-node.component.scss | 7 ++++ .../polyalg/algnode/alg-node.component.ts | 8 ++++ .../custom-connection.component.ts | 5 ++- .../polyalg/models/polyalg-plan.model.ts | 7 ++++ .../polyalg-viewer/alg-editor-utils.ts | 11 ++++++ .../polyalg/polyalg-viewer/alg-editor.ts | 37 +++++++++++++++++-- .../polyalg-viewer/alg-viewer.component.html | 10 ++++- .../querying/polyalg/polyalg.component.html | 2 +- .../querying/polyalg/polyalg.component.ts | 15 +++++++- src/app/views/views.module.ts | 5 ++- 13 files changed, 106 insertions(+), 12 deletions(-) diff --git a/src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.html b/src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.html index b40e637b..375a17e6 100644 --- a/src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.html +++ b/src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.html @@ -1,5 +1,5 @@
    - {{badge.content}} + {{badge.content}}

    Auxiliary node (not part of original plan)

    diff --git a/src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.ts b/src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.ts index ee2a982f..6652ade3 100644 --- a/src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.ts +++ b/src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.ts @@ -1,5 +1,5 @@ import {Component, Input, signal} from '@angular/core'; -import {MetadataBadge, MetadataConnection, MetadataTableEntry, PlanMetadata} from '../../models/polyalg-plan.model'; +import {BadgeLevel, MetadataBadge, MetadataConnection, MetadataTableEntry, PlanMetadata} from '../../models/polyalg-plan.model'; @Component({ selector: 'app-alg-metadata', @@ -8,6 +8,12 @@ import {MetadataBadge, MetadataConnection, MetadataTableEntry, PlanMetadata} fro }) export class AlgMetadataComponent { @Input() data: AlgMetadata; + + BADGE_COLORS = { + [BadgeLevel.INFO]: 'info', + [BadgeLevel.WARN]: 'warning', + [BadgeLevel.DANGER]: 'danger' + }; } export class AlgMetadata { diff --git a/src/app/components/polyalg/algnode/alg-node.component.html b/src/app/components/polyalg/algnode/alg-node.component.html index b0ad5506..f5b7c8d4 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.html +++ b/src/app/components/polyalg/algnode/alg-node.component.html @@ -1,3 +1,4 @@ +{{data.multiConnIdx}}
    diff --git a/src/app/components/polyalg/algnode/alg-node.component.scss b/src/app/components/polyalg/algnode/alg-node.component.scss index cb374f20..a6ab85a3 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.scss +++ b/src/app/components/polyalg/algnode/alg-node.component.scss @@ -21,6 +21,13 @@ $socket-size: 16px; position: relative; user-select: none; + .socket-label { + position: absolute; + top: -40px; + left: $node-base-width / 2; + transform: translateX(-50%); + } + .node-body { display: flex; } diff --git a/src/app/components/polyalg/algnode/alg-node.component.ts b/src/app/components/polyalg/algnode/alg-node.component.ts index 4d9eb9a8..14208f00 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.ts +++ b/src/app/components/polyalg/algnode/alg-node.component.ts @@ -91,6 +91,8 @@ export class AlgNode extends ClassicPreset.Node { readonly isSimpleMode: WritableSignal; readonly hasSimpleParams: boolean; // true if at least one parameter has a simple variant (even if it's hidden) readonly hasVisibleControls; + multiConnIdx: number | null = null; // in the case that the output of this node is connected to a node that allows multiple connections, this indicates the order + constructor(public decl: Declaration, args: { [key: string]: PlanArgument } | null, public readonly metadata: AlgMetadata | null, isSimpleMode: boolean, public isReadOnly: boolean, private updateArea: (a: AlgNode, delta: Position) => void) { @@ -165,6 +167,11 @@ export class AlgNode extends ClassicPreset.Node { this.updateArea(this, {x: -deltaX / 2, y: -deltaY}); } + setMultiConnIdx(i: number) { + this.multiConnIdx = i; + this.updateArea(this, {x: 0, y: 0}); + } + data(inputs: { [key: string]: string } = {}) { // https://retejs.org/docs/guides/processing/dataflow // build PolyAlg representation of this node @@ -184,6 +191,7 @@ export class AlgNode extends ClassicPreset.Node { let values; if (this.hasVariableInputs) { values = inputs['0']; + console.log(values); } else { values = Object.keys(inputs) .sort((a, b) => parseInt(a, 10) - parseInt(b, 10)) // keys correspond to input socket key diff --git a/src/app/components/polyalg/custom-connection/custom-connection.component.ts b/src/app/components/polyalg/custom-connection/custom-connection.component.ts index f1f4f244..94880bb7 100644 --- a/src/app/components/polyalg/custom-connection/custom-connection.component.ts +++ b/src/app/components/polyalg/custom-connection/custom-connection.component.ts @@ -8,7 +8,7 @@ import Position = Popper.Position; selector: 'app-custom-connection', template: ` - + `, styleUrl: './custom-connection.component.scss' @@ -19,10 +19,13 @@ export class CustomConnectionComponent { @Input() end: Position; @Input() path: string; + DEFAULT_WIDTH = DEFAULT_WIDTH; + } const DEFAULT_WIDTH = 5; const MAX_WIDTH = 50; + export class CustomConnection extends ClassicPreset.Connection { isMagnetic = false; width = DEFAULT_WIDTH; diff --git a/src/app/components/polyalg/models/polyalg-plan.model.ts b/src/app/components/polyalg/models/polyalg-plan.model.ts index 5de8bdd0..7f1c1597 100644 --- a/src/app/components/polyalg/models/polyalg-plan.model.ts +++ b/src/app/components/polyalg/models/polyalg-plan.model.ts @@ -25,6 +25,13 @@ export interface MetadataConnection { export interface MetadataBadge { content: string; forKey: string; + level: BadgeLevel; +} + +export enum BadgeLevel { + INFO = 'INFO', + WARN = 'WARN', + DANGER = 'DANGER' } export interface MetadataTableEntry { diff --git a/src/app/components/polyalg/polyalg-viewer/alg-editor-utils.ts b/src/app/components/polyalg/polyalg-viewer/alg-editor-utils.ts index 5611d1cc..ffa56cbe 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-editor-utils.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-editor-utils.ts @@ -4,6 +4,7 @@ import {Schemes} from './alg-editor'; import {AlgNode} from '../algnode/alg-node.component'; import {CustomConnection} from '../custom-connection/custom-connection.component'; import {DataModel} from '../../../models/ui-request.model'; +import {SocketData} from 'rete-connection-plugin'; type Sockets = AlgNodeSocket; type Input = ClassicPreset.Input; @@ -40,6 +41,12 @@ export function canCreateConnection(editor: NodeEditor, connection: Sch return source && target && source.isCompatibleWith(target); } +export function areSocketsCompatible(editor: NodeEditor, from: SocketData, to: SocketData) { + const fromNode = editor.getNode(from.nodeId); + const toNode = editor.getNode(to.nodeId); + return fromNode.decl.model === toNode.decl.model; +} + export function findRootNodeId(nodes: AlgNode[], connections: CustomConnection[]): string | null { if (connections.length === 0) { if (nodes.length === 1) { @@ -94,6 +101,10 @@ export function getModelPrefix(model: DataModel) { } } +export function getPredecessors(nodeId: string, connections: CustomConnection[]): string[] { + return connections.filter(c => c.target === nodeId).map(c => c.source); +} + function getSuccessor(nodeId: string, connections: CustomConnection[]): string | null { const outgoing = connections.filter(c => c.source === nodeId); if (outgoing.length === 0) { diff --git a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts index 7d288d3b..96a38f77 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts @@ -19,7 +19,7 @@ import {DataModel} from '../../../models/ui-request.model'; import {DataflowEngine} from 'rete-engine'; import {Position} from 'rete-angular-plugin/17/types'; import {Subject} from 'rxjs'; -import {canCreateConnection, findRootNodeId, getModelPrefix} from './alg-editor-utils'; +import {areSocketsCompatible, canCreateConnection, findRootNodeId, getModelPrefix, getPredecessors} from './alg-editor-utils'; import {setupPanningBoundary} from './panning-boundary'; import {useMagneticConnection} from './magnetic-connection'; import {MagneticConnectionComponent} from './magnetic-connection/magnetic-connection.component'; @@ -143,7 +143,8 @@ export async function createEditor(container: HTMLElement, injector: Injector, r sourceNode, source.key as never, targetNode, - target.key as never + target.key as never, + 0 ); if (!canCreateConnection(editor, connection)) { @@ -163,7 +164,7 @@ export async function createEditor(container: HTMLElement, injector: Injector, r ); }, display(from, to) { - return from.side !== to.side; + return from.side !== to.side && areSocketsCompatible(editor, from, to); }, offset(socket, position) { @@ -210,6 +211,36 @@ export async function createEditor(container: HTMLElement, injector: Injector, r //alert('Sockets are not compatible'); return; } + const target = editor.getNode(context.data.target); + if (target.hasVariableInputs) { + const nodeIds = getPredecessors(target.id, editor.getConnections()); + if (nodeIds.length > 0) { // only show id if there are multiple predecessors + if (nodeIds.length === 1) { + editor.getNode(nodeIds[0]).setMultiConnIdx(0); + } + editor.getNode(context.data.source).setMultiConnIdx(nodeIds.length); + } + } + } else if (context.type === 'connectionremove') { + const source = editor.getNode(context.data.source); + const target = editor.getNode(context.data.target); + if (target.hasVariableInputs) { + let i = 0; + const nodeIds = getPredecessors(target.id, editor.getConnections()); + for (const nodeId of nodeIds) { + if (nodeId === source.id) { + source.setMultiConnIdx(null); + } else { + if (nodeIds.length - 1 === 1) { + // nodeId is the only node left -> do not show idx + editor.getNode(nodeId).setMultiConnIdx(null); + } else { + editor.getNode(nodeId).setMultiConnIdx(i); + i++; + } + } + } + } } if (modifyingEventTypes.has(context.type)) { if (!(context.type === 'nodecreated' || context.type === 'noderemoved') || editor.getNodes().length === 1) { diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html index 54b27284..a3120a40 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html @@ -47,10 +47,16 @@
    - + - +
    @@ -66,7 +66,7 @@
    Choose a Plan Type

    - {{planType ? 'Warning: Changing the plan type will create a completely new plan!' : 'Please select the type of plan you want to create:'}} + {{ planType ? 'Warning: Changing the plan type will create a completely new plan!' : 'Please select the type of plan you want to create:' }}

    - + - - + + - - - + +
    diff --git a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts index 0ddf4ba7..1cf5ce19 100644 --- a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts +++ b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts @@ -1,8 +1,11 @@ -import {Component, Input, OnInit, Signal, signal, Type} from '@angular/core'; +import {Component, computed, inject, Input, OnInit, Signal, signal, Type} from '@angular/core'; import {EntityArg, PlanArgument} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; import {Parameter, ParamType} from '../../models/polyalg-registry'; import {DataModel} from '../../../../models/ui-request.model'; +import {PlanType} from '../../../../models/information-page.model'; +import {AdapterModel} from '../../../../views/adapters/adapter.model'; +import {CatalogService} from '../../../../services/catalog.service'; @Component({ selector: 'app-entity-arg', @@ -12,20 +15,34 @@ import {DataModel} from '../../../../models/ui-request.model'; export class EntityArgComponent implements OnInit { @Input() data: EntityControl; placeholder = 'entity.field'; + adapters: Signal; + + private readonly _catalog = inject(CatalogService); ngOnInit(): void { if (this.data.model === DataModel.GRAPH) { this.placeholder = 'entity'; } + + if (this.data.isAllocation) { + this.adapters = computed(() => { + this._catalog.listener(); + return [...this._catalog.getStores(), ...this._catalog.getSources()]; + }); + if (!this.data.value.adapterName) { + this.data.value.adapterName = this.adapters()[0].name; + } + } } } export class EntityControl extends ArgControl { - readonly isAllocation = this.value.placementId != null; + readonly isAllocation = this.planType !== 'LOGICAL'; height = signal((this.name ? 55 : 31) + (this.isAllocation ? 2 * 31 : 0)); - constructor(param: Parameter, public value: EntityArg, model: DataModel, isSimpleMode: Signal, isReadOnly: boolean) { - super(param, model, isSimpleMode, isReadOnly); + constructor(param: Parameter, public value: EntityArg, model: DataModel, planType: PlanType, + isSimpleMode: Signal, isReadOnly: boolean) { + super(param, model, planType, isSimpleMode, isReadOnly); } getArgComponent(): Type { @@ -34,9 +51,13 @@ export class EntityControl extends ArgControl { toPolyAlg(): string { if (this.isAllocation) { - return `${this.value.placementId}.${this.value.partitionId}`; + let polyAlg = `${this.value.fullName}@${this.value.adapterName}`; + if (this.value.partitionId) { + polyAlg += '.' + this.value.partitionId; + } + return polyAlg; } - return this.value.arg; + return this.value.fullName; } copyArg(): PlanArgument { diff --git a/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts b/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts index 22c80f3f..3ff470c7 100644 --- a/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts +++ b/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts @@ -4,6 +4,7 @@ import {EnumArg, PlanArgument} from '../../models/polyalg-plan.model'; import {PolyAlgService} from '../../polyalg.service'; import {Parameter} from '../../models/polyalg-registry'; import {DataModel} from '../../../../models/ui-request.model'; +import {PlanType} from '../../../../models/information-page.model'; @Component({ selector: 'app-enum-arg', @@ -29,8 +30,9 @@ export class EnumArgComponent implements OnInit { export class EnumControl extends ArgControl { height = signal(this.name ? 55 : 31); - constructor(param: Parameter, public type: string, public value: EnumArg, model: DataModel, isSimpleMode: Signal, isReadOnly: boolean) { - super(param, model, isSimpleMode, isReadOnly); + constructor(param: Parameter, public type: string, public value: EnumArg, model: DataModel, planType: PlanType, + isSimpleMode: Signal, isReadOnly: boolean) { + super(param, model, planType, isSimpleMode, isReadOnly); } getArgComponent(): Type { diff --git a/src/app/components/polyalg/controls/field-arg/field-arg.component.ts b/src/app/components/polyalg/controls/field-arg/field-arg.component.ts index 08510cb7..95b0e79b 100644 --- a/src/app/components/polyalg/controls/field-arg/field-arg.component.ts +++ b/src/app/components/polyalg/controls/field-arg/field-arg.component.ts @@ -3,6 +3,7 @@ import {ArgControl} from '../arg-control'; import {Parameter, ParamType} from '../../models/polyalg-registry'; import {FieldArg, PlanArgument} from '../../models/polyalg-plan.model'; import {DataModel} from '../../../../models/ui-request.model'; +import {PlanType} from '../../../../models/information-page.model'; @Component({ selector: 'app-field-arg', @@ -17,8 +18,8 @@ export class FieldArgComponent { export class FieldControl extends ArgControl { height = signal(this.name ? 55 : 31); - constructor(param: Parameter, public value: FieldArg, model: DataModel, isSimpleMode: Signal, isReadOnly: boolean) { - super(param, model, isSimpleMode, isReadOnly); + constructor(param: Parameter, public value: FieldArg, model: DataModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { + super(param, model, planType, isSimpleMode, isReadOnly); } getArgComponent(): Type { diff --git a/src/app/components/polyalg/controls/int-arg/int-arg.component.ts b/src/app/components/polyalg/controls/int-arg/int-arg.component.ts index 41cfb998..86a6b08c 100644 --- a/src/app/components/polyalg/controls/int-arg/int-arg.component.ts +++ b/src/app/components/polyalg/controls/int-arg/int-arg.component.ts @@ -3,6 +3,7 @@ import {ArgControl} from '../arg-control'; import {Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; import {IntArg, PlanArgument} from '../../models/polyalg-plan.model'; import {DataModel} from '../../../../models/ui-request.model'; +import {PlanType} from '../../../../models/information-page.model'; @Component({ selector: 'app-int-arg', @@ -17,8 +18,8 @@ export class IntControl extends ArgControl { valueRange: { min: number | null; max: number | null; }; height = signal(this.name ? 55 : 31); - constructor(param: Parameter, public value: IntArg, model: DataModel, isSimpleMode: Signal, isReadOnly: boolean) { - super(param, model, isSimpleMode, isReadOnly); + constructor(param: Parameter, public value: IntArg, model: DataModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { + super(param, model, planType, isSimpleMode, isReadOnly); this.valueRange = {min: null, max: null}; if (param.tags.includes(ParamTag.NON_NEGATIVE)) { diff --git a/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts b/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts index df4785c6..61e3f4ff 100644 --- a/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts +++ b/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts @@ -4,6 +4,7 @@ import {Parameter, ParamType} from '../../models/polyalg-registry'; import {LaxAggArg, PlanArgument} from '../../models/polyalg-plan.model'; import {PolyAlgService} from '../../polyalg.service'; import {DataModel} from '../../../../models/ui-request.model'; +import {PlanType} from '../../../../models/information-page.model'; @Component({ selector: 'app-lax-agg-arg', @@ -26,8 +27,8 @@ export class LaxAggArgComponent implements OnInit { export class LaxAggControl extends ArgControl { height = signal(this.name ? 125 : 101); - constructor(param: Parameter, public value: LaxAggArg, model: DataModel, isSimpleMode: Signal, isReadOnly: boolean) { - super(param, model, isSimpleMode, isReadOnly); + constructor(param: Parameter, public value: LaxAggArg, model: DataModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { + super(param, model, planType, isSimpleMode, isReadOnly); } getArgComponent(): Type { diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts index 5ac4e0f1..1f8f0d73 100644 --- a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts @@ -4,6 +4,7 @@ import {ArgControl} from '../arg-control'; import {getControl} from '../arg-control-utils'; import {Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; import {DataModel} from '../../../../models/ui-request.model'; +import {PlanType} from '../../../../models/information-page.model'; @Component({ selector: 'app-list-arg', @@ -23,15 +24,15 @@ export class ListControl extends ArgControl { hideTrivial: WritableSignal; height = computed(() => this.computeHeight()); - constructor(param: Parameter, public value: ListArg, public depth: number, model: DataModel, + constructor(param: Parameter, public value: ListArg, public depth: number, model: DataModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { - super(param, model, isSimpleMode, isReadOnly, depth === 0); + super(param, model, planType, isSimpleMode, isReadOnly, depth === 0); if (value.args.length === 1 && value.args[0].type === ParamType.LIST && (value.args[0].value as ListArg).args.length === 0) { // remove empty inner list, as they have no effect and can be confusing if not created explicitly by the user value.args = []; } - this.children = signal(value.args.map(arg => getControl(param, arg, isReadOnly, depth + 1, model, isSimpleMode))); + this.children = signal(value.args.map(arg => getControl(param, arg, isReadOnly, depth + 1, model, planType, isSimpleMode))); if (this.children().length === 0 && value.innerType === ParamType.LIST && depth === param.multiValued - 1) { value.innerType = param.type; } @@ -54,7 +55,7 @@ export class ListControl extends ArgControl { addElement() { this.hideTrivial.set(false); this.children.update(values => - [...values, getControl(this.param, null, this.isReadOnly, this.depth + 1, this.model, this.isSimpleMode)]); + [...values, getControl(this.param, null, this.isReadOnly, this.depth + 1, this.model, this.planType, this.isSimpleMode)]); } removeElement(child: ArgControl) { diff --git a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts index b3bb90cf..d9bb0608 100644 --- a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts +++ b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts @@ -3,6 +3,7 @@ import {PlanArgument, RexArg} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; import {Parameter, ParamTag, ParamType, SimpleType} from '../../models/polyalg-registry'; import {DataModel} from '../../../../models/ui-request.model'; +import {PlanType} from '../../../../models/information-page.model'; @Component({ selector: 'app-rex-arg', @@ -38,8 +39,8 @@ export class RexControl extends ArgControl { } }; - constructor(param: Parameter, private value: RexArg, model: DataModel, isSimpleMode: Signal, isReadOnly: boolean) { - super(param, model, isSimpleMode, isReadOnly); + constructor(param: Parameter, private value: RexArg, model: DataModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { + super(param, model, planType, isSimpleMode, isReadOnly); this.showAlias = param.tags.includes(ParamTag.ALIAS); } diff --git a/src/app/components/polyalg/controls/string-arg/string-arg.component.ts b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts index 2d3918fe..ac4af97d 100644 --- a/src/app/components/polyalg/controls/string-arg/string-arg.component.ts +++ b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts @@ -3,6 +3,7 @@ import {PlanArgument, StringArg} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; import {Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; import {DataModel} from '../../../../models/ui-request.model'; +import {PlanType} from '../../../../models/information-page.model'; @Component({ selector: 'app-string-arg', @@ -27,8 +28,8 @@ export class StringControl extends ArgControl { return hasTrivialAlias && hasTrivialArg; }); - constructor(param: Parameter, private value: StringArg, model: DataModel, isSimpleMode: Signal, isReadOnly: boolean) { - super(param, model, isSimpleMode, isReadOnly); + constructor(param: Parameter, private value: StringArg, model: DataModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { + super(param, model, planType, isSimpleMode, isReadOnly); this.showAlias = param.tags.includes(ParamTag.ALIAS); } diff --git a/src/app/components/polyalg/models/polyalg-plan.model.ts b/src/app/components/polyalg/models/polyalg-plan.model.ts index 7f1c1597..514cb012 100644 --- a/src/app/components/polyalg/models/polyalg-plan.model.ts +++ b/src/app/components/polyalg/models/polyalg-plan.model.ts @@ -48,11 +48,9 @@ export interface PlanArgument { } export interface EntityArg { - arg: string; - namespaceId?: number; - entityId?: number; - partitionId?: number; - placementId?: number; + fullName: string; + adapterName?: string; + partitionId?: string; } export interface RexArg { diff --git a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts index 706f2147..c3cea4fb 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts @@ -19,14 +19,7 @@ import {DataModel} from '../../../models/ui-request.model'; import {DataflowEngine} from 'rete-engine'; import {Position} from 'rete-angular-plugin/17/types'; import {Subject} from 'rxjs'; -import { - canCreateConnection, - findRootNodeId, - getMagneticConnectionProps, - getModelPrefix, - updateMultiConnAfterCreate, - updateMultiConnAfterRemove -} from './alg-editor-utils'; +import {canCreateConnection, findRootNodeId, getMagneticConnectionProps, getModelPrefix, updateMultiConnAfterCreate, updateMultiConnAfterRemove} from './alg-editor-utils'; import {setupPanningBoundary} from './panning-boundary'; import {useMagneticConnection} from './magnetic-connection'; import {MagneticConnectionComponent} from './magnetic-connection/magnetic-connection.component'; @@ -148,7 +141,7 @@ export async function createEditor(container: HTMLElement, injector: Injector, r panningBoundary = setupPanningBoundary({area, selector, padding: 40, intensity: 2}); } - const [nodes, connections] = addNode(registry, node, isReadOnly, updateSizeFct); + const [nodes, connections] = addNode(registry, planType, node, isReadOnly, updateSizeFct); for (const n of nodes) { await editor.addNode(n); } @@ -226,21 +219,21 @@ export async function createEditor(container: HTMLElement, injector: Injector, r }; } -function addNode(registry: PolyAlgService, node: PlanNode | null, isReadOnly: boolean, updateSize: (a: AlgNode, delta: Position) => void): [AlgNode[], CustomConnection[]] { +function addNode(registry: PolyAlgService, planType: PlanType, node: PlanNode | null, isReadOnly: boolean, updateSize: (a: AlgNode, delta: Position) => void): [AlgNode[], CustomConnection[]] { const nodes = []; const connections = []; if (!node) { return [nodes, connections]; } const metadata = node.metadata ? new AlgMetadata(node.metadata) : null; - const algNode = new AlgNode(registry.getDeclaration(node.opName), node.arguments, metadata, false, isReadOnly, updateSize); + const algNode = new AlgNode(registry.getDeclaration(node.opName), planType, node.arguments, metadata, false, isReadOnly, updateSize); if (node.opName.endsWith('#')) { // TODO: handle implicit project correctly algNode.label = 'PROJECT#'; } for (let i = 0; i < node.inputs.length; i++) { - const [childNodes, childConnections] = addNode(registry, node.inputs[i], isReadOnly, updateSize); + const [childNodes, childConnections] = addNode(registry, planType, node.inputs[i], isReadOnly, updateSize); const childNode = childNodes[childNodes.length - 1]; nodes.push(...childNodes); connections.push(...childConnections); @@ -261,7 +254,7 @@ function getContextMenuItems(registry: PolyAlgService, userMode: WritableSignal< if (decl.tags.includes(OperatorTag[planType])) { innerNodes.push([ decl.name.substring(decl.name.indexOf('_') + 1), - () => new AlgNode(decl, null, null, userMode() === UserMode.SIMPLE, isReadOnly, updateSize) + () => new AlgNode(decl, planType, null, null, userMode() === UserMode.SIMPLE, isReadOnly, updateSize) ]); } } diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html index a3120a40..7d251587 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html @@ -1,16 +1,18 @@ - -
    diff --git a/src/app/components/data-view/data-view.component.ts b/src/app/components/data-view/data-view.component.ts index 0ab8c67e..00fdbc20 100644 --- a/src/app/components/data-view/data-view.component.ts +++ b/src/app/components/data-view/data-view.component.ts @@ -97,7 +97,11 @@ export class DataViewComponent implements OnDestroy { this.$presentationType.set(DataPresentationType.TABLE); break; case DataModel.GRAPH: - this.$presentationType.set(DataPresentationType.GRAPH); + if (!this.containsNode()) { + this.$presentationType.set(DataPresentationType.TABLE); + } else { + this.$presentationType.set(DataPresentationType.GRAPH); + } break; default: this.$presentationType.set(DataPresentationType.TABLE); @@ -107,6 +111,10 @@ export class DataViewComponent implements OnDestroy { }); } + public containsNode(): boolean { + return this.$result().header.some(h => h.dataType.toLowerCase().includes("node")); + } + @ViewChild(ViewComponent, {static: false}) public readonly view: ViewComponent; public readonly $result: WritableSignal = signal(null); @@ -116,7 +124,8 @@ export class DataViewComponent implements OnDestroy { if (!result) { return; } - this.$result.set(CombinedResult.from(result)); + const res = CombinedResult.from(result) + this.$result.set(res); } From f1d10a54bdaee14c1a88cfbc03ce361441b1f33b Mon Sep 17 00:00:00 2001 From: datomo Date: Sat, 18 May 2024 22:24:44 +0200 Subject: [PATCH 30/54] fix for graph properties, complex normal graphs and cross-model graphs --- .../data-graph/data-graph.component.html | 4 ++-- .../data-graph/data-graph.component.ts | 19 ++++++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/app/components/data-view/data-graph/data-graph.component.html b/src/app/components/data-view/data-graph/data-graph.component.html index c7fdc3d0..527fe24c 100644 --- a/src/app/components/data-view/data-graph/data-graph.component.html +++ b/src/app/components/data-view/data-graph/data-graph.component.html @@ -5,7 +5,7 @@
    -
    +
    {{ detail.id }} @@ -19,7 +19,7 @@
    - diff --git a/src/app/components/data-view/data-graph/data-graph.component.ts b/src/app/components/data-view/data-graph/data-graph.component.ts index cf205d63..afb7a26d 100644 --- a/src/app/components/data-view/data-graph/data-graph.component.ts +++ b/src/app/components/data-view/data-graph/data-graph.component.ts @@ -1,4 +1,4 @@ -import {Component, effect} from '@angular/core'; +import {Component, effect, signal} from '@angular/core'; import {GraphResult} from '../models/result-set.model'; import {DataModel, GraphRequest} from '../../../models/ui-request.model'; import {DataTemplateComponent} from '../data-template/data-template.component'; @@ -38,7 +38,7 @@ export class DataGraphComponent extends DataTemplateComponent { jsonValid = false; public graphLoading = false; - showProperties = false; + showProperties = signal(false); detail: Detail; private zoom: any; @@ -53,9 +53,9 @@ export class DataGraphComponent extends DataTemplateComponent { protected readonly NamespaceType = DataModel; - private static filterEdges(hidden: any[], d: Edge, p: any) { - const source = !p.afterInit ? d.source : d.source['id']; - const target = !p.afterInit ? d.target : d.target['id']; + private static filterEdges(hidden: any[], d: Edge | any, p: any) { + const source = d.source instanceof String ? d.source : d.source['id']; + const target = d.target instanceof String ? d.target : d.target['id']; if (source === target) { return true; @@ -75,7 +75,9 @@ export class DataGraphComponent extends DataTemplateComponent { } private renderGraph(graph: Graph) { - graph.nodes = Array.from(this.initialNodes); // normally this does nothing, but for cross model queries this ensures that the initial nodes are present (different ids) + if (!Array.from(this.initialNodeIds).some(id => graph.nodes.filter(n => n.id === id).length > 0)) { + graph.nodes = Array.from(this.initialNodes); // (different ids) due to cross model query, initial ids are not in graph + } if (!this.initialNodeIds) { this.initialNodeIds = new Set(graph.nodes.map(n => n.id)); @@ -142,8 +144,11 @@ export class DataGraphComponent extends DataTemplateComponent { setInterval(() => simulation.force('charge', null), 1500); const action = (d) => { + if (!(d.hasOwnProperty("properties") || d.hasOwnProperty("id"))) { + return + } this.detail = new Detail(d); - this.showProperties = true; + this.showProperties.set(true); }; // Change the value of alpha, so things move around when we drag a node From 1c47fa9f78b43ca044fa1dbbd1c38da7868e46c8 Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Fri, 31 May 2024 17:16:11 +0200 Subject: [PATCH 31/54] Show modal when trying to replace existing PolyPlan --- .../polyalg-viewer/alg-viewer.component.html | 23 ++++++++++- .../polyalg-viewer/alg-viewer.component.scss | 5 +++ .../polyalg-viewer/alg-viewer.component.ts | 38 +++++++++++++++++-- .../querying/polyalg/polyalg.component.scss | 5 +++ 4 files changed, 67 insertions(+), 4 deletions(-) diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html index 7d251587..9e86e5b0 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html @@ -97,4 +97,25 @@ - \ No newline at end of file + + + + + +
    Open Plan in Editor
    + +
    + + Are you sure you want to edit this plan? This will replace the plan currently being built. + Alternatively, the plan can be opened it in a new tab. + + + +
    + + +
    +
    +
    \ No newline at end of file diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.scss b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.scss index 3e235e8c..c8c52d39 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.scss +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.scss @@ -48,4 +48,9 @@ button.accordion-button { button.accordion-button::after { background-image: var(--cui-accordion-btn-icon) !important; +} + +.modal-footer { + display: flex; + justify-content: space-between; } \ No newline at end of file diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts index bc04b744..19388511 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts @@ -78,6 +78,7 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { ); isSynchronized = computed(() => this.nodeEditorState() === 'SYNCHRONIZED' && this.textEditorState() === 'SYNCHRONIZED'); showEditButton: boolean; + showEditModal = signal(false); private modifySubscription: Subscription; nodeEditor: { onModify: any; destroy: any; toPolyAlg: any; layout?: () => Promise; showMetadata: (b: boolean) => boolean; getTransform: () => Transform }; @@ -100,7 +101,7 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { protected readonly UserMode = UserMode; ngAfterViewInit(): void { - this.showEditButton = this.isReadOnly && !(this._route.snapshot.params.route === 'polyalg'); + this.showEditButton = this.isReadOnly; this.showMetadata = this.isReadOnly; this.textEditor.setScrollMargin(5, 5); @@ -249,14 +250,45 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { } } - openInPlanEditor() { + openInPlanEditor(forced = false, newTab = false) { if (!this.isReadOnly) { return; } + const isSameRoute = this._route.snapshot.params.route === 'polyalg'; + if (isSameRoute && !forced) { + this.showEditModal.set(true); + return; + } + localStorage.setItem('polyalg.polyAlg', this.textEditor.getCode()); localStorage.setItem('polyalg.planType', this.planTypeSignal()); - this._router.navigate(['/views/querying/polyalg']).then(null); + if (isSameRoute) { + if (newTab) { + const newRelativeUrl = this._router.createUrlTree(['/views/querying/polyalg']); + const baseUrl = window.location.href.replace(this._router.url, ''); + + window.open(baseUrl + newRelativeUrl, '_blank'); + //const url = this._router.serializeUrl(this._router.createUrlTree(['/#/views/querying/polyalg'])); + //window.open(url, '_blank'); + } else { + // https://stackoverflow.com/questions/47813927/how-to-refresh-a-component-in-angular + this._router.navigateByUrl('/', {skipLocationChange: true}).then(() => { + this._router.navigate(['/views/querying/polyalg']).then(null); + }); + } + } else { + this._router.navigate(['/views/querying/polyalg']).then(null); + } + this.showEditModal.set(false); + + } + + toggleEditModal() { + this.showEditModal.update(b => !b); + } + handleEditModalChange($event: boolean) { + this.showEditModal.set($event); } executePlan() { diff --git a/src/app/views/querying/polyalg/polyalg.component.scss b/src/app/views/querying/polyalg/polyalg.component.scss index e69de29b..1c3b0551 100644 --- a/src/app/views/querying/polyalg/polyalg.component.scss +++ b/src/app/views/querying/polyalg/polyalg.component.scss @@ -0,0 +1,5 @@ + +.modal-footer { + display: flex; + justify-content: space-between; +} \ No newline at end of file From 2911712051a358ada2f4d20f6e6bbc0b02354d8c Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Mon, 3 Jun 2024 18:17:52 +0200 Subject: [PATCH 32/54] Add support for common Operators --- .../polyalg/algnode/alg-node.component.html | 2 +- .../polyalg/algnode/alg-node.component.ts | 24 +++++----- .../controls/agg-arg/agg-arg.component.ts | 5 +- .../polyalg/controls/arg-control-utils.ts | 5 +- .../polyalg/controls/arg-control.ts | 5 +- .../boolean-arg/boolean-arg.component.ts | 5 +- .../collation-arg/collation-arg.component.ts | 5 +- .../correlation-arg.component.ts | 5 +- .../entity-arg/entity-arg.component.ts | 7 ++- .../controls/enum-arg/enum-arg.component.ts | 5 +- .../controls/field-arg/field-arg.component.ts | 5 +- .../controls/int-arg/int-arg.component.ts | 5 +- .../controls/lax-agg/lax-agg-arg.component.ts | 5 +- .../controls/list-arg/list-arg.component.ts | 5 +- .../controls/rex-arg/rex-arg.component.ts | 5 +- .../string-arg/string-arg.component.ts | 5 +- .../custom-socket/custom-socket.component.ts | 10 ++-- .../polyalg/models/polyalg-registry.ts | 13 ++++- .../polyalg-viewer/alg-editor-utils.ts | 48 +++++++++++++++---- .../polyalg/polyalg-viewer/alg-editor.ts | 35 +++----------- .../polyalg-viewer/alg-viewer.component.html | 4 +- .../polyalg-viewer/alg-viewer.component.ts | 6 +-- src/app/components/polyalg/polyalg.service.ts | 5 +- .../querying/polyalg/polyalg.component.ts | 8 +++- 24 files changed, 119 insertions(+), 108 deletions(-) diff --git a/src/app/components/polyalg/algnode/alg-node.component.html b/src/app/components/polyalg/algnode/alg-node.component.html index f5b7c8d4..21f5064b 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.html +++ b/src/app/components/polyalg/algnode/alg-node.component.html @@ -16,7 +16,7 @@

    {{ data.label }}

    Simple - {{data.modelBadge}} + {{data.modelBadge}}
    diff --git a/src/app/components/polyalg/algnode/alg-node.component.ts b/src/app/components/polyalg/algnode/alg-node.component.ts index 056d1288..53e9bdb6 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.ts +++ b/src/app/components/polyalg/algnode/alg-node.component.ts @@ -1,12 +1,11 @@ import {ChangeDetectorRef, Component, computed, effect, HostBinding, Input, OnChanges, signal, Signal, WritableSignal} from '@angular/core'; import {ClassicPreset} from 'rete'; import {KeyValue} from '@angular/common'; -import {Declaration, SimpleType} from '../models/polyalg-registry'; +import {Declaration, OperatorModel, SimpleType} from '../models/polyalg-registry'; import {PlanArgument} from '../models/polyalg-plan.model'; import {getControl} from '../controls/arg-control-utils'; import {ArgControl} from '../controls/arg-control'; import {Position} from 'rete-angular-plugin/17/types'; -import {DataModel} from '../../../models/ui-request.model'; import {AlgNodeSocket} from '../custom-socket/custom-socket.component'; import {AlgMetadata} from './alg-metadata/alg-metadata.component'; import {getModelPrefix} from '../polyalg-viewer/alg-editor-utils'; @@ -65,19 +64,22 @@ const BASE_HEIGHT = 110; const TAB_SIZE = 2; // the indentation width when generating PolyAlg const SINGLE_SOCKET_PRESETS = { - [DataModel.DOCUMENT]: new AlgNodeSocket(DataModel.DOCUMENT), - [DataModel.RELATIONAL]: new AlgNodeSocket(DataModel.RELATIONAL), - [DataModel.GRAPH]: new AlgNodeSocket(DataModel.GRAPH), + [OperatorModel.DOCUMENT]: new AlgNodeSocket(OperatorModel.DOCUMENT), + [OperatorModel.RELATIONAL]: new AlgNodeSocket(OperatorModel.RELATIONAL), + [OperatorModel.GRAPH]: new AlgNodeSocket(OperatorModel.GRAPH), + [OperatorModel.COMMON]: new AlgNodeSocket(OperatorModel.COMMON), }; const MULTI_SOCKET_PRESETS = { - [DataModel.DOCUMENT]: new AlgNodeSocket(DataModel.DOCUMENT, true), - [DataModel.RELATIONAL]: new AlgNodeSocket(DataModel.RELATIONAL, true), - [DataModel.GRAPH]: new AlgNodeSocket(DataModel.GRAPH, true), + [OperatorModel.DOCUMENT]: new AlgNodeSocket(OperatorModel.DOCUMENT, true), + [OperatorModel.RELATIONAL]: new AlgNodeSocket(OperatorModel.RELATIONAL, true), + [OperatorModel.GRAPH]: new AlgNodeSocket(OperatorModel.GRAPH, true), + [OperatorModel.COMMON]: new AlgNodeSocket(OperatorModel.COMMON, true), }; const MODEL_COLORS = new Map([ - [DataModel.RELATIONAL, 'warning'], - [DataModel.DOCUMENT, 'warning'], - [DataModel.GRAPH, 'warning'] + [OperatorModel.RELATIONAL, 'warning'], + [OperatorModel.DOCUMENT, 'warning'], + [OperatorModel.GRAPH, 'warning'], + [OperatorModel.COMMON, 'warning'] ]); export class AlgNode extends ClassicPreset.Node { diff --git a/src/app/components/polyalg/controls/agg-arg/agg-arg.component.ts b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.ts index d80b4beb..1d3b77ed 100644 --- a/src/app/components/polyalg/controls/agg-arg/agg-arg.component.ts +++ b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.ts @@ -1,10 +1,9 @@ import {Component, computed, Input, OnInit, Signal, Type} from '@angular/core'; import {ArgControl} from '../arg-control'; -import {Parameter, ParamType, SimpleType} from '../../models/polyalg-registry'; +import {OperatorModel, Parameter, ParamType, SimpleType} from '../../models/polyalg-registry'; import {AggArg, PlanArgument} from '../../models/polyalg-plan.model'; import {CollationControl} from '../collation-arg/collation-arg.component'; import {PolyAlgService} from '../../polyalg.service'; -import {DataModel} from '../../../../models/ui-request.model'; import {PlanType} from '../../../../models/information-page.model'; @Component({ @@ -32,7 +31,7 @@ export class AggControl extends ArgControl { height = computed(() => this.isSimpleMode() ? 101 : 188); argsStr = this.value.argList.join(', '); - constructor(param: Parameter, public value: AggArg, model: DataModel, planType: PlanType, + constructor(param: Parameter, public value: AggArg, model: OperatorModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { super(param, model, planType, isSimpleMode, isReadOnly); } diff --git a/src/app/components/polyalg/controls/arg-control-utils.ts b/src/app/components/polyalg/controls/arg-control-utils.ts index 2bf1819e..71dce810 100644 --- a/src/app/components/polyalg/controls/arg-control-utils.ts +++ b/src/app/components/polyalg/controls/arg-control-utils.ts @@ -6,19 +6,18 @@ import {EntityControl} from './entity-arg/entity-arg.component'; import {ListControl} from './list-arg/list-arg.component'; import {AggArg, BooleanArg, CollationArg, CollDirection, CorrelationArg, defaultNullDirection, EntityArg, EnumArg, FieldArg, IntArg, LaxAggArg, ListArg, PlanArgument, RexArg, StringArg} from '../models/polyalg-plan.model'; import {EnumControl} from './enum-arg/enum-arg.component'; -import {Parameter, ParamType} from '../models/polyalg-registry'; +import {OperatorModel, Parameter, ParamType} from '../models/polyalg-registry'; import {IntControl} from './int-arg/int-arg.component'; import {FieldControl} from './field-arg/field-arg.component'; import {CorrelationControl} from './correlation-arg/correlation-arg.component'; import {CollationControl} from './collation-arg/collation-arg.component'; import {AggControl} from './agg-arg/agg-arg.component'; import {LaxAggControl} from './lax-agg/lax-agg-arg.component'; -import {DataModel} from '../../../models/ui-request.model'; import {Signal} from '@angular/core'; import {PlanType} from '../../../models/information-page.model'; export function getControl(param: Parameter, arg: PlanArgument | null, isReadOnly: boolean, depth: number, - model: DataModel, planType: PlanType, isSimpleMode: Signal): ArgControl { + model: OperatorModel, planType: PlanType, isSimpleMode: Signal): ArgControl { if (arg == null) { arg = getInitialArg(param, depth); } diff --git a/src/app/components/polyalg/controls/arg-control.ts b/src/app/components/polyalg/controls/arg-control.ts index 08370c27..fce03583 100644 --- a/src/app/components/polyalg/controls/arg-control.ts +++ b/src/app/components/polyalg/controls/arg-control.ts @@ -1,8 +1,7 @@ import {ClassicPreset} from 'rete'; import {computed, Signal, signal, Type} from '@angular/core'; -import {Parameter, SimpleType} from '../models/polyalg-registry'; +import {OperatorModel, Parameter, SimpleType} from '../models/polyalg-registry'; import {PlanArgument} from '../models/polyalg-plan.model'; -import {DataModel} from '../../../models/ui-request.model'; import {PlanType} from '../../../models/information-page.model'; export abstract class ArgControl extends ClassicPreset.Control { @@ -13,7 +12,7 @@ export abstract class ArgControl extends ClassicPreset.Control { readonly isHidden: Signal; readonly simpleType: Signal; // if node is not in simple mode, this is always null - protected constructor(public readonly param: Parameter, public readonly model: DataModel, public readonly planType: PlanType, + protected constructor(public readonly param: Parameter, public readonly model: OperatorModel, public readonly planType: PlanType, public readonly isSimpleMode: Signal, public isReadOnly: boolean, enforceName = false) { super(); this.name = param.multiValued > 0 && !enforceName ? null : param.name; diff --git a/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts index c77ff1d4..62999594 100644 --- a/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts +++ b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts @@ -1,8 +1,7 @@ import {Component, Input, Signal, signal, Type} from '@angular/core'; import {BooleanArg, PlanArgument} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; -import {Parameter, ParamType} from '../../models/polyalg-registry'; -import {DataModel} from '../../../../models/ui-request.model'; +import {OperatorModel, Parameter, ParamType} from '../../models/polyalg-registry'; import {PlanType} from '../../../../models/information-page.model'; @Component({ @@ -18,7 +17,7 @@ export class BooleanArgComponent { export class BooleanControl extends ArgControl { height = signal(50); - constructor(param: Parameter, public value: BooleanArg, model: DataModel, planType: PlanType, + constructor(param: Parameter, public value: BooleanArg, model: OperatorModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { super(param, model, planType, isSimpleMode, isReadOnly); } diff --git a/src/app/components/polyalg/controls/collation-arg/collation-arg.component.ts b/src/app/components/polyalg/controls/collation-arg/collation-arg.component.ts index 24f59200..320b763d 100644 --- a/src/app/components/polyalg/controls/collation-arg/collation-arg.component.ts +++ b/src/app/components/polyalg/controls/collation-arg/collation-arg.component.ts @@ -1,8 +1,7 @@ import {Component, computed, Input, Signal, Type} from '@angular/core'; import {ArgControl} from '../arg-control'; -import {Parameter, ParamType, SimpleType} from '../../models/polyalg-registry'; +import {OperatorModel, Parameter, ParamType, SimpleType} from '../../models/polyalg-registry'; import {CollationArg, CollDirection, CollNullDirection, defaultNullDirection, PlanArgument} from '../../models/polyalg-plan.model'; -import {DataModel} from '../../../../models/ui-request.model'; import {PlanType} from '../../../../models/information-page.model'; @Component({ @@ -24,7 +23,7 @@ export class CollationArgComponent { export class CollationControl extends ArgControl { height = computed(() => this.isSimpleMode() ? 66 : 101); - constructor(param: Parameter, public value: CollationArg, model: DataModel, planType: PlanType, + constructor(param: Parameter, public value: CollationArg, model: OperatorModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { super(param, model, planType, isSimpleMode, isReadOnly); } diff --git a/src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.ts b/src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.ts index bca8a2ea..f792f0d7 100644 --- a/src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.ts +++ b/src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.ts @@ -1,8 +1,7 @@ import {Component, Input, Signal, signal, Type} from '@angular/core'; import {ArgControl} from '../arg-control'; -import {Parameter, ParamType} from '../../models/polyalg-registry'; +import {OperatorModel, Parameter, ParamType} from '../../models/polyalg-registry'; import {CorrelationArg, PlanArgument} from '../../models/polyalg-plan.model'; -import {DataModel} from '../../../../models/ui-request.model'; import {PlanType} from '../../../../models/information-page.model'; @Component({ @@ -18,7 +17,7 @@ export class CorrelationArgComponent { export class CorrelationControl extends ArgControl { height = signal(this.name ? 55 : 31); - constructor(param: Parameter, public value: CorrelationArg, model: DataModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { + constructor(param: Parameter, public value: CorrelationArg, model: OperatorModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { super(param, model, planType, isSimpleMode, isReadOnly); } diff --git a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts index 1cf5ce19..fe3a657d 100644 --- a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts +++ b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts @@ -1,8 +1,7 @@ import {Component, computed, inject, Input, OnInit, Signal, signal, Type} from '@angular/core'; import {EntityArg, PlanArgument} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; -import {Parameter, ParamType} from '../../models/polyalg-registry'; -import {DataModel} from '../../../../models/ui-request.model'; +import {OperatorModel, Parameter, ParamType} from '../../models/polyalg-registry'; import {PlanType} from '../../../../models/information-page.model'; import {AdapterModel} from '../../../../views/adapters/adapter.model'; import {CatalogService} from '../../../../services/catalog.service'; @@ -20,7 +19,7 @@ export class EntityArgComponent implements OnInit { private readonly _catalog = inject(CatalogService); ngOnInit(): void { - if (this.data.model === DataModel.GRAPH) { + if (this.data.model === OperatorModel.GRAPH) { this.placeholder = 'entity'; } @@ -40,7 +39,7 @@ export class EntityControl extends ArgControl { readonly isAllocation = this.planType !== 'LOGICAL'; height = signal((this.name ? 55 : 31) + (this.isAllocation ? 2 * 31 : 0)); - constructor(param: Parameter, public value: EntityArg, model: DataModel, planType: PlanType, + constructor(param: Parameter, public value: EntityArg, model: OperatorModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { super(param, model, planType, isSimpleMode, isReadOnly); } diff --git a/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts b/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts index 3ff470c7..5a2533aa 100644 --- a/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts +++ b/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts @@ -2,8 +2,7 @@ import {Component, Input, OnInit, Signal, signal, Type} from '@angular/core'; import {ArgControl} from '../arg-control'; import {EnumArg, PlanArgument} from '../../models/polyalg-plan.model'; import {PolyAlgService} from '../../polyalg.service'; -import {Parameter} from '../../models/polyalg-registry'; -import {DataModel} from '../../../../models/ui-request.model'; +import {OperatorModel, Parameter} from '../../models/polyalg-registry'; import {PlanType} from '../../../../models/information-page.model'; @Component({ @@ -30,7 +29,7 @@ export class EnumArgComponent implements OnInit { export class EnumControl extends ArgControl { height = signal(this.name ? 55 : 31); - constructor(param: Parameter, public type: string, public value: EnumArg, model: DataModel, planType: PlanType, + constructor(param: Parameter, public type: string, public value: EnumArg, model: OperatorModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { super(param, model, planType, isSimpleMode, isReadOnly); } diff --git a/src/app/components/polyalg/controls/field-arg/field-arg.component.ts b/src/app/components/polyalg/controls/field-arg/field-arg.component.ts index 95b0e79b..76c4d260 100644 --- a/src/app/components/polyalg/controls/field-arg/field-arg.component.ts +++ b/src/app/components/polyalg/controls/field-arg/field-arg.component.ts @@ -1,8 +1,7 @@ import {Component, Input, Signal, signal, Type} from '@angular/core'; import {ArgControl} from '../arg-control'; -import {Parameter, ParamType} from '../../models/polyalg-registry'; +import {OperatorModel, Parameter, ParamType} from '../../models/polyalg-registry'; import {FieldArg, PlanArgument} from '../../models/polyalg-plan.model'; -import {DataModel} from '../../../../models/ui-request.model'; import {PlanType} from '../../../../models/information-page.model'; @Component({ @@ -18,7 +17,7 @@ export class FieldArgComponent { export class FieldControl extends ArgControl { height = signal(this.name ? 55 : 31); - constructor(param: Parameter, public value: FieldArg, model: DataModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { + constructor(param: Parameter, public value: FieldArg, model: OperatorModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { super(param, model, planType, isSimpleMode, isReadOnly); } diff --git a/src/app/components/polyalg/controls/int-arg/int-arg.component.ts b/src/app/components/polyalg/controls/int-arg/int-arg.component.ts index 86a6b08c..33e2f9d6 100644 --- a/src/app/components/polyalg/controls/int-arg/int-arg.component.ts +++ b/src/app/components/polyalg/controls/int-arg/int-arg.component.ts @@ -1,8 +1,7 @@ import {Component, Input, Signal, signal, Type} from '@angular/core'; import {ArgControl} from '../arg-control'; -import {Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; +import {OperatorModel, Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; import {IntArg, PlanArgument} from '../../models/polyalg-plan.model'; -import {DataModel} from '../../../../models/ui-request.model'; import {PlanType} from '../../../../models/information-page.model'; @Component({ @@ -18,7 +17,7 @@ export class IntControl extends ArgControl { valueRange: { min: number | null; max: number | null; }; height = signal(this.name ? 55 : 31); - constructor(param: Parameter, public value: IntArg, model: DataModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { + constructor(param: Parameter, public value: IntArg, model: OperatorModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { super(param, model, planType, isSimpleMode, isReadOnly); this.valueRange = {min: null, max: null}; diff --git a/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts b/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts index 61e3f4ff..a046a95f 100644 --- a/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts +++ b/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts @@ -1,9 +1,8 @@ import {Component, Input, OnInit, Signal, signal, Type} from '@angular/core'; import {ArgControl} from '../arg-control'; -import {Parameter, ParamType} from '../../models/polyalg-registry'; +import {OperatorModel, Parameter, ParamType} from '../../models/polyalg-registry'; import {LaxAggArg, PlanArgument} from '../../models/polyalg-plan.model'; import {PolyAlgService} from '../../polyalg.service'; -import {DataModel} from '../../../../models/ui-request.model'; import {PlanType} from '../../../../models/information-page.model'; @Component({ @@ -27,7 +26,7 @@ export class LaxAggArgComponent implements OnInit { export class LaxAggControl extends ArgControl { height = signal(this.name ? 125 : 101); - constructor(param: Parameter, public value: LaxAggArg, model: DataModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { + constructor(param: Parameter, public value: LaxAggArg, model: OperatorModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { super(param, model, planType, isSimpleMode, isReadOnly); } diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts index 1f8f0d73..3c6eccfe 100644 --- a/src/app/components/polyalg/controls/list-arg/list-arg.component.ts +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts @@ -2,8 +2,7 @@ import {Component, computed, Input, Signal, signal, Type, WritableSignal} from ' import {ListArg, PlanArgument} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; import {getControl} from '../arg-control-utils'; -import {Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; -import {DataModel} from '../../../../models/ui-request.model'; +import {OperatorModel, Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; import {PlanType} from '../../../../models/information-page.model'; @Component({ @@ -24,7 +23,7 @@ export class ListControl extends ArgControl { hideTrivial: WritableSignal; height = computed(() => this.computeHeight()); - constructor(param: Parameter, public value: ListArg, public depth: number, model: DataModel, planType: PlanType, + constructor(param: Parameter, public value: ListArg, public depth: number, model: OperatorModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { super(param, model, planType, isSimpleMode, isReadOnly, depth === 0); if (value.args.length === 1 && value.args[0].type === ParamType.LIST && (value.args[0].value as ListArg).args.length === 0) { diff --git a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts index d9bb0608..7f27cddf 100644 --- a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts +++ b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts @@ -1,8 +1,7 @@ import {Component, computed, Input, Signal, signal, Type} from '@angular/core'; import {PlanArgument, RexArg} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; -import {Parameter, ParamTag, ParamType, SimpleType} from '../../models/polyalg-registry'; -import {DataModel} from '../../../../models/ui-request.model'; +import {OperatorModel, Parameter, ParamTag, ParamType, SimpleType} from '../../models/polyalg-registry'; import {PlanType} from '../../../../models/information-page.model'; @Component({ @@ -39,7 +38,7 @@ export class RexControl extends ArgControl { } }; - constructor(param: Parameter, private value: RexArg, model: DataModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { + constructor(param: Parameter, private value: RexArg, model: OperatorModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { super(param, model, planType, isSimpleMode, isReadOnly); this.showAlias = param.tags.includes(ParamTag.ALIAS); } diff --git a/src/app/components/polyalg/controls/string-arg/string-arg.component.ts b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts index ac4af97d..f97bb6c3 100644 --- a/src/app/components/polyalg/controls/string-arg/string-arg.component.ts +++ b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts @@ -1,8 +1,7 @@ import {Component, computed, Input, Signal, signal, Type} from '@angular/core'; import {PlanArgument, StringArg} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; -import {Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; -import {DataModel} from '../../../../models/ui-request.model'; +import {OperatorModel, Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; import {PlanType} from '../../../../models/information-page.model'; @Component({ @@ -28,7 +27,7 @@ export class StringControl extends ArgControl { return hasTrivialAlias && hasTrivialArg; }); - constructor(param: Parameter, private value: StringArg, model: DataModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { + constructor(param: Parameter, private value: StringArg, model: OperatorModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { super(param, model, planType, isSimpleMode, isReadOnly); this.showAlias = param.tags.includes(ParamTag.ALIAS); } diff --git a/src/app/components/polyalg/custom-socket/custom-socket.component.ts b/src/app/components/polyalg/custom-socket/custom-socket.component.ts index 7a72571e..fe6aff17 100644 --- a/src/app/components/polyalg/custom-socket/custom-socket.component.ts +++ b/src/app/components/polyalg/custom-socket/custom-socket.component.ts @@ -1,6 +1,6 @@ import {ChangeDetectorRef, Component, ElementRef, HostBinding, Input, OnChanges, OnInit} from '@angular/core'; import {ClassicPreset} from 'rete'; -import {DataModel} from '../../../models/ui-request.model'; +import {OperatorModel} from '../models/polyalg-registry'; @Component({ selector: 'app-custom-socket', @@ -37,14 +37,16 @@ export class AlgNodeSocket extends ClassicPreset.Socket { /** * - * @param dataModel the DataModel this socket expects or null if it does not matter + * @param model the OperatorModel this socket expects * @param isMultiValued true if this socket supports multiple incoming connections */ - constructor(public readonly dataModel: DataModel | null, public readonly isMultiValued = false) { + constructor(public readonly model: OperatorModel | null, public readonly isMultiValued = false) { super('AlgNodeSocket'); } isCompatibleWith(socket: AlgNodeSocket) { - return socket.dataModel === this.dataModel || socket.dataModel === null; + return socket.model === this.model || + socket.model === OperatorModel.COMMON || + this.model === OperatorModel.COMMON; } } diff --git a/src/app/components/polyalg/models/polyalg-registry.ts b/src/app/components/polyalg/models/polyalg-registry.ts index 32b99b20..fb41f637 100644 --- a/src/app/components/polyalg/models/polyalg-registry.ts +++ b/src/app/components/polyalg/models/polyalg-registry.ts @@ -1,4 +1,3 @@ -import {DataModel} from '../../../models/ui-request.model'; import {PlanArgument} from './polyalg-plan.model'; export interface PolyAlgRegistry { @@ -9,7 +8,7 @@ export interface PolyAlgRegistry { export interface Declaration { name: string; aliases: string[]; - model: DataModel; + model: OperatorModel; numInputs: number; tags: OperatorTag[]; posParams: Parameter[]; @@ -65,3 +64,13 @@ export enum SimpleType { SIMPLE_COLLATION = 'SIMPLE_COLLATION', SIMPLE_AGG = 'SIMPLE_AGG' } + +/** + * Very similar to the DataModel enum, but also has a COMMON model to indicate that all Models are supported + */ +export enum OperatorModel { + RELATIONAL = 'RELATIONAL', + DOCUMENT = 'DOCUMENT', + GRAPH = 'GRAPH', + COMMON = 'COMMON' +} diff --git a/src/app/components/polyalg/polyalg-viewer/alg-editor-utils.ts b/src/app/components/polyalg/polyalg-viewer/alg-editor-utils.ts index 9c5efcca..b512433b 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-editor-utils.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-editor-utils.ts @@ -3,9 +3,11 @@ import {ClassicPreset, NodeEditor} from 'rete'; import {Schemes} from './alg-editor'; import {AlgNode} from '../algnode/alg-node.component'; import {CustomConnection} from '../custom-connection/custom-connection.component'; -import {DataModel} from '../../../models/ui-request.model'; import {SocketData} from 'rete-connection-plugin'; import {Position} from 'rete-angular-plugin/17/types'; +import {OperatorModel, OperatorTag} from '../models/polyalg-registry'; +import {PolyAlgService} from '../polyalg.service'; +import {PlanType} from '../../../models/information-page.model'; type Sockets = AlgNodeSocket; type Input = ClassicPreset.Input; @@ -43,9 +45,9 @@ export function canCreateConnection(editor: NodeEditor, connection: Sch } export function areSocketsCompatible(editor: NodeEditor, from: SocketData, to: SocketData) { - const fromNode = editor.getNode(from.nodeId); - const toNode = editor.getNode(to.nodeId); - return fromNode.decl.model === toNode.decl.model; + const fromModel = editor.getNode(from.nodeId).decl.model; + const toModel = editor.getNode(to.nodeId).decl.model; + return fromModel === toModel || fromModel === OperatorModel.COMMON || toModel === OperatorModel.COMMON; } export function findRootNodeId(nodes: AlgNode[], connections: CustomConnection[]): string | null { @@ -91,17 +93,26 @@ export function findRootNodeId(nodes: AlgNode[], connections: CustomConnection, sourceId: string, targetId: string) { const target = editor.getNode(targetId); if (target.hasVariableInputs) { @@ -189,6 +200,27 @@ export function getMagneticConnectionProps(editor: NodeEditor) { }; } +export function getContextMenuNodes(isSimpleMode: boolean, registry: PolyAlgService, planType: PlanType, + isReadOnly: boolean, updateSize: (a: AlgNode, delta: Position) => void) { + const nodes = []; + for (const model of Object.keys(OperatorModel).map(key => OperatorModel[key])) { + const innerNodes = []; + for (const decl of registry.getSortedDeclarations(model)) { + if (decl.tags.includes(OperatorTag[planType]) && !(isSimpleMode && decl.tags.includes(OperatorTag.ADVANCED))) { + const displayName = removeModelPrefix(decl.name, decl.model); + innerNodes.push([ + displayName, + () => new AlgNode(decl, planType, null, null, isSimpleMode, isReadOnly, updateSize) + ]); + } + } + if (innerNodes.length > 0) { + nodes.push([model, innerNodes]); + } + } + return nodes; +} + function getSuccessor(nodeId: string, connections: CustomConnection[]): string | null { const outgoing = connections.filter(c => c.source === nodeId); if (outgoing.length === 0) { diff --git a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts index c3cea4fb..84909d47 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts @@ -15,18 +15,16 @@ import {ConnectionPathPlugin, Transformers} from 'rete-connection-path-plugin'; import {getDOMSocketPosition} from 'rete-render-utils'; import {ContextMenuExtra, ContextMenuPlugin, Presets as ContextMenuPresets} from 'rete-context-menu-plugin'; import {PolyAlgService} from '../polyalg.service'; -import {DataModel} from '../../../models/ui-request.model'; import {DataflowEngine} from 'rete-engine'; import {Position} from 'rete-angular-plugin/17/types'; import {Subject} from 'rxjs'; -import {canCreateConnection, findRootNodeId, getMagneticConnectionProps, getModelPrefix, updateMultiConnAfterCreate, updateMultiConnAfterRemove} from './alg-editor-utils'; +import {canCreateConnection, findRootNodeId, getContextMenuNodes, getMagneticConnectionProps, updateMultiConnAfterCreate, updateMultiConnAfterRemove} from './alg-editor-utils'; import {setupPanningBoundary} from './panning-boundary'; import {useMagneticConnection} from './magnetic-connection'; import {MagneticConnectionComponent} from './magnetic-connection/magnetic-connection.component'; import {AlgMetadata} from '../algnode/alg-metadata/alg-metadata.component'; import {Transform} from 'rete-area-plugin/_types/area'; import {PlanType} from '../../../models/information-page.model'; -import {OperatorTag} from '../models/polyalg-registry'; export type Schemes = GetSchemes>; type AreaExtra = AngularArea2D | ContextMenuExtra; @@ -247,27 +245,14 @@ function addNode(registry: PolyAlgService, planType: PlanType, node: PlanNode | function getContextMenuItems(registry: PolyAlgService, userMode: WritableSignal, planType: PlanType, isReadOnly: boolean, updateSize: (a: AlgNode, delta: Position) => void) { - const nodes = []; - for (const model of Object.keys(DataModel).map(key => DataModel[key])) { - const innerNodes = []; - for (const decl of registry.getSortedDeclarations(model)) { - if (decl.tags.includes(OperatorTag[planType])) { - innerNodes.push([ - decl.name.substring(decl.name.indexOf('_') + 1), - () => new AlgNode(decl, planType, null, null, userMode() === UserMode.SIMPLE, isReadOnly, updateSize) - ]); - } - } - if (innerNodes.length > 0) { - nodes.push([model, innerNodes]); - } - } - - - const items = ContextMenuPresets.classic.setup(nodes); + const advancedItems = ContextMenuPresets.classic.setup( + getContextMenuNodes(false, registry, planType, isReadOnly, updateSize)); + const simpleItems = ContextMenuPresets.classic.setup( + getContextMenuNodes(true, registry, planType, isReadOnly, updateSize)); // adjust classic preset to hide the search bar and enable cloning (clone handler of the preset is broken) return (context: any, plugin: any) => { + const items = userMode() === UserMode.SIMPLE ? simpleItems : advancedItems; const result = items(context, plugin); result.searchBar = false; @@ -283,14 +268,6 @@ function getContextMenuItems(registry: PolyAlgService, userMode: WritableSignal< area.translate(node.id, area.area.pointer); } }; - } else if (userMode() === UserMode.SIMPLE) { - result.list.forEach(item => { - const model = getModelPrefix(item.label as DataModel); - item.subitems = item.subitems?.filter(sub => { - const opName = `${model}_${sub.label}`; - return registry.isSimpleOperator(opName); - }); - }); } return result; }; diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html index 9e86e5b0..6fad28bb 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html @@ -50,13 +50,13 @@ diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts index 19388511..89bbd665 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts @@ -8,9 +8,9 @@ import {ToasterService} from '../../toast-exposer/toaster.service'; import {Subscription, timer} from 'rxjs'; import {switchMap} from 'rxjs/operators'; import {ActivatedRoute, Router} from '@angular/router'; -import {DataModel} from '../../../models/ui-request.model'; import {Transform} from 'rete-area-plugin/_types/area'; import {PlanType} from '../../../models/information-page.model'; +import {OperatorModel} from '../models/polyalg-registry'; type editorState = 'SYNCHRONIZED' | 'CHANGED' | 'INVALID' | 'READONLY'; @@ -55,7 +55,7 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { @Input() initialPlan?: string; @Input() planType: PlanType; @Input() isReadOnly: boolean; - @Output() execute = new EventEmitter<[string, DataModel]>(); + @Output() execute = new EventEmitter<[string, OperatorModel]>(); @ViewChild('rete') container!: ElementRef; @ViewChild('textEditor') textEditor: EditorComponent; @@ -187,7 +187,7 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { } - getPolyAlgFromTree(): Promise<[string, DataModel]> { + getPolyAlgFromTree(): Promise<[string, OperatorModel]> { return this.nodeEditor.toPolyAlg(); } diff --git a/src/app/components/polyalg/polyalg.service.ts b/src/app/components/polyalg/polyalg.service.ts index abc5ab34..682ce798 100644 --- a/src/app/components/polyalg/polyalg.service.ts +++ b/src/app/components/polyalg/polyalg.service.ts @@ -1,7 +1,6 @@ import {Injectable, signal} from '@angular/core'; import {CrudService} from '../../services/crud.service'; -import {Declaration, OperatorTag, Parameter, PolyAlgRegistry} from './models/polyalg-registry'; -import {DataModel} from '../../models/ui-request.model'; +import {Declaration, OperatorModel, OperatorTag, Parameter, PolyAlgRegistry} from './models/polyalg-registry'; @Injectable({ providedIn: 'root' @@ -56,7 +55,7 @@ export class PolyAlgService { return this.declarations.get(opName); } - getSortedDeclarations(model: DataModel = null): Declaration[] { + getSortedDeclarations(model: OperatorModel = null): Declaration[] { if (model === null) { return this.sortedDeclarations; } diff --git a/src/app/views/querying/polyalg/polyalg.component.ts b/src/app/views/querying/polyalg/polyalg.component.ts index 1b458771..922d16f9 100644 --- a/src/app/views/querying/polyalg/polyalg.component.ts +++ b/src/app/views/querying/polyalg/polyalg.component.ts @@ -11,6 +11,7 @@ import {WebuiSettingsService} from '../../../services/webui-settings.service'; import {InformationObject, InformationPage, PlanType} from '../../../models/information-page.model'; import {SidebarNode} from '../../../models/sidebar-node.model'; import {BreadcrumbItem} from '../../../components/breadcrumb/breadcrumb-item'; +import {OperatorModel} from '../../../components/polyalg/models/polyalg-registry'; import {DataModel} from '../../../models/ui-request.model'; @Component({ @@ -76,7 +77,7 @@ export class PolyalgComponent implements OnInit, OnDestroy { this.websocket.close(); } - executePolyAlg([polyAlg, model]: [string, DataModel]) { + executePolyAlg([polyAlg, model]: [string, OperatorModel]) { if (polyAlg == null) { this._toast.warn('Plan is invalid'); return; @@ -85,8 +86,11 @@ export class PolyalgComponent implements OnInit, OnDestroy { this._leftSidebar.open(); this.result.set(null); + // if the OperatorModel is COMMON, we use the relational DataModel + const dataModel = model === OperatorModel.COMMON ? DataModel.RELATIONAL : DataModel[model]; + this.loading.set(true); - if (!this._crud.executePolyAlg(this.websocket, polyAlg, model, this.planType)) { + if (!this._crud.executePolyAlg(this.websocket, polyAlg, dataModel, this.planType)) { this.loading.set(false); this.result.set(new RelationalResult('Could not establish a connection with the server.')); } From b68d2cc6fd0807a468c627877a4119fd08301e3f Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Tue, 4 Jun 2024 12:34:34 +0200 Subject: [PATCH 33/54] Remove information-manager instead of hiding it --- src/app/views/querying/console/console.component.html | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/views/querying/console/console.component.html b/src/app/views/querying/console/console.component.html index 830d2653..cdb3e809 100644 --- a/src/app/views/querying/console/console.component.html +++ b/src/app/views/querying/console/console.component.html @@ -68,7 +68,7 @@ {{h.value.displayTime()}} {{h.value.fromNow()}}
    -
    {{_util.limitedString(h.value.query)}}
    +
    {{_util.limitedString( h.value.query )}}
    diff --git a/src/app/components/polyalg/controls/string-arg/string-arg.component.scss b/src/app/components/polyalg/controls/string-arg/string-arg.component.scss index e69de29b..5d3fd3dd 100644 --- a/src/app/components/polyalg/controls/string-arg/string-arg.component.scss +++ b/src/app/components/polyalg/controls/string-arg/string-arg.component.scss @@ -0,0 +1,3 @@ +#alias { + max-width: 35%; +} \ No newline at end of file diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts index 8f8803e3..6c72039f 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts @@ -52,6 +52,7 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { @Input() polyAlg?: string; @Input() initialPlan?: string; + @Input() initialUserMode?: UserMode; @Input() planType: PlanType; @Input() isReadOnly: boolean; @Output() execute = new EventEmitter<[string, OperatorModel]>(); @@ -103,6 +104,10 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { this.showEditButton = this.isReadOnly; this.showMetadata = this.isReadOnly; + if (this.initialUserMode) { + this.userMode.set(this.initialUserMode); + } + this.textEditor.setScrollMargin(5, 5); if (!this.isReadOnly) { this.textEditor.setReadOnly(this.userMode() === UserMode.SIMPLE); diff --git a/src/app/views/querying/polyalg/polyalg.component.html b/src/app/views/querying/polyalg/polyalg.component.html index 917b2786..7f912e7f 100644 --- a/src/app/views/querying/polyalg/polyalg.component.html +++ b/src/app/views/querying/polyalg/polyalg.component.html @@ -14,15 +14,14 @@
    + *ngIf="queryAnalysis && showingAnalysis" + [data]="queryAnalysis"> diff --git a/src/app/views/querying/polyalg/polyalg.component.ts b/src/app/views/querying/polyalg/polyalg.component.ts index 922d16f9..2733700b 100644 --- a/src/app/views/querying/polyalg/polyalg.component.ts +++ b/src/app/views/querying/polyalg/polyalg.component.ts @@ -13,6 +13,7 @@ import {SidebarNode} from '../../../models/sidebar-node.model'; import {BreadcrumbItem} from '../../../components/breadcrumb/breadcrumb-item'; import {OperatorModel} from '../../../components/polyalg/models/polyalg-registry'; import {DataModel} from '../../../models/ui-request.model'; +import {UserMode} from '../../../components/polyalg/polyalg-viewer/alg-editor'; @Component({ selector: 'app-polyalg', @@ -32,6 +33,7 @@ export class PolyalgComponent implements OnInit, OnDestroy { planType: PlanType = 'LOGICAL'; selectedPlanType: PlanType = 'LOGICAL'; + initialUserMode = UserMode.SIMPLE; showPlanTypeModal = signal(false); showHelpModal = signal(false); polyAlg = `PROJECT[employeeno, relationshipjoy AS happiness]( @@ -57,6 +59,7 @@ export class PolyalgComponent implements OnInit, OnDestroy { this.planType = localStorage.getItem('polyalg.planType') as PlanType; this.selectedPlanType = this.planType; localStorage.removeItem('polyalg.planType'); + this.initialUserMode = UserMode.ADVANCED; } if (!this.planType) { From 7ddd64c42734416a9e79d36bd31403a9199b9738 Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Fri, 7 Jun 2024 16:28:26 +0200 Subject: [PATCH 37/54] Add DoubleArg --- src/app/components/components.module.ts | 4 +- .../polyalg/algnode/alg-node.component.html | 8 ++-- .../polyalg/algnode/alg-node.component.ts | 2 +- .../polyalg/controls/arg-control-utils.ts | 24 +++++++++- .../double-arg/double-arg.component.html | 5 +++ .../double-arg/double-arg.component.scss | 0 .../double-arg/double-arg.component.ts | 44 +++++++++++++++++++ .../controls/list-arg/list-arg.component.html | 4 ++ .../polyalg/models/polyalg-plan.model.ts | 4 ++ .../polyalg/models/polyalg-registry.ts | 2 + .../polyalg-viewer/alg-editor-utils.ts | 2 +- 11 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 src/app/components/polyalg/controls/double-arg/double-arg.component.html create mode 100644 src/app/components/polyalg/controls/double-arg/double-arg.component.scss create mode 100644 src/app/components/polyalg/controls/double-arg/double-arg.component.ts diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts index 08aa57e2..bd9df74d 100644 --- a/src/app/components/components.module.ts +++ b/src/app/components/components.module.ts @@ -130,6 +130,7 @@ import {LaxAggArgComponent} from './polyalg/controls/lax-agg/lax-agg-arg.compone import {MagneticConnectionComponent} from './polyalg/polyalg-viewer/magnetic-connection/magnetic-connection.component'; import {PopoverModule} from 'ngx-bootstrap/popover'; import {AlgMetadataComponent} from './polyalg/algnode/alg-metadata/alg-metadata.component'; +import { DoubleArgComponent } from './polyalg/controls/double-arg/double-arg.component'; //import 'hammerjs'; @@ -257,7 +258,8 @@ import {AlgMetadataComponent} from './polyalg/algnode/alg-metadata/alg-metadata. AggArgComponent, LaxAggArgComponent, MagneticConnectionComponent, - AlgMetadataComponent + AlgMetadataComponent, + DoubleArgComponent ], exports: [ BreadcrumbComponent, diff --git a/src/app/components/polyalg/algnode/alg-node.component.html b/src/app/components/polyalg/algnode/alg-node.component.html index dfbd4467..2a6bbb72 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.html +++ b/src/app/components/polyalg/algnode/alg-node.component.html @@ -1,4 +1,4 @@ -{{data.multiConnIdx}} +{{ data.multiConnIdx }}
    @@ -16,12 +16,13 @@

    {{ data.label }}

    Simple - {{data.modelBadge}} + {{ data.modelBadge }}
    +

    (unknown operator)

    {{ data.label }} -
    +
    diff --git a/src/app/components/polyalg/algnode/alg-node.component.ts b/src/app/components/polyalg/algnode/alg-node.component.ts index 53e9bdb6..30f0a657 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.ts +++ b/src/app/components/polyalg/algnode/alg-node.component.ts @@ -100,7 +100,7 @@ export class AlgNode extends ClassicPreset.Node { constructor(public readonly decl: Declaration, public readonly planType: PlanType, args: { [key: string]: PlanArgument } | null, public readonly metadata: AlgMetadata | null, isSimpleMode: boolean, public isReadOnly: boolean, private updateArea: (a: AlgNode, delta: Position) => void) { - super(decl.name.substring(decl.name.indexOf('_') + 1)); + super(decl.convention? decl.name : decl.name.substring(decl.name.indexOf('_') + 1)); this.modelBadge = getModelPrefix(decl.model); this.modelColor = MODEL_COLORS.get(decl.model); this.isSimpleMode = signal(isSimpleMode); diff --git a/src/app/components/polyalg/controls/arg-control-utils.ts b/src/app/components/polyalg/controls/arg-control-utils.ts index 71dce810..a93b950a 100644 --- a/src/app/components/polyalg/controls/arg-control-utils.ts +++ b/src/app/components/polyalg/controls/arg-control-utils.ts @@ -4,7 +4,24 @@ import {BooleanControl} from './boolean-arg/boolean-arg.component'; import {RexControl} from './rex-arg/rex-arg.component'; import {EntityControl} from './entity-arg/entity-arg.component'; import {ListControl} from './list-arg/list-arg.component'; -import {AggArg, BooleanArg, CollationArg, CollDirection, CorrelationArg, defaultNullDirection, EntityArg, EnumArg, FieldArg, IntArg, LaxAggArg, ListArg, PlanArgument, RexArg, StringArg} from '../models/polyalg-plan.model'; +import { + AggArg, + BooleanArg, + CollationArg, + CollDirection, + CorrelationArg, + defaultNullDirection, + DoubleArg, + EntityArg, + EnumArg, + FieldArg, + IntArg, + LaxAggArg, + ListArg, + PlanArgument, + RexArg, + StringArg +} from '../models/polyalg-plan.model'; import {EnumControl} from './enum-arg/enum-arg.component'; import {OperatorModel, Parameter, ParamType} from '../models/polyalg-registry'; import {IntControl} from './int-arg/int-arg.component'; @@ -15,6 +32,7 @@ import {AggControl} from './agg-arg/agg-arg.component'; import {LaxAggControl} from './lax-agg/lax-agg-arg.component'; import {Signal} from '@angular/core'; import {PlanType} from '../../../models/information-page.model'; +import {DoubleControl} from './double-arg/double-arg.component'; export function getControl(param: Parameter, arg: PlanArgument | null, isReadOnly: boolean, depth: number, model: OperatorModel, planType: PlanType, isSimpleMode: Signal): ArgControl { @@ -31,6 +49,8 @@ export function getControl(param: Parameter, arg: PlanArgument | null, isReadOnl break; case 'INTEGER': return new IntControl(param, arg.value as IntArg, model, planType, isSimpleMode, isReadOnly); + case 'DOUBLE': + return new DoubleControl(param, arg.value as DoubleArg, model, planType, isSimpleMode, isReadOnly); case 'STRING': return new StringControl(param, arg.value as StringArg, model, planType, isSimpleMode, isReadOnly); case 'BOOLEAN': @@ -88,6 +108,8 @@ export function getInitialArg(p: Parameter, depth: number): PlanArgument { break; case ParamType.INTEGER: return {arg: 0}; + case ParamType.DOUBLE: + return {arg: 0}; case ParamType.STRING: return {arg: ''}; case ParamType.BOOLEAN: diff --git a/src/app/components/polyalg/controls/double-arg/double-arg.component.html b/src/app/components/polyalg/controls/double-arg/double-arg.component.html new file mode 100644 index 00000000..ac30f2ed --- /dev/null +++ b/src/app/components/polyalg/controls/double-arg/double-arg.component.html @@ -0,0 +1,5 @@ + + diff --git a/src/app/components/polyalg/controls/double-arg/double-arg.component.scss b/src/app/components/polyalg/controls/double-arg/double-arg.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/polyalg/controls/double-arg/double-arg.component.ts b/src/app/components/polyalg/controls/double-arg/double-arg.component.ts new file mode 100644 index 00000000..ab7c62f1 --- /dev/null +++ b/src/app/components/polyalg/controls/double-arg/double-arg.component.ts @@ -0,0 +1,44 @@ +import {Component, Input, Signal, signal, Type} from '@angular/core'; +import {ArgControl} from '../arg-control'; +import {OperatorModel, Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; +import {DoubleArg, PlanArgument} from '../../models/polyalg-plan.model'; +import {PlanType} from '../../../../models/information-page.model'; + +@Component({ + selector: 'app-double-arg', + templateUrl: './double-arg.component.html', + styleUrl: './double-arg.component.scss' +}) +export class DoubleArgComponent { + @Input() data: DoubleControl; +} + +export class DoubleControl extends ArgControl { + valueRange: { min: number | null; max: number | null; }; + height = signal(this.name ? 55 : 31); + + constructor(param: Parameter, public value: DoubleArg, model: OperatorModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { + super(param, model, planType, isSimpleMode, isReadOnly); + + this.valueRange = {min: null, max: null}; + if (param.tags.includes(ParamTag.NON_NEGATIVE)) { + this.valueRange.min = 0; + } + } + + getArgComponent(): Type { + return DoubleArgComponent; + } + + toPolyAlg(): string { + if (this.value.arg == null) { + return ''; + } + return this.value.arg.toString(); + } + + copyArg(): PlanArgument { + return {type: ParamType.DOUBLE, value: JSON.parse(JSON.stringify(this.value))}; + } + +} diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.html b/src/app/components/polyalg/controls/list-arg/list-arg.component.html index d5c892bf..50b64664 100644 --- a/src/app/components/polyalg/controls/list-arg/list-arg.component.html +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.html @@ -22,6 +22,10 @@ + + + + diff --git a/src/app/components/polyalg/models/polyalg-plan.model.ts b/src/app/components/polyalg/models/polyalg-plan.model.ts index 514cb012..a0163b60 100644 --- a/src/app/components/polyalg/models/polyalg-plan.model.ts +++ b/src/app/components/polyalg/models/polyalg-plan.model.ts @@ -80,6 +80,10 @@ export interface IntArg { arg: number; } +export interface DoubleArg { + arg: number; +} + export interface LaxAggArg { arg?: string; function: string; diff --git a/src/app/components/polyalg/models/polyalg-registry.ts b/src/app/components/polyalg/models/polyalg-registry.ts index 769e8676..69f8cc2a 100644 --- a/src/app/components/polyalg/models/polyalg-registry.ts +++ b/src/app/components/polyalg/models/polyalg-registry.ts @@ -13,6 +13,7 @@ export interface Declaration { tags: OperatorTag[]; posParams: Parameter[]; kwParams: Parameter[]; + convention?: string; // only for physical operators notRegistered?: boolean; // Only used by the frontend. Indicates that this declaration is not in the registry. } @@ -33,6 +34,7 @@ export interface Parameter { export enum ParamType { ANY = 'ANY', INTEGER = 'INTEGER', + DOUBLE = 'DOUBLE', STRING = 'STRING', BOOLEAN = 'BOOLEAN', REX = 'REX', diff --git a/src/app/components/polyalg/polyalg-viewer/alg-editor-utils.ts b/src/app/components/polyalg/polyalg-viewer/alg-editor-utils.ts index b512433b..24abbb33 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-editor-utils.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-editor-utils.ts @@ -207,7 +207,7 @@ export function getContextMenuNodes(isSimpleMode: boolean, registry: PolyAlgServ const innerNodes = []; for (const decl of registry.getSortedDeclarations(model)) { if (decl.tags.includes(OperatorTag[planType]) && !(isSimpleMode && decl.tags.includes(OperatorTag.ADVANCED))) { - const displayName = removeModelPrefix(decl.name, decl.model); + const displayName = decl.convention ? decl.name : removeModelPrefix(decl.name, decl.model); innerNodes.push([ displayName, () => new AlgNode(decl, planType, null, null, isSimpleMode, isReadOnly, updateSize) From c3d31359825224edc6ee51c284e284bc6b915a1b Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Thu, 6 Jun 2024 14:38:56 +0200 Subject: [PATCH 38/54] Add various forms of input validation --- .../controls/agg-arg/agg-arg.component.html | 4 +- .../polyalg/controls/arg-control-utils.ts | 43 ++++++++++++++++++ .../entity-arg/entity-arg.component.html | 30 +++++++++++-- .../entity-arg/entity-arg.component.scss | 44 +++++++++++++++++++ .../entity-arg/entity-arg.component.ts | 21 ++++++++- .../controls/rex-arg/rex-arg.component.html | 18 ++++---- .../controls/rex-arg/rex-arg.component.ts | 13 +++++- .../string-arg/string-arg.component.ts | 7 ++- .../polyalg/models/polyalg-plan.model.ts | 5 ++- .../polyalg-viewer/alg-validator.service.ts | 40 +++++++++++------ .../polyalg-viewer/alg-viewer.component.html | 12 ++--- .../polyalg-viewer/alg-viewer.component.ts | 9 +++- .../querying/polyalg/polyalg.component.html | 4 +- 13 files changed, 205 insertions(+), 45 deletions(-) diff --git a/src/app/components/polyalg/controls/agg-arg/agg-arg.component.html b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.html index 14860e8f..fb0b5e00 100644 --- a/src/app/components/polyalg/controls/agg-arg/agg-arg.component.html +++ b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.html @@ -13,7 +13,7 @@ - Function Argument + Function Arg @@ -34,7 +34,7 @@ - Function Argument(s) + Function Arg(s) diff --git a/src/app/components/polyalg/controls/arg-control-utils.ts b/src/app/components/polyalg/controls/arg-control-utils.ts index a93b950a..ae93f447 100644 --- a/src/app/components/polyalg/controls/arg-control-utils.ts +++ b/src/app/components/polyalg/controls/arg-control-utils.ts @@ -137,6 +137,49 @@ export function getInitialArg(p: Parameter, depth: number): PlanArgument { })(), isEnum: p.isEnum }; +} + +/** + * Checks whether the string has balanced parantheses and separators (',') only in valid locations. + * A valid location is if the separator is within parentheses, brackets or is quoted. + * @param str the string to check + */ +export function hasValidStructure(str: string) { + const separators = new Set([',']); + str = str.trim(); + let inSingleQuotes = false; + let inDoubleQuotes = false; + const stack: string[] = []; + + for (let i = 0; i < str.length; i++) { + const char = str[i]; + if (char === '\\') { + i++; + continue; // Skip the next character if it's escaped + } + if (char === '\'' && !inDoubleQuotes) { + inSingleQuotes = !inSingleQuotes; + } else if (char === '"' && !inSingleQuotes) { + inDoubleQuotes = !inDoubleQuotes; + } else if (!inSingleQuotes && !inDoubleQuotes) { + if (char === '(' || char === '[') { + stack.push(char); + } else if (char === ')') { + const lastOpen = stack.pop(); + if (!lastOpen || lastOpen !== '(') { + return false; + } + } else if (char === ']') { + const lastOpen = stack.pop(); + if (!lastOpen || lastOpen !== '[') { + return false; + } + } else if (separators.has(char) && stack.length === 0) { + return false; + } + } + } + return stack.length === 0 && !inSingleQuotes && !inDoubleQuotes; } diff --git a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.html b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.html index 0d7bbfe8..97677c8c 100644 --- a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.html +++ b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.html @@ -1,9 +1,21 @@
    - + + +
    + +
    @@ -16,8 +28,18 @@ +
    + + + +
    + + +
    +
    \ No newline at end of file diff --git a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.scss b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.scss index e69de29b..8935bdf3 100644 --- a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.scss +++ b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.scss @@ -0,0 +1,44 @@ +/* autocomplete package */ +.ng-autocomplete { + width: 100% !important; +} + +.autocomplete-container { + box-shadow: none !important; + border: 1px solid #d8dbe0; + border-radius: 0.25rem; + line-height: 2; + height: auto !important; +} + +.input-container { + height: initial; + line-height: 2; + + input { + background-color: transparent !important; + height: auto !important; + line-height: 1 !important; + padding: 0.25rem 0.5rem !important; + } +} + +.autocomplete-container .suggestions-container ul li a { + padding: 0 0.5em !important; +} + +.input-container input::placeholder { + font-size: 0.765625rem; +} + +/* bootstrap styles */ +.autocomplete-container { + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.autocomplete-container:focus-within { + border: 1px #8ad4ee solid !important; + border-radius: 0.25rem; + outline: 0 !important; + box-shadow: 0 0 0 0.2rem rgba(32, 168, 216, 0.25) !important; +} \ No newline at end of file diff --git a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts index fe3a657d..edbc3c2d 100644 --- a/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts +++ b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts @@ -1,4 +1,4 @@ -import {Component, computed, inject, Input, OnInit, Signal, signal, Type} from '@angular/core'; +import {Component, computed, inject, Input, OnInit, Signal, signal, Type, ViewEncapsulation} from '@angular/core'; import {EntityArg, PlanArgument} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; import {OperatorModel, Parameter, ParamType} from '../../models/polyalg-registry'; @@ -9,12 +9,14 @@ import {CatalogService} from '../../../../services/catalog.service'; @Component({ selector: 'app-entity-arg', templateUrl: './entity-arg.component.html', - styleUrl: './entity-arg.component.scss' + styleUrl: './entity-arg.component.scss', + encapsulation: ViewEncapsulation.None // for autocomplete styling }) export class EntityArgComponent implements OnInit { @Input() data: EntityControl; placeholder = 'entity.field'; adapters: Signal; + entityNamesList: Signal; private readonly _catalog = inject(CatalogService); @@ -23,6 +25,21 @@ export class EntityArgComponent implements OnInit { this.placeholder = 'entity'; } + this.entityNamesList = computed(() => { + const catalog = this._catalog.listener(); + + const names = []; + for (const schema of catalog.getSchemaTree('', true, 3)) { + if (this.data.model === OperatorModel.GRAPH) { + names.push(schema.name); + } + for (const table of schema.children) { + names.push(schema.name + '.' + table.name); + } + } + return names; + }); + if (this.data.isAllocation) { this.adapters = computed(() => { this._catalog.listener(); diff --git a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.html b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.html index c1c3c646..54102973 100644 --- a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.html +++ b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.html @@ -2,9 +2,10 @@ - + class="param-input px-1" [(ngModel)]="data.simpleValues.REX_PREDICATE.r1"> - + class="param-input px-1" [(ngModel)]="data.simpleValues.REX_PREDICATE.r2"> - + class="param-input px-1" [(ngModel)]="data.rex"> AS - + class="param-input px-1" [(ngModel)]="data.alias"> diff --git a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts index 7f27cddf..8d9fa250 100644 --- a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts +++ b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts @@ -3,6 +3,7 @@ import {PlanArgument, RexArg} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; import {OperatorModel, Parameter, ParamTag, ParamType, SimpleType} from '../../models/polyalg-registry'; import {PlanType} from '../../../../models/information-page.model'; +import {hasValidStructure} from '../arg-control-utils'; @Component({ selector: 'app-rex-arg', @@ -31,12 +32,15 @@ export class RexControl extends ArgControl { 'REX_PREDICATE': { r1: '', r2: '', - operator: '=' + operator: '=', + is1Valid: true, + is2Valid: true }, 'REX_UINT': { i: null } }; + isRexValid = true; constructor(param: Parameter, private value: RexArg, model: OperatorModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { super(param, model, planType, isSimpleMode, isReadOnly); @@ -50,14 +54,19 @@ export class RexControl extends ArgControl { toPolyAlg(): string { if (this.simpleType() === SimpleType.REX_PREDICATE) { const values = this.simpleValues.REX_PREDICATE; + values.is1Valid = hasValidStructure(values.r1); + values.is2Valid = hasValidStructure(values.r2); const polyAlg = `${values.operator}(${values.r1}, ${values.r2})`; this.rex.set(polyAlg); } else if (this.simpleType() === SimpleType.REX_UINT) { this.rex.set(this.simpleValues.REX_UINT.i?.toString(10)); + } else { + this.isRexValid = hasValidStructure(this.rex()); } if (this.showAlias && this.alias() && this.alias() !== this.rex()) { - return `${this.rex()} AS ${this.alias()}`; + const cleanedAlias = hasValidStructure(this.alias()) ? this.alias() : `'${this.alias()}'`; + return `${this.rex()} AS ${cleanedAlias}`; } return this.rex(); } diff --git a/src/app/components/polyalg/controls/string-arg/string-arg.component.ts b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts index f97bb6c3..f0524793 100644 --- a/src/app/components/polyalg/controls/string-arg/string-arg.component.ts +++ b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts @@ -3,6 +3,7 @@ import {PlanArgument, StringArg} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; import {OperatorModel, Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; import {PlanType} from '../../../../models/information-page.model'; +import {hasValidStructure} from '../arg-control-utils'; @Component({ selector: 'app-string-arg', @@ -37,10 +38,12 @@ export class StringControl extends ArgControl { } toPolyAlg(): string { + const cleanedArg = hasValidStructure(this.arg()) ? this.arg() : `'${this.arg()}'`; if (this.showAlias && this.alias() !== '' && this.alias() !== this.arg()) { - return `${this.arg()} AS ${this.alias()}`; + const cleanedAlias = hasValidStructure(this.alias()) ? this.alias() : `'${this.alias()}'`; + return `${cleanedArg} AS ${cleanedAlias}`; } - return this.arg(); + return cleanedArg; } copyArg(): PlanArgument { diff --git a/src/app/components/polyalg/models/polyalg-plan.model.ts b/src/app/components/polyalg/models/polyalg-plan.model.ts index a0163b60..0c62681e 100644 --- a/src/app/components/polyalg/models/polyalg-plan.model.ts +++ b/src/app/components/polyalg/models/polyalg-plan.model.ts @@ -49,8 +49,9 @@ export interface PlanArgument { export interface EntityArg { fullName: string; - adapterName?: string; - partitionId?: string; + adapterName?: string; // not null in case of an AllocationEntity + partitionId?: string; // not null in case of an AllocationEntity + partitionName?: string; // might be null } export interface RexArg { diff --git a/src/app/components/polyalg/polyalg-viewer/alg-validator.service.ts b/src/app/components/polyalg/polyalg-viewer/alg-validator.service.ts index c5db8a04..ff8d9584 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-validator.service.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-validator.service.ts @@ -73,19 +73,33 @@ export class AlgValidatorService { areParenthesesBalanced(str: string) { const stack: string[] = []; - // TODO: ignore parentheses in quoted text - for (const char of str) { - if (char === '(' || char === '[') { - stack.push(char); - } else if (char === ')') { - const lastOpen = stack.pop(); - if (!lastOpen || lastOpen !== '(') { - return false; - } - } else if (char === ']') { - const lastOpen = stack.pop(); - if (!lastOpen || lastOpen !== '[') { - return false; + let inSingleQuotes = false; + let inDoubleQuotes = false; + + for (let i = 0; i < str.length; i++) { + const char = str[i]; + if (char === '\\') { + i++; + continue; // Skip the next character if it's escaped + } + + if (char === '\'' && !inDoubleQuotes) { + inSingleQuotes = !inSingleQuotes; + } else if (char === '"' && !inSingleQuotes) { + inDoubleQuotes = !inDoubleQuotes; + } else if (!inSingleQuotes && !inDoubleQuotes) { + if (char === '(' || char === '[') { + stack.push(char); + } else if (char === ')') { + const lastOpen = stack.pop(); + if (!lastOpen || lastOpen !== '(') { + return false; + } + } else if (char === ']') { + const lastOpen = stack.pop(); + if (!lastOpen || lastOpen !== '[') { + return false; + } } } } diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html index f83149ab..8207f9e5 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html @@ -1,10 +1,10 @@ - + -
    -
    +

    {{ data.label }}

    -
    +

    Simple {{ data.modelBadge }} -

    +
    diff --git a/src/app/components/polyalg/algnode/alg-node.component.scss b/src/app/components/polyalg/algnode/alg-node.component.scss index 62c11d8c..43bd1f52 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.scss +++ b/src/app/components/polyalg/algnode/alg-node.component.scss @@ -8,7 +8,6 @@ $collapse-width: 16px; $socket-margin: 6px; $socket-size: 16px; - :host { display: block; background: white; @@ -55,12 +54,16 @@ $socket-size: 16px; border-radius: 4px 4px 0 0; } + .title-wrapper.auxiliary { + background-color: $secondary; + } + .simple-badge:hover { background-color: $danger !important; } &:hover { - border-color: $primary; + border-color: $primary !important; box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); .title-wrapper { @@ -69,7 +72,7 @@ $socket-size: 16px; } &.selected { - border-color: $primary; + border-color: $primary !important; .title-wrapper { background-color: $primary !important; @@ -123,6 +126,8 @@ $socket-size: 16px; .control { padding: $socket-margin math.div($socket-size, 2) + $socket-margin; } +} - +:host:has(.auxiliary) { + border: 4px solid $secondary; } diff --git a/src/app/components/polyalg/algnode/alg-node.component.ts b/src/app/components/polyalg/algnode/alg-node.component.ts index 4932fb4d..966c46b2 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.ts +++ b/src/app/components/polyalg/algnode/alg-node.component.ts @@ -93,7 +93,8 @@ export class AlgNode extends ClassicPreset.Node { readonly modelColor: string; readonly isSimpleMode: WritableSignal; readonly hasSimpleParams: boolean; // true if at least one parameter has a simple variant (even if it's hidden) - readonly hasVisibleControls; + readonly hasVisibleControls: Signal; + readonly isAuxiliary: boolean; multiConnIdx: number | null = null; // in the case that the output of this node is connected to a node that allows multiple connections, this indicates the order @@ -104,6 +105,7 @@ export class AlgNode extends ClassicPreset.Node { this.modelBadge = getModelPrefix(decl.model); this.modelColor = MODEL_COLORS.get(decl.model); this.isSimpleMode = signal(isSimpleMode); + this.isAuxiliary = metadata?.isAuxiliary; if (metadata) { this.isMetaVisible.set(true); diff --git a/src/app/components/polyalg/controls/agg-arg/agg-arg.component.html b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.html index fb0b5e00..aa301b4f 100644 --- a/src/app/components/polyalg/controls/agg-arg/agg-arg.component.html +++ b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.html @@ -62,8 +62,6 @@ class="form-control form-control-sm" [(ngModel)]="data.value.filter"> -

    WITHIN GROUP: TODO ({{data.value.collList}})

    - diff --git a/src/app/components/polyalg/controls/agg-arg/agg-arg.component.ts b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.ts index 1d3b77ed..150804e7 100644 --- a/src/app/components/polyalg/controls/agg-arg/agg-arg.component.ts +++ b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.ts @@ -5,6 +5,7 @@ import {AggArg, PlanArgument} from '../../models/polyalg-plan.model'; import {CollationControl} from '../collation-arg/collation-arg.component'; import {PolyAlgService} from '../../polyalg.service'; import {PlanType} from '../../../../models/information-page.model'; +import {sanitizeAlias} from '../arg-control-utils'; @Component({ selector: 'app-agg-arg', @@ -15,6 +16,7 @@ export class AggArgComponent implements OnInit { constructor(private _registry: PolyAlgService) { } + @Input() data: AggControl; // TODO: support multiple args, colls fChoices: string[] = []; fChoicesSimple = ['AVG', 'COUNT', 'MAX', 'MIN', 'SUM'].sort(); @@ -48,7 +50,11 @@ export class AggControl extends ArgControl { const collation = this.value.collList.length > 0 ? ` WITHIN GROUP (${this.value.collList.map(coll => CollationControl.collToPolyAlg(coll)).join(', ')})` : ''; const filter = this.value.filter ? ' FILTER ' + this.value.filter : ''; - const alias = this.value.alias ? ` AS ${this.value.alias}` : ''; + + let alias = ''; + if (this.value.alias) { + alias = ' AS ' + sanitizeAlias(this.value.alias); + } return `${this.value.function}(${distStr}${argList})${approx}${collation}${filter}${alias}`; } diff --git a/src/app/components/polyalg/controls/arg-control-utils.ts b/src/app/components/polyalg/controls/arg-control-utils.ts index 5947e1ad..dfff915e 100644 --- a/src/app/components/polyalg/controls/arg-control-utils.ts +++ b/src/app/components/polyalg/controls/arg-control-utils.ts @@ -171,3 +171,13 @@ export function hasValidStructure(str: string) { } return stack.length === 0 && !inSingleQuotes && !inDoubleQuotes; } + +export function sanitizeAlias(alias: string) { + if ((alias.startsWith('\'') && alias.endsWith('\'')) || (alias.startsWith('"') && alias.endsWith('"'))) { + return alias; + } + if (alias.match(/^[a-zA-Z#$@öÖäÄüÜàÀçÇáÁèÈíÍîÎóÓòôÔÒíÍëËâÂïÏéÉñÑß.\d-]*$/)) { + return alias; + } + return '\'' + alias + '\''; +} diff --git a/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts b/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts index a046a95f..b620a3ad 100644 --- a/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts +++ b/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts @@ -4,6 +4,7 @@ import {OperatorModel, Parameter, ParamType} from '../../models/polyalg-registry import {LaxAggArg, PlanArgument} from '../../models/polyalg-plan.model'; import {PolyAlgService} from '../../polyalg.service'; import {PlanType} from '../../../../models/information-page.model'; +import {sanitizeAlias} from '../arg-control-utils'; @Component({ selector: 'app-lax-agg-arg', @@ -35,8 +36,12 @@ export class LaxAggControl extends ArgControl { } toPolyAlg(): string { - const alias = this.value.alias ? ` AS ${this.value.alias}` : ''; - return `${this.value.function}(${this.value.input})${alias}`; + const functionCall = `${this.value.function}(${this.value.input})`; + if (this.value.alias && functionCall !== this.value.alias) { + const cleanedAlias = sanitizeAlias(this.value.alias); + return `${functionCall} AS ${cleanedAlias}`; + } + return functionCall; } copyArg(): PlanArgument { diff --git a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts index 8d9fa250..df4e8fdd 100644 --- a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts +++ b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts @@ -3,7 +3,7 @@ import {PlanArgument, RexArg} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; import {OperatorModel, Parameter, ParamTag, ParamType, SimpleType} from '../../models/polyalg-registry'; import {PlanType} from '../../../../models/information-page.model'; -import {hasValidStructure} from '../arg-control-utils'; +import {hasValidStructure, sanitizeAlias} from '../arg-control-utils'; @Component({ selector: 'app-rex-arg', @@ -16,6 +16,9 @@ export class RexArgComponent { protected readonly SimpleType = SimpleType; } +const NODE_PREFIX = 'PolyNode '; +const PATH_PREFIX = 'PolyPath '; + export class RexControl extends ArgControl { readonly showAlias: boolean; height = signal(this.name ? 55 : 31); @@ -41,10 +44,19 @@ export class RexControl extends ArgControl { } }; isRexValid = true; + isPolyNode = false; + isPolyPath = false; constructor(param: Parameter, private value: RexArg, model: OperatorModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { super(param, model, planType, isSimpleMode, isReadOnly); this.showAlias = param.tags.includes(ParamTag.ALIAS); + this.isPolyNode = this.param.tags.includes(ParamTag.POLY_NODE); + this.isPolyPath = this.param.tags.includes(ParamTag.POLY_PATH); + if (this.isPolyNode && this.rex().startsWith(NODE_PREFIX)) { + this.rex.update(s => s.substring(NODE_PREFIX.length)); + } else if (this.isPolyPath && this.rex().startsWith('PolyPath ')) { + this.rex.update(s => s.substring('PolyPath '.length)); + } } getArgComponent(): Type { @@ -60,13 +72,17 @@ export class RexControl extends ArgControl { this.rex.set(polyAlg); } else if (this.simpleType() === SimpleType.REX_UINT) { this.rex.set(this.simpleValues.REX_UINT.i?.toString(10)); + } else if (this.isPolyNode) { + return this.rex().startsWith(NODE_PREFIX) ? this.rex() : NODE_PREFIX + this.rex(); + + } else if (this.isPolyPath) { + return this.rex().startsWith(PATH_PREFIX) ? this.rex() : PATH_PREFIX + this.rex(); } else { this.isRexValid = hasValidStructure(this.rex()); } if (this.showAlias && this.alias() && this.alias() !== this.rex()) { - const cleanedAlias = hasValidStructure(this.alias()) ? this.alias() : `'${this.alias()}'`; - return `${this.rex()} AS ${cleanedAlias}`; + return `${this.rex()} AS ${sanitizeAlias(this.alias())}`; } return this.rex(); } diff --git a/src/app/components/polyalg/controls/string-arg/string-arg.component.ts b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts index f0524793..3c62f795 100644 --- a/src/app/components/polyalg/controls/string-arg/string-arg.component.ts +++ b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts @@ -3,7 +3,7 @@ import {PlanArgument, StringArg} from '../../models/polyalg-plan.model'; import {ArgControl} from '../arg-control'; import {OperatorModel, Parameter, ParamTag, ParamType} from '../../models/polyalg-registry'; import {PlanType} from '../../../../models/information-page.model'; -import {hasValidStructure} from '../arg-control-utils'; +import {hasValidStructure, sanitizeAlias} from '../arg-control-utils'; @Component({ selector: 'app-string-arg', @@ -40,8 +40,7 @@ export class StringControl extends ArgControl { toPolyAlg(): string { const cleanedArg = hasValidStructure(this.arg()) ? this.arg() : `'${this.arg()}'`; if (this.showAlias && this.alias() !== '' && this.alias() !== this.arg()) { - const cleanedAlias = hasValidStructure(this.alias()) ? this.alias() : `'${this.alias()}'`; - return `${cleanedArg} AS ${cleanedAlias}`; + return `${cleanedArg} AS ${sanitizeAlias(this.alias())}`; } return cleanedArg; } diff --git a/src/app/components/polyalg/models/polyalg-registry.ts b/src/app/components/polyalg/models/polyalg-registry.ts index 69f8cc2a..591ade7f 100644 --- a/src/app/components/polyalg/models/polyalg-registry.ts +++ b/src/app/components/polyalg/models/polyalg-registry.ts @@ -57,7 +57,9 @@ export enum OperatorTag { export enum ParamTag { ALIAS = 'ALIAS', NON_NEGATIVE = 'NON_NEGATIVE', - HIDE_TRIVIAL = 'HIDE_TRIVIAL' + HIDE_TRIVIAL = 'HIDE_TRIVIAL', + POLY_NODE = 'POLY_NODE', + POLY_PATH = 'POLY_PATH', } export enum SimpleType { diff --git a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts index 4066e9e0..bc5d0037 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts @@ -227,10 +227,6 @@ function addNode(registry: PolyAlgService, planType: PlanType, node: PlanNode | const metadata = node.metadata ? new AlgMetadata(node.metadata) : null; const decl = registry.getDeclaration(node.opName) || registry.createDeclarationForUndef(node.opName, node.inputs, planType); const algNode = new AlgNode(decl, planType, node.arguments, metadata, false, isReadOnly, updateSize); - if (node.opName.endsWith('#')) { - // TODO: handle implicit project correctly - algNode.label = 'PROJECT#'; - } for (let i = 0; i < node.inputs.length; i++) { const [childNodes, childConnections] = addNode(registry, planType, node.inputs[i], isReadOnly, updateSize); diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html index 1b25e2d4..0707c03b 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html @@ -71,7 +71,7 @@
    - + diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts index eea4b7fc..a6795703 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts @@ -35,6 +35,7 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { untracked(() => { this.modifySubscription?.unsubscribe(); const oldTransform = this.nodeEditor ? this.nodeEditor.getTransform() : null; + this.nodeEditor?.destroy(); createEditor(el, this.injector, _registry, this.polyAlgPlan(), this.planTypeSignal(), this.isReadOnly, this.userMode, oldTransform) .then(editor => { this.nodeEditor = editor; @@ -135,6 +136,9 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { } if (changes.planType) { + if (this.planType === 'PHYSICAL' && this.planTypeSignal() != null) { + this.setUserMode(UserMode.ADVANCED); // user mode for PHYSICAL is Advanced by default + } this.planTypeSignal.set(this.planType); } diff --git a/src/app/models/ui-request.model.ts b/src/app/models/ui-request.model.ts index 82ba023a..de6eceb6 100644 --- a/src/app/models/ui-request.model.ts +++ b/src/app/models/ui-request.model.ts @@ -68,13 +68,17 @@ export class PolyAlgRequest extends UIRequest { polyAlg: string; model: DataModel; planType: PlanType; + dynamicValues: string[]; + dynamicTypes: string[]; noLimit = false; // TODO: handle queries with large results - constructor(polyAlg: string, model: DataModel, planType: PlanType) { + constructor(polyAlg: string, model: DataModel, planType: PlanType, dynamicValues = null, dynamicTypes = null) { super(); this.polyAlg = polyAlg; this.model = model; this.planType = planType; + this.dynamicValues = dynamicValues; + this.dynamicTypes = dynamicTypes; } } diff --git a/src/app/services/crud.service.ts b/src/app/services/crud.service.ts index 629d5522..75bd5e65 100644 --- a/src/app/services/crud.service.ts +++ b/src/app/services/crud.service.ts @@ -667,6 +667,11 @@ export class CrudService { return socket.sendMessage(request); } + executePhysicalPolyAlg(socket: WebSocket, polyAlg: string, model: DataModel, dynamicValues: string[], dynamicTypes: string[]) { + const request = new PolyAlgRequest(polyAlg, model, 'PHYSICAL', dynamicValues, dynamicTypes); + return socket.sendMessage(request); + } + buildTreeFromPolyAlg(polyAlg: string, planType: PlanType) { const request = new PolyAlgRequest(polyAlg, DataModel.RELATIONAL, planType); // datamodel doesn't matter when building the plan return this._http.post(`${this.httpUrl}/buildPolyPlan`, request, this.httpOptions); diff --git a/src/app/views/querying/polyalg/polyalg.component.html b/src/app/views/querying/polyalg/polyalg.component.html index b430ba12..e6d2eaab 100644 --- a/src/app/views/querying/polyalg/polyalg.component.html +++ b/src/app/views/querying/polyalg/polyalg.component.html @@ -101,3 +101,48 @@
    Help
    + + + +
    Execute Plan
    + +
    + + Please specify the dynamic parameters: + + {{param[0]}}:{{param[1]}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    diff --git a/src/app/views/querying/polyalg/polyalg.component.ts b/src/app/views/querying/polyalg/polyalg.component.ts index 2733700b..6eb654ba 100644 --- a/src/app/views/querying/polyalg/polyalg.component.ts +++ b/src/app/views/querying/polyalg/polyalg.component.ts @@ -36,6 +36,7 @@ export class PolyalgComponent implements OnInit, OnDestroy { initialUserMode = UserMode.SIMPLE; showPlanTypeModal = signal(false); showHelpModal = signal(false); + showParamsModal = signal(false); polyAlg = `PROJECT[employeeno, relationshipjoy AS happiness]( FILTER[<(age0, 30)]( JOIN[=(employeeno, employeeno0)]( @@ -43,6 +44,8 @@ export class PolyalgComponent implements OnInit, OnDestroy { PROJECT[employeeno AS employeeno0, age AS age0]( PROJECT[employeeno, age]( SCAN[public.emp])))))`; + physicalExecForm: { polyAlg: string, model: DataModel, params: string[][], values: (string | boolean)[] } + = {polyAlg: null, model: null, params: null, values: null}; constructor( private _crud: CrudService, @@ -85,13 +88,44 @@ export class PolyalgComponent implements OnInit, OnDestroy { this._toast.warn('Plan is invalid'); return; } + // if the OperatorModel is COMMON, we use the relational DataModel + const dataModel = model === OperatorModel.COMMON ? DataModel.RELATIONAL : DataModel[model]; + + if (this.planType === 'PHYSICAL') { + const regex = /\?\d+:[A-Z_]+(\([\w,\s]+\))?/g; // matches '?0:INTEGER' + const matches = [...new Set(polyAlg.match(regex))]; + const params = matches.map(m => m.substring(1).split(':')).sort((a, b) => +a[0] - +b[0]); + for (let i = 0; i < params.length; i++) { + const type = params[i][1]; + const idx = type.indexOf('('); + params[i].push(idx === -1 ? type : type.substring(0, idx)); + } + + let values: (string | boolean)[] = params.map(p => p[1] === 'BOOLEAN' ? false : ''); + if (this.physicalExecForm.values != null && JSON.stringify(this.physicalExecForm.params) === JSON.stringify(params)) { + console.log('equal params!'); + values = this.physicalExecForm.values; // keep existing values + } else { + console.log('not equal params: ', this.physicalExecForm.params, params); + } + this.physicalExecForm = { + polyAlg: polyAlg, + model: dataModel, + params: params, + values: values + }; + if (matches.length === 0) { + this.executePhysicalPlan(); + } else { + this.showParamsModal.set(true); + } + return; + } + this._leftSidebar.setNodes([]); this._leftSidebar.open(); this.result.set(null); - // if the OperatorModel is COMMON, we use the relational DataModel - const dataModel = model === OperatorModel.COMMON ? DataModel.RELATIONAL : DataModel[model]; - this.loading.set(true); if (!this._crud.executePolyAlg(this.websocket, polyAlg, dataModel, this.planType)) { this.loading.set(false); @@ -99,6 +133,28 @@ export class PolyalgComponent implements OnInit, OnDestroy { } } + executePhysicalPlan() { + console.log('executing', this.physicalExecForm.values); + this.showParamsModal.set(false); + this._leftSidebar.setNodes([]); + this._leftSidebar.open(); + this.result.set(null); + + const p = this.physicalExecForm; + this.loading.set(true); + if (!this._crud.executePhysicalPolyAlg( + this.websocket, + p.polyAlg, + p.model, + p.values.map(v => `${v}`), + p.params.map(p => p[1]))) { + + this.loading.set(false); + this.result.set(new RelationalResult('Could not establish a connection with the server.')); + } + + } + initWebsocket() { //function to define behavior when clicking on a page link const nodeBehavior = (tree, node, $event) => { @@ -207,6 +263,14 @@ export class PolyalgComponent implements OnInit, OnDestroy { toggleHelpModal() { this.showHelpModal.update(b => !b); } + + handleParamsModalChange($event: boolean) { + this.showParamsModal.set($event); + } + + toggleParamsModal() { + this.showParamsModal.update(b => !b); + } } // https://stackoverflow.com/a/42820432 From 41e840d7e94b5c0ad81eb3e8aa0d91ea95ab817f Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Thu, 20 Jun 2024 14:41:07 +0200 Subject: [PATCH 43/54] Small fixes --- .../polyalg/controls/agg-arg/agg-arg.component.html | 2 +- src/app/components/polyalg/controls/arg-control-utils.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/components/polyalg/controls/agg-arg/agg-arg.component.html b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.html index aa301b4f..790a494b 100644 --- a/src/app/components/polyalg/controls/agg-arg/agg-arg.component.html +++ b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.html @@ -55,7 +55,7 @@ - + Filter Date: Thu, 20 Jun 2024 18:41:26 +0200 Subject: [PATCH 44/54] Partially implement WindowGroupArg --- src/app/components/components.module.ts | 6 ++- .../polyalg/controls/arg-control-utils.ts | 7 +++- .../controls/list-arg/list-arg.component.html | 4 ++ .../window-arg/window-arg.component.html | 17 +++++++++ .../window-arg/window-arg.component.scss | 0 .../window-arg/window-arg.component.ts | 37 +++++++++++++++++++ .../polyalg/models/polyalg-plan.model.ts | 10 ++++- .../polyalg/models/polyalg-registry.ts | 3 +- .../polyalg-viewer/alg-editor-utils.ts | 3 +- 9 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 src/app/components/polyalg/controls/window-arg/window-arg.component.html create mode 100644 src/app/components/polyalg/controls/window-arg/window-arg.component.scss create mode 100644 src/app/components/polyalg/controls/window-arg/window-arg.component.ts diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts index bd9df74d..c261b3a6 100644 --- a/src/app/components/components.module.ts +++ b/src/app/components/components.module.ts @@ -130,7 +130,8 @@ import {LaxAggArgComponent} from './polyalg/controls/lax-agg/lax-agg-arg.compone import {MagneticConnectionComponent} from './polyalg/polyalg-viewer/magnetic-connection/magnetic-connection.component'; import {PopoverModule} from 'ngx-bootstrap/popover'; import {AlgMetadataComponent} from './polyalg/algnode/alg-metadata/alg-metadata.component'; -import { DoubleArgComponent } from './polyalg/controls/double-arg/double-arg.component'; +import {DoubleArgComponent} from './polyalg/controls/double-arg/double-arg.component'; +import {WindowArgComponent} from './polyalg/controls/window-arg/window-arg.component'; //import 'hammerjs'; @@ -259,7 +260,8 @@ import { DoubleArgComponent } from './polyalg/controls/double-arg/double-arg.com LaxAggArgComponent, MagneticConnectionComponent, AlgMetadataComponent, - DoubleArgComponent + DoubleArgComponent, + WindowArgComponent ], exports: [ BreadcrumbComponent, diff --git a/src/app/components/polyalg/controls/arg-control-utils.ts b/src/app/components/polyalg/controls/arg-control-utils.ts index 571e77a5..34faeed6 100644 --- a/src/app/components/polyalg/controls/arg-control-utils.ts +++ b/src/app/components/polyalg/controls/arg-control-utils.ts @@ -4,7 +4,7 @@ import {BooleanControl} from './boolean-arg/boolean-arg.component'; import {RexControl} from './rex-arg/rex-arg.component'; import {EntityControl} from './entity-arg/entity-arg.component'; import {ListControl} from './list-arg/list-arg.component'; -import {AggArg, BooleanArg, CollationArg, CollDirection, CorrelationArg, defaultNullDirection, DoubleArg, EntityArg, EnumArg, FieldArg, IntArg, LaxAggArg, ListArg, PlanArgument, RexArg, StringArg} from '../models/polyalg-plan.model'; +import {AggArg, BooleanArg, CollationArg, CollDirection, CorrelationArg, defaultNullDirection, DoubleArg, EntityArg, EnumArg, FieldArg, IntArg, LaxAggArg, ListArg, PlanArgument, RexArg, StringArg, WindowGroupArg} from '../models/polyalg-plan.model'; import {EnumControl} from './enum-arg/enum-arg.component'; import {OperatorModel, Parameter, ParamType} from '../models/polyalg-registry'; import {IntControl} from './int-arg/int-arg.component'; @@ -16,6 +16,7 @@ import {LaxAggControl} from './lax-agg/lax-agg-arg.component'; import {Signal} from '@angular/core'; import {PlanType} from '../../../models/information-page.model'; import {DoubleControl} from './double-arg/double-arg.component'; +import {WindowControl} from './window-arg/window-arg.component'; export function getControl(param: Parameter, arg: PlanArgument | null, isReadOnly: boolean, depth: number, model: OperatorModel, planType: PlanType, isSimpleMode: Signal): ArgControl { @@ -54,6 +55,8 @@ export function getControl(param: Parameter, arg: PlanArgument | null, isReadOnl return new CollationControl(param, arg.value as CollationArg, model, planType, isSimpleMode, isReadOnly); case 'CORR_ID': return new CorrelationControl(param, arg.value as CorrelationArg, model, planType, isSimpleMode, isReadOnly); + case 'WINDOW_GROUP': + return new WindowControl(param, arg.value as WindowGroupArg, model, planType, isSimpleMode, isReadOnly); } return new StringControl(param, {'arg': JSON.stringify(arg)}, model, planType, isSimpleMode, isReadOnly); } @@ -115,6 +118,8 @@ export function getInitialArg(p: Parameter, depth: number): PlanArgument { }; case ParamType.CORR_ID: return {arg: 0}; + case ParamType.WINDOW_GROUP: + return {isRows: false, lowerBound: '', upperBound: '', aggCalls: [], orderKeys: []}; } return null; })(), diff --git a/src/app/components/polyalg/controls/list-arg/list-arg.component.html b/src/app/components/polyalg/controls/list-arg/list-arg.component.html index edd5a4ce..c65944c1 100644 --- a/src/app/components/polyalg/controls/list-arg/list-arg.component.html +++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.html @@ -47,6 +47,10 @@ + + + + diff --git a/src/app/components/polyalg/controls/window-arg/window-arg.component.html b/src/app/components/polyalg/controls/window-arg/window-arg.component.html new file mode 100644 index 00000000..f627de3d --- /dev/null +++ b/src/app/components/polyalg/controls/window-arg/window-arg.component.html @@ -0,0 +1,17 @@ + +(Warning: not yet fully implemented) +
    + +
    + + + + +

    AggCalls: {{JSON.stringify( data.value.aggCalls )}}

    +

    OrderKeys: {{JSON.stringify( data.value.orderKeys )}}

    \ No newline at end of file diff --git a/src/app/components/polyalg/controls/window-arg/window-arg.component.scss b/src/app/components/polyalg/controls/window-arg/window-arg.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/polyalg/controls/window-arg/window-arg.component.ts b/src/app/components/polyalg/controls/window-arg/window-arg.component.ts new file mode 100644 index 00000000..649be926 --- /dev/null +++ b/src/app/components/polyalg/controls/window-arg/window-arg.component.ts @@ -0,0 +1,37 @@ +import {Component, Input, Signal, signal, Type} from '@angular/core'; +import {ArgControl} from '../arg-control'; +import {PlanArgument, WindowGroupArg} from '../../models/polyalg-plan.model'; +import {OperatorModel, Parameter, ParamType} from '../../models/polyalg-registry'; +import {PlanType} from '../../../../models/information-page.model'; + +@Component({ + selector: 'app-window-arg', + templateUrl: './window-arg.component.html', + styleUrl: './window-arg.component.scss' +}) +export class WindowArgComponent { + @Input() data: WindowControl; + + protected readonly JSON = JSON; +} + +export class WindowControl extends ArgControl { + height = signal(this.name ? 200 : 180); + + constructor(param: Parameter, public value: WindowGroupArg, model: OperatorModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) { + super(param, model, planType, isSimpleMode, isReadOnly); + } + + copyArg(): PlanArgument { + return {type: ParamType.WINDOW_GROUP, value: JSON.parse(JSON.stringify(this.value))}; + } + + getArgComponent(): Type { + return WindowArgComponent; + } + + toPolyAlg(): string { + return 'not implemented'; // Not yet implemented in the backend! + } + +} \ No newline at end of file diff --git a/src/app/components/polyalg/models/polyalg-plan.model.ts b/src/app/components/polyalg/models/polyalg-plan.model.ts index 997d1fcf..fe474894 100644 --- a/src/app/components/polyalg/models/polyalg-plan.model.ts +++ b/src/app/components/polyalg/models/polyalg-plan.model.ts @@ -118,7 +118,15 @@ export interface FieldArg { arg: string; } -type ArgType = EntityArg | RexArg | ListArg | StringArg | BooleanArg | EnumArg | IntArg | LaxAggArg | AggArg | CollationArg | CorrelationArg | FieldArg; +export interface WindowGroupArg { + isRows: boolean; + lowerBound: string; + upperBound: string; + aggCalls: string[]; + orderKeys: CollationArg[]; +} + +type ArgType = EntityArg | RexArg | ListArg | StringArg | BooleanArg | EnumArg | IntArg | LaxAggArg | AggArg | CollationArg | CorrelationArg | FieldArg | WindowGroupArg; export enum CollDirection { ASCENDING = 'ASC', diff --git a/src/app/components/polyalg/models/polyalg-registry.ts b/src/app/components/polyalg/models/polyalg-registry.ts index 591ade7f..2cc2ff08 100644 --- a/src/app/components/polyalg/models/polyalg-registry.ts +++ b/src/app/components/polyalg/models/polyalg-registry.ts @@ -44,7 +44,8 @@ export enum ParamType { FIELD = 'FIELD', LIST = 'LIST', COLLATION = 'COLLATION', - CORR_ID = 'CORR_ID' + CORR_ID = 'CORR_ID', + WINDOW_GROUP = 'WINDOW_GROUP' } export enum OperatorTag { diff --git a/src/app/components/polyalg/polyalg-viewer/alg-editor-utils.ts b/src/app/components/polyalg/polyalg-viewer/alg-editor-utils.ts index 24abbb33..060fdd0e 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-editor-utils.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-editor-utils.ts @@ -206,7 +206,8 @@ export function getContextMenuNodes(isSimpleMode: boolean, registry: PolyAlgServ for (const model of Object.keys(OperatorModel).map(key => OperatorModel[key])) { const innerNodes = []; for (const decl of registry.getSortedDeclarations(model)) { - if (decl.tags.includes(OperatorTag[planType]) && !(isSimpleMode && decl.tags.includes(OperatorTag.ADVANCED))) { + if (decl.tags.includes(OperatorTag[planType]) && !(isSimpleMode && decl.tags.includes(OperatorTag.ADVANCED)) && + !decl.notRegistered) { const displayName = decl.convention ? decl.name : removeModelPrefix(decl.name, decl.model); innerNodes.push([ displayName, From f32ee2b79402863d020fafd4d06eaecdbaccba6d Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Fri, 21 Jun 2024 16:23:43 +0200 Subject: [PATCH 45/54] Improve error handling --- .../polyalg-viewer/alg-viewer.component.html | 4 +- .../polyalg-viewer/alg-viewer.component.ts | 72 ++++++++++--------- .../querying/polyalg/polyalg.component.html | 25 +++++-- .../querying/polyalg/polyalg.component.ts | 7 +- 4 files changed, 63 insertions(+), 45 deletions(-) diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html index 0707c03b..e475ff9c 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html @@ -49,13 +49,13 @@
    - - diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts index a6795703..fb26b1e3 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts @@ -21,37 +21,6 @@ type editorState = 'SYNCHRONIZED' | 'CHANGED' | 'INVALID' | 'READONLY'; }) export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { - constructor(private injector: Injector, - private _registry: PolyAlgService, - private _toast: ToasterService, - private _validator: AlgValidatorService, - private _router: Router, - private _route: ActivatedRoute) { - - effect(() => { - const el = this.container.nativeElement; - - if (this.showNodeEditor() && this.polyAlgPlan() !== undefined && this.planTypeSignal() !== undefined && el) { - untracked(() => { - this.modifySubscription?.unsubscribe(); - const oldTransform = this.nodeEditor ? this.nodeEditor.getTransform() : null; - this.nodeEditor?.destroy(); - createEditor(el, this.injector, _registry, this.polyAlgPlan(), this.planTypeSignal(), this.isReadOnly, this.userMode, oldTransform) - .then(editor => { - this.nodeEditor = editor; - this.generateTextFromNodeEditor(); - this.modifySubscription = this.nodeEditor.onModify.pipe( - switchMap(() => { - this.nodeEditorState.set('CHANGED'); - return timer(500); - }) - ).subscribe(() => this.generateTextFromNodeEditor(true)); - }); - }); - } - }); - } - @Input() polyAlg?: string; @Input() initialPlan?: string; @Input() initialUserMode?: UserMode; @@ -100,9 +69,39 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { fontSize: '1rem', tabSize: 2 }; - protected readonly UserMode = UserMode; + constructor(private injector: Injector, + private _registry: PolyAlgService, + private _toast: ToasterService, + private _validator: AlgValidatorService, + private _router: Router, + private _route: ActivatedRoute) { + + effect(() => { + const el = this.container.nativeElement; + + if (this.showNodeEditor() && this.polyAlgPlan() !== undefined && this.planTypeSignal() !== undefined && el) { + untracked(() => { + this.modifySubscription?.unsubscribe(); + const oldTransform = this.nodeEditor ? this.nodeEditor.getTransform() : null; + this.nodeEditor?.destroy(); + createEditor(el, this.injector, _registry, this.polyAlgPlan(), this.planTypeSignal(), this.isReadOnly, this.userMode, oldTransform) + .then(editor => { + this.nodeEditor = editor; + this.generateTextFromNodeEditor(); + this.modifySubscription = this.nodeEditor.onModify.pipe( + switchMap(() => { + this.nodeEditorState.set('CHANGED'); + return timer(500); + }) + ).subscribe(() => this.generateTextFromNodeEditor(true)); + }); + }); + } + }); + } + ngAfterViewInit(): void { this.showEditButton = this.isReadOnly; this.showMetadata = this.isReadOnly; @@ -127,10 +126,13 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { } this._validator.buildPlan(this.polyAlg, this.planType).subscribe({ next: (plan) => this.polyAlgPlan.set(plan), - error: () => { - this.nodeEditorState.set('INVALID'); - this.textEditorState.set('INVALID'); + error: (err) => { this.textEditor.setCode(this.polyAlg); + this.textEditorState.set('INVALID'); + this.textEditorError.set(err.error.errorMsg); + this.nodeEditorState.set('INVALID'); + this.nodeEditorError.set(err.error.errorMsg); + this._toast.error('Unable to build the initial plan. It most likely contains a (yet) unsupported feature.'); } }); } diff --git a/src/app/views/querying/polyalg/polyalg.component.html b/src/app/views/querying/polyalg/polyalg.component.html index e6d2eaab..fa1e8e3c 100644 --- a/src/app/views/querying/polyalg/polyalg.component.html +++ b/src/app/views/querying/polyalg/polyalg.component.html @@ -89,11 +89,25 @@
    Help
    - The PolyPlan Builder can be used to create or edit plans using either +

    The PolyPlan Builder can be used to create or edit plans using either the Algebra Editor or the Node Editor.

    +

    The color around an editor indicates its state:

      -
    • the Algebra Editor
    • -
    • or the Node Editor.
    • +
    • + Yellow + indicates the content was edited and not yet synchronized with the other editor. +
    • +
    • + Red + indicates that the the plan is invalid. You can hover over the editor title to display the + error message. +
    +

    A plan can only be executed if both editors are synchronized and valid.

    +

    You can choose between Simple and Advanced mode. Simple mode disables the Algebra Editor and + hides advanced nodes. Some nodes also have a simplified variant. All properties can be unlocked by clicking on the + Simple + badge. +

    @@ -109,7 +123,7 @@
    Execute Plan
    Please specify the dynamic parameters: - + {{param[0]}}:{{param[1]}} @@ -139,10 +153,11 @@
    Execute Plan
    +
    - + diff --git a/src/app/views/querying/polyalg/polyalg.component.ts b/src/app/views/querying/polyalg/polyalg.component.ts index 6eb654ba..b7f544ec 100644 --- a/src/app/views/querying/polyalg/polyalg.component.ts +++ b/src/app/views/querying/polyalg/polyalg.component.ts @@ -103,10 +103,7 @@ export class PolyalgComponent implements OnInit, OnDestroy { let values: (string | boolean)[] = params.map(p => p[1] === 'BOOLEAN' ? false : ''); if (this.physicalExecForm.values != null && JSON.stringify(this.physicalExecForm.params) === JSON.stringify(params)) { - console.log('equal params!'); values = this.physicalExecForm.values; // keep existing values - } else { - console.log('not equal params: ', this.physicalExecForm.params, params); } this.physicalExecForm = { polyAlg: polyAlg, @@ -271,6 +268,10 @@ export class PolyalgComponent implements OnInit, OnDestroy { toggleParamsModal() { this.showParamsModal.update(b => !b); } + + clearParamsModal() { + this.physicalExecForm.values = this.physicalExecForm.params.map(p => p[1] === 'BOOLEAN' ? false : ''); + } } // https://stackoverflow.com/a/42820432 From bacd2243b9a8fa0bc2bc3d24380950a04f2959fa Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Thu, 27 Jun 2024 13:53:08 +0200 Subject: [PATCH 46/54] Add sample plans for all types --- .../polyalg-viewer/alg-viewer.component.html | 4 ++++ .../polyalg-viewer/alg-viewer.component.ts | 13 +++++++++-- .../querying/algebra/node/node.component.ts | 9 ++++---- .../querying/polyalg/polyalg.component.html | 6 ++++- .../querying/polyalg/polyalg.component.ts | 22 +++++++++++-------- 5 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html index e475ff9c..28e58852 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html @@ -69,6 +69,10 @@ (click)="synchronizeEditors()"> Synchronize +
    diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts index fb26b1e3..0e0ff491 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts @@ -57,6 +57,7 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { showNodeEditor = computed(() => this._registry.registryLoaded()); private isNodeFocused = false; // If a node is focused we must assume that a control has changed. Thus, the nodeEditor cannot be 'SYNCHRONIZED'. showMetadata = false; + skipOldTransformReuse = false; // when updating the entire PolyAlg, we want to reset the node editor transform polyAlgSnapshot: string; // keep track whether the textEditor has changed initialPolyAlg: string; // only used for initially setting the text representation @@ -84,11 +85,12 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { if (this.showNodeEditor() && this.polyAlgPlan() !== undefined && this.planTypeSignal() !== undefined && el) { untracked(() => { this.modifySubscription?.unsubscribe(); - const oldTransform = this.nodeEditor ? this.nodeEditor.getTransform() : null; + const oldTransform = (this.nodeEditor && !this.skipOldTransformReuse) ? this.nodeEditor.getTransform() : null; this.nodeEditor?.destroy(); createEditor(el, this.injector, _registry, this.polyAlgPlan(), this.planTypeSignal(), this.isReadOnly, this.userMode, oldTransform) .then(editor => { this.nodeEditor = editor; + this.skipOldTransformReuse = false; this.generateTextFromNodeEditor(); this.modifySubscription = this.nodeEditor.onModify.pipe( switchMap(() => { @@ -124,6 +126,7 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { if (this.polyAlg == null) { return; } + this.skipOldTransformReuse = true; this._validator.buildPlan(this.polyAlg, this.planType).subscribe({ next: (plan) => this.polyAlgPlan.set(plan), error: (err) => { @@ -244,7 +247,9 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { return; } this.isNodeFocused = false; - this.generateTextFromNodeEditor(true); + + // Wait a short amount of time for ng-autocomplete to update the value when selecting a suggestion + setTimeout(() => this.generateTextFromNodeEditor(true), 100); } onNodeEditorFocus() { @@ -341,4 +346,8 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { // if all nodes are already in the state !this.showMetadata, then the returned boolean is inverted this.showMetadata = this.nodeEditor.showMetadata(!this.showMetadata); } + + clearPlan() { + this.generateNodesFromTextEditor(''); + } } diff --git a/src/app/views/querying/algebra/node/node.component.ts b/src/app/views/querying/algebra/node/node.component.ts index a9e9bb98..c78f404e 100644 --- a/src/app/views/querying/algebra/node/node.component.ts +++ b/src/app/views/querying/algebra/node/node.component.ts @@ -9,15 +9,19 @@ import {CdkDragDrop, moveItemInArray} from '@angular/cdk/drag-drop'; styleUrls: ['./node.component.scss'] }) export class NodeComponent implements OnInit, AfterViewChecked { - isView = false; constructor() { } + isView = false; + @ViewChild('element', {read: ElementRef}) public element: ElementRef; @Input() node: Node; @Output() autocompleteChanged = new EventEmitter(); + + protected readonly AlgType = AlgType; + ngOnInit() { } @@ -83,7 +87,4 @@ export class NodeComponent implements OnInit, AfterViewChecked { trackFields(index: number, obj: any): any { return obj.length; } - - - protected readonly AlgType = AlgType; } diff --git a/src/app/views/querying/polyalg/polyalg.component.html b/src/app/views/querying/polyalg/polyalg.component.html index fa1e8e3c..b4b30020 100644 --- a/src/app/views/querying/polyalg/polyalg.component.html +++ b/src/app/views/querying/polyalg/polyalg.component.html @@ -92,9 +92,13 @@
    Help

    The PolyPlan Builder can be used to create or edit plans using either the Algebra Editor or the Node Editor.

    The color around an editor indicates its state:

    - - - - -
    {{prop.key}}{{prop.value}}
    -
    * calculated value
    -
    - -
    - -

    query

    -
    -
    -
    - - -
      -
    • - -
    • -
    - -
  • - diff --git a/src/app/explain-visualizer/components/plan-node/plan-node.component.scss b/src/app/explain-visualizer/components/plan-node/plan-node.component.scss deleted file mode 100644 index 64cc2faf..00000000 --- a/src/app/explain-visualizer/components/plan-node/plan-node.component.scss +++ /dev/null @@ -1,269 +0,0 @@ -@import url(../../assets/css/styles.css); - -ul { - display: flex; - justify-content: center; - padding: 12px 0 0 0; - position: relative; - transition: all 0.5s; - margin: -5px auto auto; -} - -ul ul::before { - content: ''; - position: absolute; - top: 0; - left: 50%; - border-left: 2px solid #c4c4c4; - height: 12px; - width: 0 -} - -/* css fix when using different recursion */ -ul li:last-child:not(:first-child) ul { - margin-left: -4px; - - li:only-child ul { - margin-left: 0 !important; - } -} - -ul li { - float: left; - text-align: center; - list-style-type: none; - position: relative; - padding: 12px 3px 0 3px; - transition: all 0.5s -} - -ul li:before, -ul li:after { - content: ''; - position: absolute; - top: 0; - right: 50%; - border-top: 2px solid #c4c4c4; - width: 50%; - height: 12px -} - -ul li:after { - right: auto; - left: 50%; - border-left: 2px solid #c4c4c4 -} - -ul li:only-child { - padding-top: 0 -} - -ul li:only-child:after, -ul li:only-child:before { - display: none -} - -ul li:first-child::before, -ul li:last-child::after { - border: 0 none -} - -ul li:last-child::before { - border-right: 2px solid #c4c4c4; - border-radius: 0 6px 0 0 -} - -ul li:first-child::after { - border-radius: 6px 0 0 0 -} - -ul li .plan-node:hover + ul::before { - border-color: #00B5E2 -} - -ul li .plan-node:hover + ul li::after, -ul li .plan-node:hover + ul li::before, -ul li .plan-node:hover + ul ul::before { - border-color: #008CAF -} - -/* - PLAN NODE -*/ -.plan-node { - text-decoration: none; - color: #4d525a; - display: inline-block; - transition: all 0.1s; - position: relative; - padding: 6px 10px; - background-color: #fff; - font-size: 12px; - border: 1px solid #dedede; - margin-bottom: 4px; - border-radius: 3px; - overflow-wrap: break-word; - word-wrap: break-word; - word-break: break-all; - width: 240px; - box-shadow: 1px 1px 3px 0 rgba(0, 0, 0, 0.1) -} - -.plan-node header { - margin-bottom: 6px; - cursor: pointer; -} - -.plan-node header:hover { - background-color: #f7f7f7 -} - -.plan-node header h4 { - font-size: 13px; - width: 100%; - text-align: left; - font-weight: 600; - margin: 0; -} - -.plan-node header .node-duration { - float: right; - margin-left: 10px; - font-size: 13px -} - -.plan-node .prop-list { - float: left; - text-align: left; - overflow-wrap: break-word; - word-wrap: break-word; - word-break: break-all; - margin-top: 10px; - margin-bottom: 6px -} - -.plan-node .relation-name { - text-align: left; - margin-bottom: 0.3em; -} - -.plan-node .planner-estimate { - border-top: 1px solid #dedede; - text-align: left; - padding-top: 3px; - margin-top: 6px; - width: 100% -} - -.plan-node .tags { - margin-top: 6px; - text-align: left -} - -.plan-node .tags span { - display: inline-block; - background-color: #FB4418; - color: #fff; - font-size: 10px; - font-weight: 600; - margin-right: 3px; - margin-bottom: 3px; - padding: 3px; - border-radius: 3px; - line-height: 1.1 -} - -.plan-node:hover { - border-color: #00B5E2 -} - -.plan-node .node-description { - text-align: left; - font-style: italic; - padding-top: 10px; - word-break: normal -} - -.plan-node .node-description .node-type { - font-weight: 600; - background-color: #00B5E2; - color: #fff; - padding: 0 6px -} - -.plan-node .btn-default { - border: 0 -} - -.node-bar-container { - height: 5px; - margin-top: 10px; - margin-bottom: 3px; - border-radius: 6px; - background-color: #dedede; - position: relative -} - -.node-bar-container .node-bar { - border-radius: 6px; - height: 100%; - text-align: left; - position: absolute; - left: 0; - top: 0 -} - -.node-bar-label { - text-align: left; - display: block -} - -.expanded { - width: 400px !important; - overflow: visible !important; - padding: 6px 10px !important -} - -.expanded .tags span { - margin-right: 3px !important -} - -.compact { - width: 140px -} - -.dot { - width: 30px; - overflow: hidden; - padding: 3px -} - -.dot .tags span { - margin-right: 1px -} - -.dot .node-bar-container { - margin-top: 3px -} - - -.table { - width: 100% -} - -.table td { - border-bottom: 1px solid #dedede; - padding: 6px -} - -.table tr:hover { - background-color: #f7f7f7 -} - -.model-mark { - font-style: normal; - font-weight: bold; - position: absolute; - right: 5px; - top: 0; -} diff --git a/src/app/explain-visualizer/components/plan-node/plan-node.component.ts b/src/app/explain-visualizer/components/plan-node/plan-node.component.ts deleted file mode 100644 index 60dd4eab..00000000 --- a/src/app/explain-visualizer/components/plan-node/plan-node.component.ts +++ /dev/null @@ -1,258 +0,0 @@ -import {Component, DoCheck, Input, OnInit} from '@angular/core'; -import {IPlan} from '../../models/iplan'; -import {EstimateDirection, HighlightType, ViewMode} from '../../models/enums'; -import * as _ from 'lodash'; -import {PlanService} from '../../services/plan.service'; -import {SyntaxHighlightService} from '../../services/syntax-highlight.service'; -import {HelpService} from '../../services/help.service'; -import {ColorService} from '../../services/color.service'; - -@Component({ - selector: 'app-plan-node', - templateUrl: './plan-node.component.html', - styleUrls: ['./plan-node.component.scss'], -}) -export class PlanNodeComponent implements OnInit, DoCheck { - - // consts - NORMAL_WIDTH = 220; - COMPACT_WIDTH = 140; - DOT_WIDTH = 30; - EXPANDED_WIDTH = 400; - - MIN_ESTIMATE_MISS = 100; - COSTLY_TAG = 'costliest'; - MOST_CPU = 'most cpu'; - LARGE_TAG = 'largest'; - ESTIMATE_TAG = 'bad estimate'; - - // inputs - @Input() plan: IPlan; - @Input() node: any; - @Input() viewOptions: any; - - // UI flags - showDetails: boolean; - - // calculated properties - executionTimePercent: number; - backgroundColor: string; - highlightValue: number; - barContainerWidth: number; - barWidth: number; - props: Array; - tags: Array; - plannerRowEstimateValue: number; - plannerRowEstimateDirection: EstimateDirection; - - // required for custom change detection - currentHighlightType: string; - currentCompactView: boolean; - currentExpandedView: boolean; - - // expose enum to view - estimateDirections = EstimateDirection; - highlightTypes = HighlightType; - viewModes = ViewMode; - - showQuery = false; // todo check - - constructor(private _planService: PlanService, - private _syntaxHighlightService: SyntaxHighlightService, - private _helpService: HelpService, - private _colorService: ColorService) { - } - - ngOnInit() { - this.currentHighlightType = this.viewOptions.highlightType; - this.calculateBar(); - this.calculateProps(); - this.calculateDuration(); - this.calculateTags(); - - this.plannerRowEstimateDirection = this.node[this._planService.PLANNER_ESTIMATE_DIRECTION]; - this.plannerRowEstimateValue = _.round(this.node[this._planService.PLANNER_ESTIMATE_FACTOR], 1); - } - - ngDoCheck() { - if (this.currentHighlightType !== this.viewOptions.highlightType) { - this.currentHighlightType = this.viewOptions.highlightType; - this.calculateBar(); - } - - if (this.currentCompactView !== this.viewOptions.showCompactView) { - this.currentCompactView = this.viewOptions.showCompactView; - this.calculateBar(); - } - - if (this.currentExpandedView !== this.showDetails) { - this.currentExpandedView = this.showDetails; - this.calculateBar(); - } - } - - getFormattedQuery() { - const keyItems: Array = []; - - // relation name will be highlighted for SCAN nodes - const relationName: string = this.node[this._planService.RELATION_NAME_PROP]; - if (relationName) { - keyItems.push(this.node[this._planService.SCHEMA_PROP] + '.' + relationName); - keyItems.push(' ' + relationName); - keyItems.push(' ' + this.node[this._planService.ALIAS_PROP] + ' '); - } - - // group key will be highlighted for AGGREGATE nodes - const groupKey: Array = this.node[this._planService.GROUP_KEY_PROP]; - if (groupKey) { - keyItems.push('GROUP BY ' + groupKey.join(',')); - } - - // hash condition will be highlighted for HASH JOIN nodes - const hashCondition: string = this.node[this._planService.HASH_CONDITION_PROP]; - if (hashCondition) { - keyItems.push(hashCondition.replace('(', '').replace(')', '')); - } - - if (this.node[this._planService.NODE_TYPE_PROP].toUpperCase() === 'LIMIT') { - keyItems.push('LIMIT'); - } - return this._syntaxHighlightService.highlight(this.plan.query, keyItems); - } - - calculateBar() { - switch (this.viewOptions.viewMode) { - case ViewMode.DOT: - this.barContainerWidth = this.DOT_WIDTH; - break; - case ViewMode.COMPACT: - this.barContainerWidth = this.COMPACT_WIDTH; - break; - default: - this.barContainerWidth = this.NORMAL_WIDTH; - break; - } - - // expanded view width trumps others - if (this.currentExpandedView) { - this.barContainerWidth = this.EXPANDED_WIDTH; - } - - - switch (this.currentHighlightType) { - case HighlightType.CPU: - this.highlightValue = (this.node[this._planService.ACTUAL_CPU_PROP]); - if (this.plan.planStats.maxCpu === 0) { - this.barWidth = 0; - } else { - // this.barWidth = Math.round((this.highlightValue / this.plan.planStats.maxDuration) * this.barContainerWidth); - this.barWidth = Math.round((this.highlightValue / this.plan.planStats.maxCpu) * 100); - } - break; - case HighlightType.ROWS: - this.highlightValue = (this.node[this._planService.ACTUAL_ROWS_PROP]); - if (this.plan.planStats.maxRows === 0) { - this.barWidth = 0; - } else { - // this.barWidth = Math.round((this.highlightValue / this.plan.planStats.maxRows) * this.barContainerWidth); - this.barWidth = Math.round((this.highlightValue / this.plan.planStats.maxRows) * 100); - } - break; - case HighlightType.COST: - this.highlightValue = (this.node[this._planService.ACTUAL_COST_PROP]); - if (this.plan.planStats.maxCost === 0) { - this.barWidth = 0; - } else { - // this.barWidth = Math.round((this.highlightValue / this.plan.planStats.maxCost) * this.barContainerWidth); - this.barWidth = Math.round((this.highlightValue / this.plan.planStats.maxCost) * 100); - } - break; - } - - if (this.barWidth < 0) { - this.barWidth = 0; - } - - this.backgroundColor = this._colorService.numberToColorHsl(1 - this.barWidth / this.barContainerWidth); - } - - calculateDuration() { - this.executionTimePercent = (_.round((this.node[this._planService.ACTUAL_DURATION_PROP] / this.plan.planStats.executionTime) * 100)); - } - - // create an array of node propeties so that they can be displayed in the view - calculateProps() { - this.props = _.chain(this.node) - .omit(this._planService.PLANS_PROP) - .map((value, key) => { - return {key: key, value: value}; - }) - .value(); - } - - calculateTags() { - this.tags = []; - if (this.node[this._planService.MOST_CPU_NODE_PROP]) { - this.tags.push(this.MOST_CPU); - } - if (this.node[this._planService.COSTLIEST_NODE_PROP]) { - this.tags.push(this.COSTLY_TAG); - } - if (this.node[this._planService.LARGEST_NODE_PROP]) { - this.tags.push(this.LARGE_TAG); - } - if (this.node[this._planService.PLANNER_ESTIMATE_FACTOR] >= this.MIN_ESTIMATE_MISS) { - this.tags.push(this.ESTIMATE_TAG); - } - } - - getNodeTypeDescription() { - return this._helpService.getNodeTypeDescription(this.node[this._planService.NODE_TYPE_PROP]); - } - - getNodeName() { - if (this.viewOptions.viewMode === ViewMode.DOT && !this.showDetails) { - // return this.node[this._planService.NODE_TYPE_PROP].replace(/[^A-Z]/g, '').toUpperCase(); - return this.node[this._planService.NODE_TYPE_PROP].replace(/[^A-Z]/g, ''); - } - - // return (this.node[this._planService.NODE_TYPE_PROP]).toUpperCase(); - return (this.node[this._planService.NODE_TYPE_PROP]); - } - - getTagName(tagName: String) { - if (this.viewOptions.viewMode === ViewMode.DOT && !this.showDetails) { - return tagName.charAt(0); - } - return tagName; - } - - shouldShowPlannerEstimate() { - if (this.viewOptions.showPlannerEstimate && this.showDetails) { - return true; - } - - if (this.viewOptions.viewMode === ViewMode.DOT) { - return false; - } - - return this.viewOptions.showPlannerEstimate; - } - - shouldShowNodeBarLabel() { - if (this.showDetails) { - return true; - } - - if (this.viewOptions.viewMode === ViewMode.DOT) { - return false; - } - - return true; - } - - - getDataModelShort() { - return this.node[this._planService.MODEL].toUpperCase().substring(0, 1); - } -} diff --git a/src/app/explain-visualizer/components/plan-view/plan-view.component.html b/src/app/explain-visualizer/components/plan-view/plan-view.component.html deleted file mode 100644 index 21198ab2..00000000 --- a/src/app/explain-visualizer/components/plan-view/plan-view.component.html +++ /dev/null @@ -1,88 +0,0 @@ - - -
    - -
    -
    - {{plan.planStats.maxCpu| number:'.0-2'}} - most cpu -
    -
    - {{plan.planStats.executionTime | duration}} - execution time ({{plan.planStats.executionTime | durationUnit}}) -
    -
    - {{plan.planStats.planningTime | number:'.0-2'}} - planning time (ms) -
    -
    - {{plan.planStats.maxDuration | duration}} - slowest node ({{plan.planStats.maxDuration | durationUnit}}) -
    -
    - {{plan.planStats.maxRows | number:'.0-2'}} - largest node (rows) -
    -
    - {{plan.planStats.maxCost | number:'.0-2'}} - costliest node -
    -
    -
    - -
    - -
    -
    diff --git a/src/app/explain-visualizer/components/plan-view/plan-view.component.scss b/src/app/explain-visualizer/components/plan-view/plan-view.component.scss deleted file mode 100644 index 0c87f0f9..00000000 --- a/src/app/explain-visualizer/components/plan-view/plan-view.component.scss +++ /dev/null @@ -1,248 +0,0 @@ -@import url(../../assets/css/styles.css); - -/* - MENU -*/ -.menu { - width: 210px; - height: 250px; - position: absolute; - font-size: 12px; - top: 115px; - left: 0; - background-color: #787878; - box-shadow: 1px 1px 2px 1px rgba(0, 0, 0, 0.2); - color: #fff; - border-top-right-radius: 3px; - border-bottom-right-radius: 3px; - z-index: 1 -} - -.menu header h3 { - padding: 10px 20px 0 0; - margin: 0 0 6px 3em; - font-size: 16px; - font-weight: 600; - line-height: 2; - text-align: left; -} - -.menu ul { - margin: 0 0 0 10px; - list-style: none; - padding: 0; -} - -.menu ul li { - line-height: 1.5; -} - -.menu-toggle { - font-size: 22px; - float: left; - padding-left: 6px; - line-height: 2; - cursor: pointer -} - -.menu-hidden { - width: 45px; - height: 45px; - border-top-right-radius: 50%; - border-bottom-right-radius: 50% -} - -.menu-hidden ul, -.menu-hidden h3 { - visibility: hidden -} - -.menu .button-group { - display: flex; - margin-bottom: 6px -} - -/* - PAGE -*/ -.page, -.page-stretch { - padding-top: 10px; - margin: auto; - width: 1000px; - min-height: 600px -} - -.page em, -.page-stretch em { - font-style: italic -} - -.page-content h2 { - font-size: 26px; - line-height: 2 -} - -.page-content h3 { - font-size: 22px; - margin-top: 18px; - line-height: 2 -} - -.page-content p, -.page-content ul { - font-size: 16px; - line-height: 1.5; - margin-bottom: 18px -} - -.page-content ul { - list-style-type: disc; - position: relative; - left: 18px -} - -.page-stretch { - margin: auto; - width: 95% -} - -.page-stretch h2 { - text-align: left; - font-size: 17px; - max-width: 1000px; - margin: auto; - margin-bottom: 20px -} - -.page-stretch h2 .sub-title { - font-size: 13px; - font-style: italic -} - -/* - BUTTON GROUP -*/ -.button-group button { - margin: 0; - background-color: #f7f7f7; - border-radius: 0; - float: left; - border: 1px solid #dedede; - cursor: pointer -} - -.button-group button:first-of-type { - border-top-left-radius: 6px; - border-bottom-left-radius: 6px -} - -.button-group button:last-of-type { - border-top-right-radius: 6px; - border-bottom-right-radius: 6px -} - -.button-group .selected { - background-color: #dedede -} - -/* - PLAN STATS -*/ -.plan-stats { - display: flex; - font-size: 13px; - margin: 0; - padding-bottom: 10px; - /*border-bottom: 1px solid #dedede;*/ - border-radius: 12px; - width: 400px; - position: relative; -} - -.plan-stats div { - padding-right: 10px; - flex-grow: 1 -} - -.plan-stats .stat-value { - display: block; - text-align: center; - font-size: 17px -} - -.plan-stats .stat-label { - display: block; - text-align: center; - font-size: 12px -} - -/*.plan-stats:after { - content: ''; - position: absolute; - top: 100%; - left: 50%; - margin-left: -9px; - width: 0; - height: 0; - border-top: solid 9px #dedede; - border-left: solid 9px transparent; - border-right: solid 9px transparent -}*/ - -.plan-stats .btn-close { - padding: 6px; - background-color: transparent; - font-size: 17px; - text-align: center; - margin-left: 6px; - cursor: pointer; - border: 0 -} - -/* - INPUTS -*/ -.input-box:-moz-placeholder { - color: #ababab; - font-size: 18px -} - -.input-box::-moz-placeholder { - color: #ababab; - font-size: 18px -} - -.input-box:-ms-input-placeholder { - color: #ababab; - font-size: 18px -} - -.input-box::-webkit-input-placeholder { - color: #ababab; - font-size: 18px -} - -.input-box:focus { - box-shadow: 0 0 5px #51cbee -} - -.input-box-main { - font-size: 18px; - width: 700px; - border: 0; - border-bottom: 2px solid #00B5E2; - padding: 10px; - margin-top: 10px; - margin-bottom: 10px -} - -.input-box-lg { - width: 98%; - height: 210px; - margin-bottom: 6px; - margin-bottom: 10px; - border-radius: 3px; - border: 1px solid #dedede; - padding: 10px -} diff --git a/src/app/explain-visualizer/components/plan-view/plan-view.component.ts b/src/app/explain-visualizer/components/plan-view/plan-view.component.ts deleted file mode 100644 index 71c65465..00000000 --- a/src/app/explain-visualizer/components/plan-view/plan-view.component.ts +++ /dev/null @@ -1,64 +0,0 @@ -import {Component, Input, OnInit} from '@angular/core'; -import {IPlan} from '../../models/iplan'; -import {HighlightType, ViewMode} from '../../models/enums'; -import {PlanService} from '../../services/plan.service'; - -@Component({ - selector: 'app-plan-view', - templateUrl: './plan-view.component.html', - styleUrls: ['./plan-view.component.scss'], -}) -export class PlanViewComponent implements OnInit { - - id: string; - plan: IPlan; - rootContainer: any; - hideMenu = true; - @Input() query; - @Input() planObject; - - viewOptions: any = { - showPlanStats: true, - showHighlightBar: true, - showPlannerEstimate: false, - showTags: true, - highlightType: HighlightType.NONE, - viewMode: ViewMode.FULL - }; - - showPlannerEstimate = true; - - highlightTypes = HighlightType; // exposing the enum to the view - viewModes = ViewMode; - - constructor(private _planService: PlanService) { - - } - - initPlan() { - this.plan = this._planService.createPlan('name', JSON.parse(this.planObject), this.query); - - this.rootContainer = this.plan.content; - this.plan.planStats = { - executionTime: this.rootContainer['Execution Time'] || this.rootContainer['Total Runtime'], - planningTime: this.rootContainer['Planning Time'] || 0, - maxRows: this.rootContainer[this._planService.MAXIMUM_ROWS_PROP] || 0, - maxCost: this.rootContainer[this._planService.MAXIMUM_COSTS_PROP] || 0, - maxDuration: this.rootContainer[this._planService.MAXIMUM_DURATION_PROP] || 0, - maxCpu: this.rootContainer[this._planService.MAXIMUM_CPU_PROP] || 0 - }; - } - - ngOnInit() { - this.initPlan(); - } - - toggleHighlight(type: HighlightType) { - this.viewOptions.highlightType = type; - } - - analyzePlan() { - this._planService.analyzePlan(this.plan); - } - -} diff --git a/src/app/explain-visualizer/explain-visualizer.module.ts b/src/app/explain-visualizer/explain-visualizer.module.ts deleted file mode 100644 index 09212d10..00000000 --- a/src/app/explain-visualizer/explain-visualizer.module.ts +++ /dev/null @@ -1,41 +0,0 @@ -import {NgModule} from '@angular/core'; -import {CommonModule} from '@angular/common'; -import {DurationPipe, DurationUnitPipe, MomentDatePipe} from './pipes'; -import {PlanNodeComponent} from './components/plan-node/plan-node.component'; -import {PlanViewComponent} from './components/plan-view/plan-view.component'; -import {PlanService} from './services/plan.service'; -import {SyntaxHighlightService} from './services/syntax-highlight.service'; -import {HelpService} from './services/help.service'; -import {ColorService} from './services/color.service'; -import {FormsModule} from '@angular/forms'; -import {ButtonCloseDirective} from '@coreui/angular'; - -@NgModule({ - declarations: [ - PlanNodeComponent, - PlanViewComponent, - MomentDatePipe, - DurationPipe, - DurationUnitPipe - ], - imports: [ - CommonModule, - FormsModule, - ButtonCloseDirective - ], - providers: [ - PlanService, - SyntaxHighlightService, - HelpService, - ColorService - ], - exports: [ - PlanNodeComponent, - MomentDatePipe, - DurationPipe, - DurationUnitPipe, - PlanViewComponent - ] -}) -export class ExplainVisualizerModule { -} diff --git a/src/app/explain-visualizer/models/enums.ts b/src/app/explain-visualizer/models/enums.ts deleted file mode 100644 index 85d9ddd9..00000000 --- a/src/app/explain-visualizer/models/enums.ts +++ /dev/null @@ -1,18 +0,0 @@ -export class HighlightType { - static NONE = 'none'; - static CPU = 'cpu cost'; - static ROWS = 'row cost'; - static COST = 'io cost'; -} - -export enum EstimateDirection { - over, - under, - equal -} - -export class ViewMode { - static FULL = 'full'; - static COMPACT = 'compact'; - static DOT = 'dot'; -} diff --git a/src/app/explain-visualizer/models/iplan.ts b/src/app/explain-visualizer/models/iplan.ts deleted file mode 100644 index a17e0b23..00000000 --- a/src/app/explain-visualizer/models/iplan.ts +++ /dev/null @@ -1,17 +0,0 @@ -export class IPlan { - id: string; - name: string; - content: any; - query: string; - createdOn: Date; - planStats: any; - formattedQuery: string; - - constructor(id: string, name: string, content: any, query: string, createdOn: Date) { - this.id = id; - this.name = name; - this.content = content; - this.query = query; - this.createdOn = createdOn; - } -} diff --git a/src/app/explain-visualizer/pipes.ts b/src/app/explain-visualizer/pipes.ts deleted file mode 100644 index b37f30f9..00000000 --- a/src/app/explain-visualizer/pipes.ts +++ /dev/null @@ -1,46 +0,0 @@ -import {Pipe, PipeTransform} from '@angular/core'; -import * as _ from 'lodash'; -import * as moment from 'moment'; - -@Pipe({name: 'momentDate'}) -export class MomentDatePipe implements PipeTransform { - transform(value: string, args: string[]): any { - return moment(value).format('LLL'); - } -} - -@Pipe({name: 'duration'}) -export class DurationPipe implements PipeTransform { - transform(value: number): string { - let duration = ''; - - if (value < 1) { - duration = '<1'; - } else if (value > 1 && value < 1000) { - duration = _.round(value, 2).toString(); - } else if (value >= 1000 && value < 60000) { - duration = _.round(value / 1000, 2).toString(); - } else if (value >= 60000) { - duration = _.round(value / 60000, 2).toString(); - } - return duration; - } -} - -@Pipe({name: 'durationUnit'}) -export class DurationUnitPipe implements PipeTransform { - transform(value: number) { - let unit = ''; - - if (value < 1) { - unit = 'ms'; - } else if (value > 1 && value < 1000) { - unit = 'ms'; - } else if (value >= 1000 && value < 60000) { - unit = 's'; - } else if (value >= 60000) { - unit = 'min'; - } - return unit; - } -} diff --git a/src/app/explain-visualizer/services/color.service.ts b/src/app/explain-visualizer/services/color.service.ts deleted file mode 100644 index fba28340..00000000 --- a/src/app/explain-visualizer/services/color.service.ts +++ /dev/null @@ -1,65 +0,0 @@ -import {Injectable} from '@angular/core'; - -@Injectable({ - providedIn: 'root' -}) -export class ColorService { - /** - * http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion - * - * Converts an HSL color value to RGB. Conversion formula - * adapted from http://en.wikipedia.org/wiki/HSL_color_space. - * Assumes h, s, and l are contained in the set [0, 1] and - * returns r, g, and b in the set [0, 255]. - * - * @param Number h The hue - * @param Number s The saturation - * @param Number l The lightness - * @return Array The RGB representation - */ - hslToRgb(h, s, l) { - let r, g, b; - - if (s === 0) { - r = g = b = l; // achromatic - } else { - const q = l < 0.5 ? l * (1 + s) : l + s - l * s; - const p = 2 * l - q; - r = this.hue2rgb(p, q, h + 1 / 3); - g = this.hue2rgb(p, q, h); - b = this.hue2rgb(p, q, h - 1 / 3); - } - - return [Math.floor(r * 255), Math.floor(g * 255), Math.floor(b * 255)]; - } - - hue2rgb(p, q, t) { - if (t < 0) { - t += 1; - } - if (t > 1) { - t -= 1; - } - if (t < 1 / 6) { - return p + (q - p) * 6 * t; - } - if (t < 1 / 2) { - return q; - } - if (t < 2 / 3) { - return p + (q - p) * (2 / 3 - t) * 6; - } - return p; - } - - // convert a number to a color using hsl - numberToColorHsl(i) { - // as the function expects a value between 0 and 1, and red = 0° and green = 120° - // we convert the input to the appropriate hue value - const hue = i * 100 * 1.2 / 360; - // we convert hsl to rgb (saturation 100%, lightness 50%) - const rgb = this.hslToRgb(hue, .9, .4); - // we format to css value and return - return 'rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ')'; - } -} diff --git a/src/app/explain-visualizer/services/help.service.ts b/src/app/explain-visualizer/services/help.service.ts deleted file mode 100644 index aab4507d..00000000 --- a/src/app/explain-visualizer/services/help.service.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {Injectable} from '@angular/core'; - -@Injectable({ - providedIn: 'root' -}) -export class HelpService { - getNodeTypeDescription(nodeType: string) { - return NODE_DESCRIPTIONS[nodeType.toUpperCase()]; - } -} - -export const NODE_DESCRIPTIONS = { - 'LIMIT': 'returns a specified number of rows from a record set.', - 'SORT': 'sorts a record set based on the specified sort key.', - 'NESTED LOOP': `merges two record sets by looping through every record in the first set and - trying to find a match in the second set. All matching records are returned.`, - 'MERGE JOIN': `merges two record sets by first sorting them on a join key.`, - 'HASH': `generates a hash table from the records in the input recordset. Hash is used by - Hash Join.`, - 'HASH JOIN': `joins to record sets by hashing one of them (using a Hash Scan).`, - 'AGGREGATE': `groups records together based on a GROUP BY or aggregate function (like sum()).`, - 'HASHAGGREGATE': `groups records together based on a GROUP BY or aggregate function (like sum()). Hash Aggregate uses - a hash to first organize the records by a key.`, - 'SEQ SCAN': `finds relevant records by sequentially scanning the input record set. When reading from a table, - Seq Scans (unlike Index Scans) perform a single read operation (only the table is read).`, - 'INDEX SCAN': `finds relevant records based on an Index. Index Scans perform 2 read operations: one to - read the index and another to read the actual value from the table.`, - 'INDEX ONLY SCAN': `finds relevant records based on an Index. Index Only Scans perform a single read operation - from the index and do not read from the corresponding table.`, - 'BITMAP HEAP SCAN': 'searches through the pages returned by the Bitmap Index Scan for relevant rows.', - 'BITMAP INDEX SCAN': `uses a Bitmap Index (index which uses 1 bit per page) to find all relevant pages. - Results of this node are fed to the Bitmap Heap Scan.`, - 'CTE SCAN': `performs a sequential scan of Common Table Expression (CTE) query results. Note that - results of a CTE are materialized (calculated and temporarily stored).` - -}; diff --git a/src/app/explain-visualizer/services/plan.service.ts b/src/app/explain-visualizer/services/plan.service.ts deleted file mode 100644 index 245f043a..00000000 --- a/src/app/explain-visualizer/services/plan.service.ts +++ /dev/null @@ -1,227 +0,0 @@ -import {Injectable} from '@angular/core'; -import {IPlan} from '../models/iplan'; -import * as moment from 'moment'; -import * as _ from 'lodash'; -import {EstimateDirection} from '../models/enums'; - -@Injectable({ - providedIn: 'root' -}) -export class PlanService { - // Polpyheny properties - EXPRESSIONS = 'exprs'; - AGGREGATIONS = 'aggs'; - FIELDS = 'fields'; - CONDITION = 'condition'; - TRANSFORMATION = 'transformation'; - TABLE = 'table'; - CPU_COST = 'cpu cost'; - ROW_COUNT = 'rowcount'; - MODEL = 'model'; - - // plan property keys - NODE_TYPE_PROP = 'relOp'; - ACTUAL_ROWS_PROP = 'rows cost'; - PLAN_ROWS_PROP = 'Plan Rows'; - ACTUAL_TOTAL_TIME_PROP = 'Actual Total Time'; - ACTUAL_LOOPS_PROP = 'Actual Loops'; - TOTAL_COST_PROP = 'io cost'; - PLANS_PROP = 'inputs'; - RELATION_NAME_PROP = 'Relation Name'; - SCHEMA_PROP = 'Schema'; - ALIAS_PROP = 'Alias'; - GROUP_KEY_PROP = 'group'; - SORT_KEY_PROP = 'Sort Key'; - JOIN_TYPE_PROP = 'joinType'; - INDEX_NAME_PROP = 'Index Name'; - HASH_CONDITION_PROP = 'Hash Cond'; - - // computed by pev - COMPUTED_TAGS_PROP = '*Tags'; - - COSTLIEST_NODE_PROP = '*Costiest Node (by cost)'; - LARGEST_NODE_PROP = '*Largest Node (by rows)'; - SLOWEST_NODE_PROP = '*Slowest Node (by duration)'; - MOST_CPU_NODE_PROP = '*Most Cpu Prop'; - - MAXIMUM_COSTS_PROP = '*Most Expensive Node (cost)'; - MAXIMUM_ROWS_PROP = '*Largest Node (rows)'; - MAXIMUM_DURATION_PROP = '*Slowest Node (time)'; - MAXIMUM_CPU_PROP = '*Most Cpu Node'; - ACTUAL_DURATION_PROP = '*Actual Duration'; - ACTUAL_COST_PROP = '*Actual Cost'; - ACTUAL_CPU_PROP = 'Actual Cpu'; - PLANNER_ESTIMATE_FACTOR = '*Planner Row Estimate Factor'; - PLANNER_ESTIMATE_DIRECTION = '*Planner Row Estimate Direction'; - - CTE_SCAN_PROP = 'CTE Scan'; - CTE_NAME_PROP = 'CTE Name'; - - ARRAY_INDEX_KEY = 'arrayIndex'; - - PEV_PLAN_TAG = 'plan_'; - - private _maxRows = 0; - private _maxCost = 0; - private _maxDuration = 0; - private _maxCpu = 0; - - getPlans(): IPlan[] { - const plans: IPlan[] = []; - - for (const i in localStorage) { - if (_.startsWith(i, this.PEV_PLAN_TAG)) { - plans.push(JSON.parse(localStorage[i])); - } - } - - return _.chain(plans) - .sortBy('createdOn') - .reverse() - .value(); - } - - getPlan(id: string): IPlan { - return JSON.parse(localStorage.getItem(id)); - } - - createPlan(planName: string, planContent: any, planQuery): IPlan { - const plan: IPlan = { - id: this.PEV_PLAN_TAG + new Date().getTime().toString(), - name: planName || 'plan created on ' + moment().format('LLL'), - createdOn: new Date(), - content: planContent, - query: planQuery, - planStats: planContent, - formattedQuery: planQuery - }; - this.analyzePlan(plan); - return plan; - } - - isJsonString(str) { - try { - JSON.parse(str); - } catch (e) { - return false; - } - return true; - } - - analyzePlan(plan: IPlan) { - this.processNode(plan.content.Plan); - plan.content[this.MAXIMUM_ROWS_PROP] = this._maxRows; - plan.content[this.MAXIMUM_COSTS_PROP] = this._maxCost; - plan.content[this.MAXIMUM_DURATION_PROP] = this._maxDuration; - plan.content[this.MAXIMUM_CPU_PROP] = this._maxCpu; - - this.findOutlierNodes(plan.content.Plan); - - } - - deletePlan(plan: IPlan) { - localStorage.removeItem(plan.id); - } - - deleteAllPlans() { - localStorage.clear(); - } - - // recursively walk down the plan to compute various metrics - processNode(node) { - this.calculatePlannerEstimate(node); - this.calculateActuals(node); - - _.each(node, (value, key) => { - this.calculateMaximums(node, key, value); - - if (key === this.PLANS_PROP) { - _.each(value, (val) => { - this.processNode(val); - }); - } - }); - } - - calculateMaximums(node, key, value) { - if (key === this.ACTUAL_ROWS_PROP && this._maxRows < value) { - this._maxRows = value; - } - if (key === this.ACTUAL_COST_PROP && this._maxCost < value) { - this._maxCost = value; - } - if (key === this.ACTUAL_DURATION_PROP && this._maxDuration < value) { - this._maxDuration = value; - } - if (key === this.ACTUAL_CPU_PROP && this._maxCpu < value) { - this._maxCpu = value; - } - } - - findOutlierNodes(node) { - node[this.SLOWEST_NODE_PROP] = false; - node[this.LARGEST_NODE_PROP] = false; - node[this.COSTLIEST_NODE_PROP] = false; - - if (node[this.ACTUAL_COST_PROP] === this._maxCost && this._maxCost > 0) { - node[this.COSTLIEST_NODE_PROP] = true; - } - if (node[this.ACTUAL_ROWS_PROP] === this._maxRows && this._maxRows > 0) { - node[this.LARGEST_NODE_PROP] = true; - } - if (node[this.ACTUAL_DURATION_PROP] === this._maxDuration && this._maxDuration > 0) { - node[this.SLOWEST_NODE_PROP] = true; - } - if (node[this.ACTUAL_CPU_PROP] === this._maxCpu && this._maxCpu > 0) { - node[this.MOST_CPU_NODE_PROP] = true; - } - - _.each(node, (value, key) => { - if (key === this.PLANS_PROP) { - _.each(value, (val) => { - this.findOutlierNodes(val); - }); - } - }); - } - - // actual duration and actual cost are calculated by subtracting child values from the total - calculateActuals(node) { - node[this.ACTUAL_DURATION_PROP] = node[this.ACTUAL_TOTAL_TIME_PROP]; - node[this.ACTUAL_COST_PROP] = node[this.TOTAL_COST_PROP]; - node[this.ACTUAL_CPU_PROP] = node[this.CPU_COST]; - - // console.log (node); - _.each(node.Plans, subPlan => { - // console.log('processing chldren', subPlan); - // since CTE scan duration is already included in its subnodes, it should be be - // subtracted from the duration of this node - if (subPlan[this.NODE_TYPE_PROP] !== this.CTE_SCAN_PROP) { - node[this.ACTUAL_DURATION_PROP] = node[this.ACTUAL_DURATION_PROP] - subPlan[this.ACTUAL_TOTAL_TIME_PROP]; - node[this.ACTUAL_COST_PROP] = node[this.ACTUAL_COST_PROP] - subPlan[this.TOTAL_COST_PROP]; - } - }); - - if (node[this.ACTUAL_COST_PROP] < 0) { - node[this.ACTUAL_COST_PROP] = 0; - } - - // since time is reported for an invidual loop, actual duration must be adjusted by number of loops - // node[this.ACTUAL_DURATION_PROP] = node[this.ACTUAL_DURATION_PROP] * node[this.ACTUAL_LOOPS_PROP]; - } - - // figure out order of magnitude by which the planner mis-estimated how many rows would be - // invloved in this node - calculatePlannerEstimate(node) { - node[this.PLANNER_ESTIMATE_FACTOR] = node[this.ROW_COUNT] / node[this.ACTUAL_ROWS_PROP]; - node[this.PLANNER_ESTIMATE_DIRECTION] = EstimateDirection.under; - if (node[this.ROW_COUNT] === node[this.ACTUAL_ROWS_PROP]) { - node[this.PLANNER_ESTIMATE_DIRECTION] = EstimateDirection.equal; - } - - if (node[this.PLANNER_ESTIMATE_FACTOR] < 1) { - node[this.PLANNER_ESTIMATE_DIRECTION] = EstimateDirection.over; - node[this.PLANNER_ESTIMATE_FACTOR] = node[this.ACTUAL_ROWS_PROP] / node[this.ROW_COUNT]; - } - } -} diff --git a/src/app/explain-visualizer/services/syntax-highlight.service.ts b/src/app/explain-visualizer/services/syntax-highlight.service.ts deleted file mode 100644 index 73d8911a..00000000 --- a/src/app/explain-visualizer/services/syntax-highlight.service.ts +++ /dev/null @@ -1,191 +0,0 @@ -import {Injectable} from '@angular/core'; -import * as _ from 'lodash'; -import hljs from 'highlight.js'; - -@Injectable({ - providedIn: 'root' -}) -export class SyntaxHighlightService { - OPEN_TAG = ' _OPEN_TAG_'; - CLOSE_TAG = '_CLOSE_TAG_'; - - highlight(code: string, keyItems: Array) { - hljs.registerLanguage('sql', LANG_SQL); - /*hljs.configure({ - tabReplace: ' ' - });*/ - - // prior to syntax highlighting, we want to tag key items in the raw code. making the - // query upper case and ensuring that all comma separated values have a space - // makes it simpler to find the items we're looing for - let result: string = code.toUpperCase().replace(', ', ','); - _.each(keyItems, (keyItem: string) => { - result = result.replace(keyItem.toUpperCase(), `${this.OPEN_TAG}${keyItem}${this.CLOSE_TAG}`); - }); - - result = hljs.highlightAuto(result).value; - result = result.replace(new RegExp(this.OPEN_TAG, 'g'), ``); - result = result.replace(new RegExp(this.CLOSE_TAG, 'g'), ''); - - return result; - } -} - -export const LANG_SQL = function (hljs) { - const COMMENT_MODE = hljs.COMMENT('--', '$'); - return { - case_insensitive: true, - illegal: /[<>{}*]/, - contains: [ - { - beginKeywords: - 'begin end start commit rollback savepoint lock alter create drop rename call ' + - 'delete do handler insert load replace select truncate update set show pragma grant ' + - 'merge describe use explain help declare prepare execute deallocate release ' + - 'unlock purge reset change stop analyze cache flush optimize repair kill ' + - 'install uninstall checksum restore check backup revoke', - end: /;/, endsWithParent: true, - keywords: { - keyword: - 'abort abs absolute acc acce accep accept access accessed accessible account acos action activate add ' + - 'addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias ' + - 'allocate allow alter always analyze ancillary and any anydata anydataset anyschema anytype apply ' + - 'archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan ' + - 'atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid ' + - 'authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile ' + - 'before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float ' + - 'binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound ' + - 'buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel ' + - 'capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base ' + - 'char_length character_length characters characterset charindex charset charsetform charsetid check ' + - 'checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close ' + - 'cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation ' + - 'collect colu colum column column_value columns columns_updated comment commit compact compatibility ' + - 'compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn ' + - 'connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection ' + - 'consider consistent constant constraint constraints constructor container content contents context ' + - 'contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost ' + - 'count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation ' + - 'critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user ' + - 'cursor curtime customdatum cycle d data database databases datafile datafiles datalength date_add ' + - 'date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts ' + - 'day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate ' + - 'declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults ' + - 'deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank ' + - 'depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor ' + - 'deterministic diagnostics difference dimension direct_load directory disable disable_all ' + - 'disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div ' + - 'do document domain dotnet double downgrade drop dumpfile duplicate duration e each edition editionable ' + - 'editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt ' + - 'end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors ' + - 'escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding ' + - 'execu execut execute exempt exists exit exp expire explain export export_set extended extent external ' + - 'external_1 external_2 externally extract f failed failed_login_attempts failover failure far fast ' + - 'feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final ' + - 'finish first first_value fixed flash_cache flashback floor flush following follows for forall force ' + - 'form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ' + - 'ftp full function g general generated get get_format get_lock getdate getutcdate global global_name ' + - 'globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups ' + - 'gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex ' + - 'hierarchy high high_priority hosts hour http i id ident_current ident_incr ident_seed identified ' + - 'identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment ' + - 'index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile ' + - 'initial initialized initially initrans inmemory inner innodb input insert install instance instantiable ' + - 'instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat ' + - 'is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists ' + - 'k keep keep_duplicates key keys kill l language large last last_day last_insert_id last_value lax lcase ' + - 'lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit ' + - 'lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate ' + - 'locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call ' + - 'logoff logon logs long loop low low_priority lower lpad lrtrim ltrim m main make_set makedate maketime ' + - 'managed management manual map mapping mask master master_pos_wait match matched materialized max ' + - 'maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans ' + - 'md5 measures median medium member memcompress memory merge microsecond mid migration min minextents ' + - 'minimum mining minus minute minvalue missing mod mode model modification modify module monitoring month ' + - 'months mount move movement multiset mutex n name name_const names nan national native natural nav nchar ' + - 'nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile ' + - 'nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile ' + - 'nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder ' + - 'nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck ' + - 'noswitch not nothing notice notrim novalidate now nowait nth_value nullif nulls num numb numbe ' + - 'nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ' + - 'ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old ' + - 'on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date ' + - 'oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary ' + - 'out outer outfile outline output over overflow overriding p package pad parallel parallel_enable ' + - 'parameters parent parse partial partition partitions pascal passing password password_grace_time ' + - 'password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex ' + - 'pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc ' + - 'performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin ' + - 'policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction ' + - 'prediction_cost prediction_details prediction_probability prediction_set prepare present preserve ' + - 'prior priority private private_sga privileges procedural procedure procedure_analyze processlist ' + - 'profiles project prompt protection public publishingservername purge quarter query quick quiesce quota ' + - 'quotename radians raise rand range rank raw read reads readsize rebuild record records ' + - 'recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh ' + - 'regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy ' + - 'reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename ' + - 'repair repeat replace replicate replication required reset resetlogs resize resource respect restore ' + - 'restricted result result_cache resumable resume retention return returning returns reuse reverse revoke ' + - 'right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows ' + - 'rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll ' + - 'sdo_georaster sdo_topo_geometry search sec_to_time second section securefile security seed segment select ' + - 'self sequence sequential serializable server servererror session session_user sessions_per_user set ' + - 'sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor ' + - 'si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin ' + - 'size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex ' + - 'source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows ' + - 'sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone ' + - 'standby start starting startup statement static statistics stats_binomial_test stats_crosstab ' + - 'stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep ' + - 'stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev ' + - 'stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate ' + - 'subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum ' + - 'suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate ' + - 'sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime t table tables tablespace tan tdo ' + - 'template temporary terminated tertiary_weights test than then thread through tier ties time time_format ' + - 'time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr ' + - 'timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking ' + - 'transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate ' + - 'try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress ' + - 'under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unpivot ' + - 'unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert ' + - 'url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date ' + - 'utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var ' + - 'var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray ' + - 'verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear ' + - 'wellformed when whene whenev wheneve whenever where while whitespace with within without work wrapped ' + - 'xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces ' + - 'xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek', - literal: - 'true false null', - built_in: - 'array bigint binary bit blob boolean char character date dec decimal float int int8 integer interval number ' + - 'numeric real record serial serial8 smallint text varchar varying void' - }, - contains: [ - { - className: 'string', - begin: '\'', end: '\'', - contains: [hljs.BACKSLASH_ESCAPE, {begin: '\'\''}] - }, - { - className: 'string', - begin: '"', end: '"', - contains: [hljs.BACKSLASH_ESCAPE, {begin: '""'}] - }, - { - className: 'string', - begin: '`', end: '`', - contains: [hljs.BACKSLASH_ESCAPE] - }, - hljs.C_NUMBER_MODE, - hljs.C_BLOCK_COMMENT_MODE, - COMMENT_MODE - ] - }, - hljs.C_BLOCK_COMMENT_MODE, - COMMENT_MODE - ] - }; -}; diff --git a/src/app/models/information-page.model.ts b/src/app/models/information-page.model.ts index c183c354..66e560aa 100644 --- a/src/app/models/information-page.model.ts +++ b/src/app/models/information-page.model.ts @@ -41,8 +41,6 @@ export interface InformationObject extends Duration { labels?: string[]; colors?: string[]; graphType?: string; - //debugger - queryPlan: string; //PolyAlg plans jsonPolyAlg: string; planType: PlanType; diff --git a/src/scss/vendors/_variables.scss b/src/scss/vendors/_variables.scss index 3330a601..3d9adeeb 100644 --- a/src/scss/vendors/_variables.scss +++ b/src/scss/vendors/_variables.scss @@ -2,14 +2,3 @@ @import "../variables"; @import "~bootstrap/scss/mixins"; @import "../node_modules/@coreui/coreui/scss/variables"; - -@font-face { - font-family: 'FontAwesome'; - src: url("~app/explain-visualizer/assets/fonts/fontawesome-webfont.eot?v=4.5.0"); - src: url("~app/explain-visualizer/assets/fonts/fontawesome-webfont.eot?#iefix&v=4.5.0") format("embedded-opentype"), - url("~app/explain-visualizer/assets/fonts/fontawesome-webfont.woff2?v=4.5.0") format("woff2"), - url("~app/explain-visualizer/assets/fonts/fontawesome-webfont.woff?v=4.5.0") format("woff"), - url("~app/explain-visualizer/assets/fonts/fontawesome-webfont.ttf?v=4.5.0") format("truetype"); - font-weight: normal; - font-style: normal -} From 3e4b16ffebdd8977ee03e21025cc8176bd48add9 Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Fri, 12 Jul 2024 12:44:33 +0200 Subject: [PATCH 51/54] Remove Alg Builder --- .../left-sidebar/left-sidebar.component.ts | 19 +- .../default-layout.component.html | 9 +- src/app/models/ui-request.model.ts | 30 - src/app/services/crud.service.ts | 29 +- .../querying/algebra/algebra.component.html | 108 --- .../querying/algebra/algebra.component.scss | 169 ---- .../querying/algebra/algebra.component.ts | 788 ------------------ .../views/querying/algebra/algebra.model.ts | 182 ---- .../querying/algebra/node/node.component.html | 243 ------ .../querying/algebra/node/node.component.scss | 43 - .../querying/algebra/node/node.component.ts | 90 -- .../views/querying/querying.component.html | 1 - src/app/views/views.module.ts | 4 - 13 files changed, 11 insertions(+), 1704 deletions(-) delete mode 100644 src/app/views/querying/algebra/algebra.component.html delete mode 100644 src/app/views/querying/algebra/algebra.component.scss delete mode 100644 src/app/views/querying/algebra/algebra.component.ts delete mode 100644 src/app/views/querying/algebra/algebra.model.ts delete mode 100644 src/app/views/querying/algebra/node/node.component.html delete mode 100644 src/app/views/querying/algebra/node/node.component.scss delete mode 100644 src/app/views/querying/algebra/node/node.component.ts diff --git a/src/app/components/left-sidebar/left-sidebar.component.ts b/src/app/components/left-sidebar/left-sidebar.component.ts index 7a5e3cd7..adf41f47 100644 --- a/src/app/components/left-sidebar/left-sidebar.component.ts +++ b/src/app/components/left-sidebar/left-sidebar.component.ts @@ -16,12 +16,6 @@ import {CatalogState} from '../../models/catalog.model'; //docs: https://angular2-tree.readme.io/docs/ export class LeftSidebarComponent implements OnInit, AfterViewInit { - private readonly _router = inject(Router); - public readonly _sidebar = inject(LeftSidebarService); - public readonly _catalog = inject(CatalogService); - - public readonly sidebarAvailable: Signal; - constructor() { this.router = this._router; //this.nodes = nodes; @@ -65,13 +59,18 @@ export class LeftSidebarComponent implements OnInit, AfterViewInit { ); this.sidebarAvailable = computed(() => { - return this.buttons.length > 0 || this.error || this.nodes.length > 0 - }) + return this.buttons.length > 0 || this.error || this.nodes.length > 0; + }); } static readonly EXPAND_SHOWN_ROUTES: String[] = [ - '/views/monitoring', '/views/config', '/views/uml', '/views/querying/console', - '/views/querying/algebra', '/views/notebooks']; + '/views/monitoring', '/views/config', '/views/uml', '/views/querying/console', '/views/notebooks']; + + private readonly _router = inject(Router); + public readonly _sidebar = inject(LeftSidebarService); + public readonly _catalog = inject(CatalogService); + + public readonly sidebarAvailable: Signal; @ViewChild('tree', {static: false}) treeComponent: TreeComponent; nodes = []; diff --git a/src/app/containers/default-layout/default-layout.component.html b/src/app/containers/default-layout/default-layout.component.html index 7a15d607..68064b21 100644 --- a/src/app/containers/default-layout/default-layout.component.html +++ b/src/app/containers/default-layout/default-layout.component.html @@ -51,18 +51,11 @@ Graphical Querying
    -
  • - - - Plan Builder - -
  • - PolyPlan Builder + Plan Builder
  • diff --git a/src/app/models/ui-request.model.ts b/src/app/models/ui-request.model.ts index de6eceb6..654e3603 100644 --- a/src/app/models/ui-request.model.ts +++ b/src/app/models/ui-request.model.ts @@ -1,6 +1,5 @@ import {SortState} from '../components/data-view/models/sort-state.model'; import {TableConstraint, UiColumnDefinition} from '../components/data-view/models/result-set.model'; -import {Node} from '../views/querying/algebra/algebra.model'; import {EntityType} from './catalog.model'; import {PlanType} from './information-page.model'; @@ -34,35 +33,6 @@ export class EntityRequest extends UIRequest { } } -export class RelAlgRequest extends UIRequest { - type = 'RelAlgRequest'; - topNode: Node; - createView: boolean; - analyze: boolean; - tableType; - string; - viewName: string; - store: string; - freshness: string; - interval; - timeUnit: string; - useCache: boolean; - - constructor(node: Node, cache: boolean, analyzeQuery: boolean, createView?: boolean, tableType?: string, viewName?: string, store?: string, freshness?: string, interval?, timeUnit?: string) { - super(); - this.topNode = node; - this.useCache = cache; - this.analyze = analyzeQuery; - this.createView = createView || false; - this.tableType = tableType || 'table'; - this.viewName = viewName || 'viewName'; - this.store = store || null; - this.freshness = freshness || null; - this.interval = interval || null; - this.timeUnit = timeUnit || null; - } -} - export class PolyAlgRequest extends UIRequest { type = 'PolyAlgRequest'; polyAlg: string; diff --git a/src/app/services/crud.service.ts b/src/app/services/crud.service.ts index 75bd5e65..3d7c4a56 100644 --- a/src/app/services/crud.service.ts +++ b/src/app/services/crud.service.ts @@ -3,16 +3,14 @@ import {HttpClient, HttpHeaders} from '@angular/common/http'; import {WebuiSettingsService} from './webui-settings.service'; import {EntityMeta, IndexModel, ModifyPartitionRequest, PartitionFunctionModel, PartitioningRequest, PathAccessRequest, PlacementFieldsModel} from '../components/data-view/models/result-set.model'; import {webSocket} from 'rxjs/webSocket'; -import {ColumnRequest, ConstraintRequest, DataModel, DeleteRequest, EditCollectionRequest, EditTableRequest, EntityRequest, ExploreTable, GraphRequest, MaterializedRequest, Method, MonitoringRequest, Namespace, PolyAlgRequest, QueryRequest, RelAlgRequest, StatisticRequest} from '../models/ui-request.model'; +import {ColumnRequest, ConstraintRequest, DataModel, DeleteRequest, EditCollectionRequest, EditTableRequest, EntityRequest, ExploreTable, GraphRequest, MaterializedRequest, Method, MonitoringRequest, Namespace, PolyAlgRequest, QueryRequest, StatisticRequest} from '../models/ui-request.model'; import {AutoDockerResult, AutoDockerStatus, CreateDockerResponse, DockerInstanceInfo, DockerSettings, HandshakeInfo, InstancesAndAutoDocker, UpdateDockerResponse} from '../models/docker.model'; import {ForeignKey, Uml} from '../views/uml/uml.model'; import {Validators} from '@angular/forms'; import {AdapterModel} from '../views/adapters/adapter.model'; import {QueryInterface} from '../views/query-interfaces/query-interfaces.model'; -import {AlgNodeModel, Node} from '../views/querying/algebra/algebra.model'; import {WebSocket} from './webSocket'; import {Observable} from 'rxjs'; -import {map} from 'rxjs/operators'; import {PolyAlgRegistry} from '../components/polyalg/models/polyalg-registry'; import {PlanNode} from '../components/polyalg/models/polyalg-plan.model'; import {PlanType} from '../models/information-page.model'; @@ -438,24 +436,6 @@ export class CrudService { return this._http.post(`${this.httpUrl}/createForeignKey`, fk, this.httpOptions); } - /** - * Execute an algebra expression - */ - executeAlg(socket: WebSocket, relAlg: Node, cache: boolean, analyzeQuery, createView?: boolean, tableType?: string, viewName?: string, store?: string, freshness?: string, interval?: string, timeUnit?: string) { - let request; - if (createView) { - if (tableType === 'MATERIALIZED') { - request = new RelAlgRequest(relAlg, cache, analyzeQuery, createView, 'materialized', viewName, store, freshness, interval, timeUnit); - } else { - request = new RelAlgRequest(relAlg, cache, analyzeQuery, createView, 'view', viewName); - } - } else { - request = new RelAlgRequest(relAlg, cache, analyzeQuery); - console.log(request); - } - return socket.sendMessage(request); - } - renameTable(meta: EntityMeta) { return this._http.post(`${this.httpUrl}/renameTable`, meta, this.httpOptions); @@ -540,13 +520,6 @@ export class CrudService { return this._http.post(`${this.httpUrl}/updateQueryInterfaceSettings`, request, this.httpOptions); } - getAlgebraNodes() { - return this._http.get(`${this.httpUrl}/getAlgebraNodes`) - .pipe(map(algs => new Map(Object.entries(algs) - .sort() - .map(([k, v], i) => [k, v as AlgNodeModel[]])))); - } - removeQueryInterface(queryInterfaceId: string) { return this._http.post(`${this.httpUrl}/removeQueryInterface`, queryInterfaceId, this.httpOptions); } diff --git a/src/app/views/querying/algebra/algebra.component.html b/src/app/views/querying/algebra/algebra.component.html deleted file mode 100644 index 1e49970c..00000000 --- a/src/app/views/querying/algebra/algebra.component.html +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - -
    - - - - -
    - -
    - - -
    -
    - - - - - - {{ n.value.class }} - -  ( - - - ) - - - - - - - - - - - - - - - -
    -
    -
    -
    -
    - - - - - {{ $result().query }} - - ! - - - - -
    - Error: -

    {{ $result().error }}

    -
    - -
    -

    - Successfully executed -

    -
    - - - - - - -
    - -
    -
    \ No newline at end of file diff --git a/src/app/views/querying/algebra/algebra.component.scss b/src/app/views/querying/algebra/algebra.component.scss deleted file mode 100644 index 2c7cdd6c..00000000 --- a/src/app/views/querying/algebra/algebra.component.scss +++ /dev/null @@ -1,169 +0,0 @@ -#operatorList .rel-op:hover { - background: rgba(0, 0, 0, 0.05); -} - -.rel-op { - cursor: grab; -} - -#drop { - height: 1000px; -} - -#drop .node, .cdk-drag-preview .node { - position: absolute; - min-width: 260px; - z-index: 1; - left: 0; - top: 0; - - .in, .out { - position: absolute; - left: 50%; - margin-left: -10px; - width: 20px; - height: 20px; - border-radius: 100%; - z-index: 1; - } - - .in { - bottom: -10px; - background: rgba(181, 229, 160, 0.3); - } - - .out { - top: -10px; - background: rgba(150, 216, 228, 0.3); - } - - .drag-handle { - cursor: move; - } - - .del { - margin-left: 1em; - color: #c8ced3; - } - - .del:hover { - color: #dc3545; - cursor: pointer; - } - - .form-group { - input { - margin-left: 1em; - } - } - - .list-group-item { - padding: 0.5rem 1.25rem; - } - - .param-wrapper { - display: flex; - justify-content: space-between; - - label { - margin-bottom: 0; - margin-right: 1em; - line-height: 2em; - } - } - - .param-input { - width: 100%; - } - - input[type=checkbox].param-input { - box-shadow: none; - } -} - -#drop:not(.connecting) .out:hover { - background: rgba(150, 216, 228, 0.7); - cursor: pointer; -} - -#drop.connecting .in:hover { - background: rgba(181, 229, 160, 0.7); -} - -#drop .cdk-drag-placeholder { - display: none; -} - -svg { - position: absolute; - top: 0; - left: 0; - stroke: black; - stroke-width: 1; - z-index: 0; - pointer-events: none; -} - -.r-0 { - right: 2rem; -} - -.t-0 { - top: 1rem; -} - -#result-wrapper { - padding-bottom: 1em; - padding-right: 0; -} - - -/* autocomplete package */ -.ng-autocomplete { - width: 100% !important; -} - -.autocomplete-container { - box-shadow: none !important; - border: 1px solid #e4e7ea; - border-radius: 0.25rem; - line-height: 2; - height: auto !important; -} - -.input-container { - height: initial; - line-height: initial; - - input { - background-color: transparent !important; - height: auto !important; - line-height: 1 !important; - padding: 0.25rem 0.5rem !important; - } -} - -.autocomplete-container .suggestions-container ul li a { - padding: 0 0.5em !important; -} - -.input-container input::placeholder { - font-size: 0.765625rem; -} - -/* bootstrap styles */ -.autocomplete-container { - transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; -} - -.autocomplete-container:focus-within { - border: 1px #8ad4ee solid !important; - border-radius: 0.25rem; - outline: 0 !important; - box-shadow: 0 0 0 0.2rem rgba(32, 168, 216, 0.25) !important; -} - -.analyze-query { - margin-top: 0.25rem; - font-size: 0.765625rem; -} diff --git a/src/app/views/querying/algebra/algebra.component.ts b/src/app/views/querying/algebra/algebra.component.ts deleted file mode 100644 index e9a2d593..00000000 --- a/src/app/views/querying/algebra/algebra.component.ts +++ /dev/null @@ -1,788 +0,0 @@ -import { - AfterViewInit, - Component, - computed, - effect, - ElementRef, - HostBinding, - inject, - OnDestroy, - OnInit, - Signal, - signal, - untracked, - ViewChild, - ViewEncapsulation, - WritableSignal -} from '@angular/core'; -import {AlgNodeModel, AlgType, Connection, Node} from './algebra.model'; -import {RelationalResult, Result} from '../../../components/data-view/models/result-set.model'; -import {CrudService} from '../../../services/crud.service'; -import {ToasterService} from '../../../components/toast-exposer/toaster.service'; -import * as $ from 'jquery'; -import 'jquery-ui/ui/widget'; -import 'jquery-ui/ui/widgets/draggable'; -import 'jquery-ui/ui/widgets/droppable'; -import {SvgLine} from '../../uml/uml.model'; -import {SidebarNode} from '../../../models/sidebar-node.model'; -import {LeftSidebarService} from '../../../components/left-sidebar/left-sidebar.service'; -import {InformationPage} from '../../../models/information-page.model'; -import {BreadcrumbItem} from '../../../components/breadcrumb/breadcrumb-item'; -import {BreadcrumbService} from '../../../components/breadcrumb/breadcrumb.service'; -import {WebuiSettingsService} from '../../../services/webui-settings.service'; -import {Subscription} from 'rxjs'; -import {WebSocket} from '../../../services/webSocket'; -import {UtilService} from '../../../services/util.service'; -import {ViewInformation} from '../../../components/data-view/data-view.component'; -import {CatalogService} from '../../../services/catalog.service'; -import {KeyValue} from '@angular/common'; - -@Component({ - selector: 'app-algebra', - templateUrl: './algebra.component.html', - styleUrls: ['./algebra.component.scss'], - encapsulation: ViewEncapsulation.None -}) -export class AlgebraComponent implements OnInit, AfterViewInit, OnDestroy { - - private readonly _crud = inject(CrudService); - private readonly _toast = inject(ToasterService); - private readonly _leftSidebar = inject(LeftSidebarService); - private readonly _breadcrumb = inject(BreadcrumbService); - private readonly _settings = inject(WebuiSettingsService); - private readonly _catalog = inject(CatalogService); - private readonly _util = inject(UtilService); - - @ViewChild('dropArea', {read: ElementRef}) dropArea: ElementRef; - @HostBinding('class.is-open') - public readonly $result: WritableSignal> = signal(null); - private counter = 0; - public connections = new Map(); - public nodes = new Map(); - public temporalLine: SvgLine; - operators = []; - autocomplete;// names of the schemas, tables and columns - sidebarNodes: SidebarNode[] = []; - private subscriptions = new Subscription(); - analyzerId: string; - showingAnalysis = false; - queryAnalysis: InformationPage; - webSocket: WebSocket; - - //temporal values while dragging - scrollTop: number; - scrollLeft: number; - draggingNodeX: number; - draggingNodeY: number; - socketOn: boolean; - - analyzeQuery = true; - private cache = true; - private $algModels: WritableSignal> = signal(new Map()); - private $algs: Signal>; - public $loading: WritableSignal = signal(false); - - constructor() { - this.socketOn = false; - this.webSocket = new WebSocket(); - this.initWebsocket(); - - this.$algs = computed(() => { - const map = new Map(); - for (let [k, v] of this.$algModels().entries()) { - for (let alg of v) { - map.set(alg.name, alg); - } - } - return map; - }); - - effect(() => { - const catalog = this._catalog.listener(); - - untracked(() => { - const autocomplete = {schemas: []}; - for (const schema of catalog.getSchemaTree('', true, 3)) { - autocomplete.schemas.push(schema.name); - autocomplete[schema.name] = {tables: []}; - for (const table of schema.children) { - autocomplete[schema.name].tables.push([table.name, table.tableType]); - autocomplete[schema.name][table.name] = {columns: []}; - for (const col of table.children) { - autocomplete[schema.name][table.name].columns.push(col.name); - } - } - } - this.autocomplete = autocomplete; - }); - }); - - effect(() => { - const nodes: SidebarNode[] = []; - for (const [k, v] of this.$algModels()?.entries()) { - nodes.push(new SidebarNode('operator_' + k, k, '', '').asSeparator()); - for (let alg of v) { - nodes.push(AlgebraComponent.toSidebarNode(alg)); - } - } - untracked(() => { - this.sidebarNodes = nodes; - this._leftSidebar.setNodes(nodes); - this._leftSidebar.open(); - }); - }); - } - - static toSidebarNode(nodeModel: AlgNodeModel): SidebarNode { - const node = new SidebarNode('operator_' + nodeModel.name, nodeModel.name, nodeModel.icon, null, true); - if (nodeModel.symbol) { - node.setAlgSymbol(nodeModel.symbol); - } - return node; - } - - - ngOnInit() { - this._leftSidebar.open(); - this.getOperators(); - - const sub2 = this.webSocket.reconnecting.subscribe( - b => { - if (b) { - this.getOperators(); - } - } - ); - this.subscriptions.add(sub2); - } - - ngAfterViewInit() { - this.initDraggable(); - //todo find solution without timeout - //setTimeout(() => this.initSidebar(), 200); - } - - ngOnDestroy() { - this.counter = 0; - $(document).off(); - $('#drop').off(); - this._leftSidebar.close(); - this.subscriptions.unsubscribe(); - this.webSocket.close(); - } - - initWebsocket() { - const sub = this.webSocket.onMessage().subscribe({ - next: msg => { - //if msg contains nodes of the sidebar - if (Array.isArray(msg)) { - const sidebarNodesTemp: SidebarNode[] = msg; - const backToPlanBuilder = new SidebarNode('back-to-plan-builder', 'plan-builder', 'fa fa-cubes').setAction((tree, node, $event) => { - this.showingAnalysis = false; - this._breadcrumb.hide(); - }); - const sidebarNodes: SidebarNode[] = [backToPlanBuilder]; - //set analyzerId to close it when leaving the page. - if (sidebarNodesTemp.length > 0) { - const split = sidebarNodesTemp[0].routerLink.split('/'); - this.analyzerId = split[0]; - for (const s of sidebarNodesTemp) { - const s2 = SidebarNode.fromJson(s, {allowRouting: false}); - sidebarNodes.push(s2.setAction((tree, node, $event) => { - //todo define behavior when clicking on analyzer-sidebarNode - const analyzerPage = s2.routerLink.split('/')[1]; - this._crud.getAnalyzerPage(this.analyzerId, analyzerPage).subscribe({ - next: res => { - this.queryAnalysis = res; - this.showingAnalysis = true; - this._breadcrumb.setBreadcrumbs([new BreadcrumbItem(node.data.name)]); - node.setIsActive(true); - }, error: err => { - console.log(err); - } - }); - })); - } - } - sidebarNodes.unshift(new SidebarNode('analyzer', 'analyzer').asSeparator()); - sidebarNodes.unshift(new SidebarNode('separator', ' ').asSeparator()); - this._leftSidebar.setNodes(this.sidebarNodes.concat(sidebarNodes)); - } - //a result - else { - $('#run i').removeClass().addClass('fa fa-play'); - this.$loading.set(false); - this.$result.set(msg); - } - }, error: err => { - setTimeout(() => { - this.initWebsocket(); - }, +this._settings.getSetting('reconnection.timeout')); - } - }); - this.subscriptions.add(sub); - } - - treeDrop(e) { - const id = 'node' + this.counter++; - const x = Math.max(0, Math.min(this.dropArea.nativeElement.offsetWidth - 270, e.event.offsetX)); - const y = Math.max(0, Math.min(this.dropArea.nativeElement.offsetHeight - 140, e.event.offsetY)); - const name = e.element.data.name; - const alg = this.$algs().get(name); - const node = new Node(id, e.element.data.name, x, y); - node.algSymbol = e.element.data.algSymbol; - node.icon = e.element.data.icon; - node.inputCount = alg.inputs; - node.type = alg.type; - node.class = alg.name; - - if (e.element.data.name.includes('Scan')) { - const ac = []; - const acType = []; - if (this.autocomplete) { - for (const v1 of this.autocomplete.schemas) { - for (const v2 of this.autocomplete[v1].tables) { - ac.push(v1 + '.' + v2[0]); - acType.push(v2[1]); - } - } - node.autocomplete = ac; - node.tableTypes = acType; - node.initialNames = ac; - } - } - this.nodes.set(id, node); - } - - /** - * initialize the functionality that nodes can be connected by drag and drop - */ - initDraggable() { - const self = this; - - let isDragging = false; - let source = ''; - $(document).on('mousedown', '#drop .node .out', e => { - isDragging = true; - $('#drop').addClass('connecting'); - const x = $(e.target).parents('.node').parent().position().left + $(e.target).parents('.node').outerWidth() / 2; - const y = $(e.target).parents('.node').parent().position().top; - self.temporalLine = {x1: x, x2: x, y1: y, y2: y}; - - source = $(e.target).parents('.node').attr('id'); - e.preventDefault(); - }).on('mousemove', e => { - if (isDragging) { - e.preventDefault(); - const dropContainer = $('svg#line'); - const x = e.pageX - dropContainer.offset().left; - const y = e.pageY - dropContainer.offset().top; - self.temporalLine.x2 = x; - self.temporalLine.y2 = y; - } - }).on('mouseup', e => { - if (!isDragging) { - return; - } - if ($(e.target).hasClass('in')) { - const target = $(e.target).parents('.node').attr('id'); - if (source !== target) {//don't allow to connect with own node - self.addConnection(source, target); - } - } - isDragging = false; - $('#drop').removeClass('connecting'); - self.temporalLine = null; - }); - - } - - /** - * Delete a node and all connections that belong to it - */ - deleteNode(node: Node) { - const id = node.id; - this.connections.forEach((v, k) => { - if (v.target.id === id || v.source.id === id) { - this.connections.delete(k); - } - }); - this.nodes.delete(id); - this.connections.delete(id); - } - - /** - * Delete every single node - */ - deleteAll() { - this.nodes = new Map(); - this.connections = new Map(); - } - - /** - * When two nodes are connected: - * if there was a connection: delete it - * if not: create a new connection - */ - addConnection(source, target) { - if (this.connections.has(source + target)) { - this.connections.delete(source + target); - } else { - this.connections.set(source + target, { - id: source + target, - source: this.nodes.get(source), - target: this.nodes.get(target) - }); - } - this.setAutocomplete(); - } - - /** - * List LogicalOperators for the select menu - */ - getOperators() { - //see https://stackoverflow.com/questions/43100718/typescript-enum-to-object-array - this._crud.getAlgebraNodes().subscribe(algs => { - this.$algModels.set(algs); - });//Object.keys(LogicalOperator).map(key => LogicalOperator[key]); - } - - - /** - * The topmost node and perform a bottomUp iteration to setup the autocomplete for all nodes - */ - setAutocomplete() { - if (this.autocomplete === undefined) { - return; - } - const tree = this.getTree(); - if (tree !== undefined) { - this.bottomUp(tree); - } - } - - /** - * Iterate the tree from bottom to top - */ - bottomUp(node: Node) { - for (const n of node.children) { - this.bottomUp(n); - } - return this.updateNodeAutocomplete(node); - } - - /** - * Set the autocomplete fields for each node - * default: get columns from all children - * Scan node: get columns from autocomplete object - * Project node: get columns from all children, but only save the ones that are being projected - */ - updateNodeAutocomplete(node: Node) { - const self = this; - - function getNode() { - return self.nodes.get(node.id); - } - - if (node.type === AlgType.Scan) { - if (node.entityName === undefined || !node.entityName.includes('\.')) { - getNode().acSchema.clear(); - getNode().acTable.clear(); - getNode().acSchema.add(node.entityName); - - } else { // node.tableName.includes('\.') - const tN = node.entityName.split('\.'); - getNode().acSchema.clear(); - getNode().acSchema.add(tN[0]); - getNode().acTable.clear(); - getNode().acTable.add(tN[1]); - const ac = []; - const cols = new Set(); - const tableCols = new Set(); - this.autocomplete.schemas - .filter(namespace => getNode().acSchema.has(namespace)) - .forEach((v1, i1) => { - this.autocomplete[v1].tables - .filter((v) => getNode().acTable.has(v[0])) - .forEach((v2, i2) => { - ac.push(v1 + '.' + v2[0]); - this.autocomplete[v1][v2[0]].columns.forEach((v3, i3) => { - cols.add(v3); - tableCols.add(v2[0] + '.' + v3); - }); - }); - }); - getNode().autocomplete = ac; - getNode().acColumns = cols; - getNode().acTableColumns = tableCols; - } - } else if (node.type === AlgType.Project) { - node = this.getFromChildren(node); - for (const col of getNode().acColumns) { - let contains = false; - for (const f of node.fields) { - if (f.split('\.')[1] === col) { - contains = true; - } - } - if (!contains) { - getNode().acColumns.delete(col); - } - } - const ac = []; - for (const tCol of getNode().acTableColumns) { - ac.push(tCol); - if (!node.fields.includes(tCol)) { - getNode().acTableColumns.delete(tCol); - } - } - getNode().autocomplete = ac; - } else { // all other nodes - node = this.getFromChildren(node); - } - return getNode(); - } - - /** - * iterate all children of a node and add all columns to its set - */ - getFromChildren(node: Node) { - const self = this; - - function getNode() { - return self.nodes.get(node.id); - } - - getNode().acSchema.clear(); - getNode().acTable.clear(); - getNode().acTableColumns.clear(); - getNode().acColumns.clear(); - for (const n of node.children) { - getNode().acSchema = new Set([...getNode().acSchema, ...n.acSchema]); - getNode().acTable = new Set([...getNode().acTable, ...n.acTable]); - getNode().acColumns = new Set([...getNode().acColumns, ...n.acColumns]); - getNode().acTableColumns = new Set([...getNode().acTableColumns, ...n.acTableColumns]); - } - getNode().autocomplete = [...getNode().acTableColumns, ...getNode().acColumns]; - return getNode(); - } - - // bottom node (start) - getX1(s: Node) { - if (s === undefined) { - return; - } - if (s.dragging && this.draggingNodeX !== undefined) { - return this.draggingNodeX + s.width / 2 + 50; - } - return s.$left() + s.width / 2; - } - - // bottom node (end) - getX2(t: Node) { - if (t === undefined) { - return; - } - if (t.dragging && this.draggingNodeX !== undefined) { - return this.draggingNodeX + t.width / 2; - } - return t.$left() + t.width / 2; - } - - getY1(s: Node) { - if (s === undefined) { - return; - } - if (s.dragging && this.draggingNodeY !== undefined) { - return this.draggingNodeY - 28; - } - return s.$top() - 16; - } - - getY2(t: Node) { - if (t === undefined) { - return; - } - if (t.dragging && this.draggingNodeY !== undefined) { - return this.draggingNodeY; - } - return t.$top() + t.height - 5; - } - - clickRun(event) { - this.cache = !(event.shiftKey && event.altKey); - this.runPlan(); - this.cache = true; - } - - /** - * Get the tree and perform a REST request to execute it - */ - runPlan() { - this.$loading.set(true); - $('#run i').removeClass().addClass('fa fa-hourglass-half'); - const tree = this.getTree(); - if (tree === undefined) { - $('#run i').removeClass().addClass('fa fa-play'); - this._toast.warn('Please provide a plan to be executed.', 'no plan'); - this.$loading.set(false); - return; - } - if (!this._crud.executeAlg(this.webSocket, tree, this.cache, this.analyzeQuery)) { - $('#run i').removeClass().addClass('fa fa-play'); - this.$result.set(new RelationalResult('Could not establish a connection with the server.')); - this.$loading.set(false); - } - } - - createView(info: ViewInformation) { - this._crud.executeAlg( - this.webSocket, - this.getTree(), - this.cache, - this.analyzeQuery, - true, - info.tableType, - info.newViewName, - info.stores, - info.freshness, - info.interval as unknown as string, - info.timeUnit); - } - - - /** - * Get the topmost node to be able to iterate the tree - * The topmost node is the one that has no outgoing connections - */ - getTopNode() { - let topNode: Node; - const haveOutgoingConnections = new Map(); - this.connections.forEach((v, k) => { - haveOutgoingConnections.set(v.source.id, v.source); - if (!haveOutgoingConnections.get(v.target.id)) { - topNode = v.target; - } - }); - return this.nodes.get(topNode.id); - } - - /** - * Get the whole tree by getting the topmost node and adding all children using the walkTree method - */ - getTree(): Node { - let tree; - if (this.connections.size === 0) { - if (this.nodes.size === 1) { - //get only node in Map - tree = this.walkTree(this.nodes.values().next().value.clone()); - } else { - //$('#run i').removeClass().addClass('fa fa-play'); - //this._toast.warn( 'Please provide a plan to be executed.', 'no plan' ); - return undefined; - } - } else { - tree = this.walkTree(this.getTopNode().clone()); - } - return tree; - } - - /** - * Add all children to a node recursively - */ - walkTree(node: Node): Node { - const children = []; - this.connections.forEach((v, k) => { - if (v.target.id === node.id) { - children.push(this.nodes.get(this.walkTree(v.source).id)); - // children.push( this.walkTree(v.source) ); - } - }); - node.children = children; - node.inputCount = children.length; - return node; - } - - - /** - * Set temporal values when dragging a node - */ - dragStart(e, node: Node) { - const $bg = $('#wrapper'); - this.scrollTop = $bg.scrollTop(); - this.scrollLeft = $bg.scrollLeft(); - this.nodes.get(node.id).dragging = true; - } - - /** - * Set temporal values when dragging a node - */ - draggingNode(e, node: Node) { - const $bg = $('#wrapper'); - - this.draggingNodeX = node.$left() + e.distance.x + $bg.scrollLeft() - this.scrollLeft; - this.draggingNodeY = node.$top() + e.distance.y + $bg.scrollTop() - this.scrollTop; - } - - /** - * Save the position of a node when it was moved - */ - savePos(e, node: Node) { - const $bg = $('#drop'); - this.nodes.get(node.id).dragging = false; - const nodeElement = $('#' + node.id); - const nodeWidth = nodeElement.width(); - const nodeHeight = nodeElement.height(); - const scrollTopDistance = $bg.scrollTop() - this.scrollTop; - const scrollLeftDistance = $bg.scrollLeft() - this.scrollLeft; - console.log(node.$left() + e.distance.x + scrollLeftDistance); - node.$left.set(Math.max(0, Math.min(node.$left() + e.distance.x + scrollLeftDistance, this.dropArea.nativeElement.offsetWidth - nodeWidth - 4))); - node.$top.set(Math.max(0, Math.min(node.$top() + e.distance.y + scrollTopDistance, this.dropArea.nativeElement.offsetHeight - nodeHeight - 4))); - this.draggingNodeX = null; - this.draggingNodeY = null; - } - - trackNode(index: number, e: KeyValue) { - if (e.value.id) { - return e.value.id; - } - return 1; - } - - /** - * Export the tree as JSON and save it to the clipboard - */ - exportTree() { - if (this.nodes.size === 0) { - return; - } - // see https://2ality.com/2015/08/es6-map-json.html - const out = {nodes: [...this.nodes], connections: [...this.connections]}; - this._util.clipboard(JSON.stringify(out)); - this._toast.success('The plan was exported to JSON and copied to your clipboard', null, 'exported'); - } - - /** - * Import a tree in the JSON format - */ - importTree() { - const input = prompt('Please paste your plan here.'); - if (input === null || input === '') { - return; - } - const inputObj = JSON.parse(input); - if (inputObj.nodes) { - const importedNodes = new Map(); - for (const [k, v] of Object.entries(inputObj.nodes)) { - importedNodes.set(v[0], Node.fromJson(v[1], this.dropArea.nativeElement.offsetWidth, this.dropArea.nativeElement.offsetHeight)); - } - this.nodes = importedNodes; - this.counter = importedNodes.size; - } - if (inputObj.connections) { - const importedConnections = new Map(); - for (const conn of Object.values(inputObj.connections)) { - importedConnections.set(conn[0], { - id: conn[1].id, - source: this.nodes.get(conn[1].source.id), - target: this.nodes.get(conn[1].target.id) - }); - } - this.connections = importedConnections; - } - this.setAutocomplete(); - } - - /** - * Calculates tree height of a balanced tree - */ - treeHeight() { - return Math.floor(Math.log2(this.counter)); - } - - /** - * Formats all nodes in the working field - */ - formatNodesTree() { - const height = this.treeHeight(); - let leftPadding = 0; - let upperPadding = 0; - let ind = 0; - for (let i = 0; i <= height; i++) { - upperPadding = i * 250; - for (let j = 0; j <= Math.pow(2, i) - 1; j++) { - leftPadding = 300 * j; - this.nodes.get('node' + ind).$left.set(leftPadding); - this.nodes.get('node' + ind).$top.set(upperPadding); - - ind++; - - } - - } - - } - - /** - * Formats all nodes as a square in the working field - */ - formatNodesSquare() { - const height = this.treeHeight(); - let leftPadding = 0; - let upperPadding = 0; - let ind = 0; - const edge = Math.ceil(Math.sqrt(this.counter)); - for (let i = 0; i < edge; i++) { - upperPadding = i * 250; - for (let j = 0; j < edge; j++) { - leftPadding = 300 * j; - this.nodes.get('node' + ind).$left.set(leftPadding); - this.nodes.get('node' + ind).$top.set(upperPadding); - - ind++; - - } - - } - } - - - /** - * Parses the whole JSON string received from the Query by Gesture Bridge and puts the parsed tree into the - * Pan Builder working field. - * @param data is JSON string received over the Web SOcket - */ - parseJson(data) { - const input = data; - if (input === null || input === '') { - return; - } - const inputObj = JSON.parse(data); - if (inputObj.nodes) { - const importedNodes = new Map(); - for (const [k, v] of Object.entries(inputObj.nodes)) { - importedNodes.set(v[0], Node.fromJson(v[1], this.dropArea.nativeElement.offsetWidth, this.dropArea.nativeElement.offsetHeight)); - } - this.nodes = importedNodes; - this.counter = importedNodes.size; - } - if (inputObj.connections) { - const importedConnections = new Map(); - for (const conn of Object.values(inputObj.connections)) { - importedConnections.set(conn[0], { - id: conn[1].id, - source: this.nodes.get(conn[1].source.id), - target: this.nodes.get(conn[1].target.id) - }); - } - this.connections = importedConnections; - } - this.setAutocomplete(); - } - - - toggleCache(b: boolean) { - this.cache = b; - } - -} diff --git a/src/app/views/querying/algebra/algebra.model.ts b/src/app/views/querying/algebra/algebra.model.ts deleted file mode 100644 index 7ff9970a..00000000 --- a/src/app/views/querying/algebra/algebra.model.ts +++ /dev/null @@ -1,182 +0,0 @@ -import {SortState} from '../../../components/data-view/models/sort-state.model'; -import {SidebarNode} from '../../../models/sidebar-node.model'; -import {signal, WritableSignal} from '@angular/core'; - -export enum LogicalOperator { - Scan = 'RelScan', - Join = 'Join', - Filter = 'RelFilter', - Project = 'RelProject', - Aggregate = 'Aggregate', - Minus = 'Minus', - Sort = 'Sort', - Union = 'Union', - Intersect = 'Intersect' -} - -export class LogicalOperatorUtil { - static operatorToSidebarNode(operator: LogicalOperator): SidebarNode { - let sidebarNode: SidebarNode; - switch (operator) { - case LogicalOperator.Scan: - sidebarNode = new SidebarNode('operator_' + operator, operator, 'fa fa-database', null, true); - break; - case LogicalOperator.Join: - sidebarNode = new SidebarNode('operator_' + operator, operator, null, null, true).setAlgSymbol('⋈'); - break; - case LogicalOperator.Filter: - sidebarNode = new SidebarNode('operator_' + operator, operator, null, null, true).setAlgSymbol('σ'); - break; - case LogicalOperator.Project: - sidebarNode = new SidebarNode('operator_' + operator, operator, null, null, true).setAlgSymbol('π'); - break; - case LogicalOperator.Aggregate: - sidebarNode = new SidebarNode('operator_' + operator, operator, 'fa fa-plus-circle', null, true); - break; - case LogicalOperator.Sort: - sidebarNode = new SidebarNode('operator_' + operator, operator, 'fa fa-arrows-v', null, true); - break; - case LogicalOperator.Union: - sidebarNode = new SidebarNode('operator_' + operator, operator, null, null, true).setAlgSymbol('∪'); - break; - case LogicalOperator.Minus: - sidebarNode = new SidebarNode('operator_' + operator, operator, 'fa fa-minus-circle', null, true); - break; - case LogicalOperator.Intersect: - sidebarNode = new SidebarNode('operator_' + operator, operator, null, null, true).setAlgSymbol('∩'); - break; - default: - sidebarNode = new SidebarNode('operator_' + operator, operator, 'fa fa-arrows', null, true); - } - return sidebarNode.setAutoActive(false); - } -} - -export interface Connection { - id: string; - source: Node; - target: Node; -} - -export type AlgNodeModel = { - name: string; - inputs: number; - icon: string; - symbol: string; - type: AlgType; -} - -export enum AlgType { - Join = 'Join', - Filter = 'Filter', - Project = 'Project', - Scan = 'Scan', - Aggregate = 'Aggregate', - Union = 'Union', - Sort = 'Sort', - Minus = 'Minus', - Modify = 'Modify', - ModifyCollect = 'ModifyCollect', - Intersect = 'Intersect', -} - -export class Node { - children: Node[] = []; - inputCount = 0; - dragging: boolean; - height: number; - width: number; - icon: string; - algSymbol: string; - class: string; - - //autocomplete - autocomplete: string[]; - acColumns = new Set(); - acTableColumns = new Set(); - acSchema = new Set(); - acTable = new Set(); - zIndex = 1; - - //parameters: - //Scan - entityName: string; - entityType: String; - - //Join - join = 'INNER'; - operator = '='; - col1: string; - col2: string; - - //filter - //(operator) - field: string; - filter: string; - - //project - fields: string[] = ['']; - - //aggregate - groupBy: string; - aggregation = 'SUM'; - alias: string; - //(field) - - //sort - sortColumns: SortState[] = [new SortState()]; - - //union, minus, intersect - all = false; - - //additional information needed for Views - tableTypes: String[]; - initialNames: String[]; - public $left: WritableSignal; - public $top: WritableSignal; - - constructor( - public id: string, - public type: AlgType, - left: number, - top: number - ) { - this.$left = signal(left); - this.$top = signal(top); - this.dragging = false; - } - - static fromJson(o: any, width: number, height: number) { - const n = new Node(o.id, o.type, o.left, o.top); - for (const [key, val] of Object.entries(o)) { - n[key] = o[key]; - } - //handle sets - const sets = ['acColumns', 'acTableColumns', 'acSchema', 'acTable']; - for (const s of sets) { - if (o[s].size !== undefined) { - n[s] = new Set([...o[s]]); - } else { - n[s] = new Set(); - } - } - //make sure, it is in the working area - if (n.$left() > width) { - n.$left.set(width - 260); - } - if (n.$top() > height) { - n.$top.set(height - 100); - } - - return n; - } - - clone() { - const n = new Node(this.id, this.type, this.$left(), this.$top()); - for (const [key, val] of Object.entries(this)) { - n[key] = val; - } - return n; - } - -} diff --git a/src/app/views/querying/algebra/node/node.component.html b/src/app/views/querying/algebra/node/node.component.html deleted file mode 100644 index 9489cf95..00000000 --- a/src/app/views/querying/algebra/node/node.component.html +++ /dev/null @@ -1,243 +0,0 @@ - -
    -
    - - - -
  • -
    - - - -
    -
  • - - -
  • -
    - - -
    -
  • - -
  • -
    - - -
    -
  • - -
  • -
    - - -
    -
  • -
  • -
    - - -
    -
  • -
    - - -
  • -
    - - -
    -
  • -
    - - -
    -
  • -
  • -
    - - -
    -
  • -
    - - -
  • - + add field -
  • -
  • -
    - - -
    -
  • -
    - - - -
  • -
    - - -
    -
  • -
  • -
    - - -
    -
  • -
  • -
    - - -
    -
  • -
  • -
    - - -
    -
  • -
    - - -
  • - + add column -
  • -
  • -
    - -
     
    - - - -
    -
  • -
    - -
  • -
    - - -
    -
  • - -
  • -
    - - -
    -
  • - -
  • -
    - - -
    -
  • - - - - - - - - - -
    -
    diff --git a/src/app/views/querying/algebra/node/node.component.scss b/src/app/views/querying/algebra/node/node.component.scss deleted file mode 100644 index 7b449d83..00000000 --- a/src/app/views/querying/algebra/node/node.component.scss +++ /dev/null @@ -1,43 +0,0 @@ -.param-wrapper label { - white-space: nowrap; -} - -.sort-wrapper:not(:last-child) { - margin-bottom: 5px; -} - -.sort-direction { - margin-left: 0.5em; - width: 7em; -} - -.sort-remove { - cursor: pointer; - line-height: 1.8; - margin-left: 0.5em; - color: #73818f; -} - -.sort-remove:hover { - color: initial; -} - -.sort-wrapper { - height: 30px; -} - -.sort-wrapper .icon-menu { - line-height: 1.8; - margin-right: 0.5em; - cursor: ns-resize; -} - -.sort-placeholder { - background: #f0f3f5; - margin: 5px 0; - border-radius: 0.25rem; -} - -.cdk-drag-preview { - display: flex; -} diff --git a/src/app/views/querying/algebra/node/node.component.ts b/src/app/views/querying/algebra/node/node.component.ts deleted file mode 100644 index c78f404e..00000000 --- a/src/app/views/querying/algebra/node/node.component.ts +++ /dev/null @@ -1,90 +0,0 @@ -import {AfterViewChecked, Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core'; -import {AlgType, Node} from '../algebra.model'; -import {SortDirection, SortState} from '../../../../components/data-view/models/sort-state.model'; -import {CdkDragDrop, moveItemInArray} from '@angular/cdk/drag-drop'; - -@Component({ - selector: 'app-node', - templateUrl: './node.component.html', - styleUrls: ['./node.component.scss'] -}) -export class NodeComponent implements OnInit, AfterViewChecked { - - constructor() { - } - - isView = false; - - @ViewChild('element', {read: ElementRef}) public element: ElementRef; - @Input() node: Node; - @Output() autocompleteChanged = new EventEmitter(); - - - protected readonly AlgType = AlgType; - - ngOnInit() { - - } - - ngAfterViewChecked() { - this.node.height = this.element.nativeElement.offsetHeight; - this.node.width = this.element.nativeElement.offsetWidth; - } - - addSortColumn() { - this.node.sortColumns.push(new SortState()); - this.node.height += 35; - } - - addProjectionColumn() { - this.node.fields.push(''); - this.node.height += 35; - } - - removeSortColumn(index: number) { - if (this.node.sortColumns.length > 1) { - this.node.sortColumns.splice(index, 1); - this.node.height -= 35; - } - } - - removeProjectionColumn(index: number) { - if (this.node.fields.length > 1) { - this.node.fields.splice(index, 1); - this.node.height -= 35; - } else { - this.node.fields[0] = ''; - } - this.autocompleteChange(); - } - - sortColumn(node: Node, event: CdkDragDrop) { - moveItemInArray(node.sortColumns, event.previousIndex, event.currentIndex); - } - - toggleDirection(col: SortState) { - col.direction = col.direction === SortDirection.DESC ? SortDirection.ASC : SortDirection.DESC; - } - - getAcCols(): string[] { - return [...this.node.acColumns]; - } - - getAcTableCols(): string[] { - return [...this.node.acTableColumns]; - } - - autocompleteChange() { - if (this.node.initialNames.includes(this.node.entityName)) { - const index = this.node.initialNames.indexOf(this.node.entityName); - this.node.entityType = this.node.tableTypes[index]; - this.isView = this.node.tableTypes[index] === 'VIEW'; - - } - this.autocompleteChanged.emit(); - } - - trackFields(index: number, obj: any): any { - return obj.length; - } -} diff --git a/src/app/views/querying/querying.component.html b/src/app/views/querying/querying.component.html index f03ab123..fbd01b1c 100644 --- a/src/app/views/querying/querying.component.html +++ b/src/app/views/querying/querying.component.html @@ -1,6 +1,5 @@ - diff --git a/src/app/views/views.module.ts b/src/app/views/views.module.ts index c81c463f..057b8f2d 100644 --- a/src/app/views/views.module.ts +++ b/src/app/views/views.module.ts @@ -15,9 +15,7 @@ import {EditTablesComponent} from './schema-editing/edit-tables/edit-tables.comp import {MonitoringComponent} from './monitoring/monitoring.component'; import {DashboardComponent} from './dashboard/dashboard.component'; import {DragDropModule} from '@angular/cdk/drag-drop'; -import {AlgebraComponent} from './querying/algebra/algebra.component'; import {QueryingComponent} from './querying/querying.component'; -import {NodeComponent} from './querying/algebra/node/node.component'; import {AutocompleteLibModule} from 'angular-ng-autocomplete'; import {AdaptersComponent} from './adapters/adapters.component'; import {RefinementOptionsComponent} from './querying/graphical-querying/refinement-options/refinement-options.component'; @@ -176,9 +174,7 @@ import {PolyalgComponent, ScrollToDirective} from './querying/polyalg/polyalg.co GraphEditGraphComponent, MonitoringComponent, DashboardComponent, - AlgebraComponent, QueryingComponent, - NodeComponent, AdaptersComponent, RefinementOptionsComponent, AboutComponent, From e58f781346b7ed3ac081991b5ac0a11abe162929 Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Fri, 12 Jul 2024 13:07:48 +0200 Subject: [PATCH 52/54] Cleanup & comments --- .../polyalg/algnode/alg-node.component.ts | 2 +- .../polyalg/polyalg-viewer/alg-editor.ts | 19 +++++++++---------- .../polyalg-viewer/alg-viewer.component.ts | 17 ++++++++++++++--- .../querying/polyalg/polyalg.component.ts | 3 +++ src/scss/style.scss | 3 +-- 5 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/app/components/polyalg/algnode/alg-node.component.ts b/src/app/components/polyalg/algnode/alg-node.component.ts index 30bcd15e..8e376cab 100644 --- a/src/app/components/polyalg/algnode/alg-node.component.ts +++ b/src/app/components/polyalg/algnode/alg-node.component.ts @@ -101,7 +101,7 @@ export class AlgNode extends ClassicPreset.Node { constructor(public readonly decl: Declaration, public readonly planType: PlanType, args: { [key: string]: PlanArgument } | null, public readonly metadata: AlgMetadata | null, isSimpleMode: boolean, public isReadOnly: boolean, private updateArea: (a: AlgNode, delta: Position) => void) { - super(decl.convention? decl.name : decl.name.substring(decl.name.indexOf('_') + 1)); + super(decl.convention ? decl.name : decl.name.substring(decl.name.indexOf('_') + 1)); this.modelBadge = getModelPrefix(decl.model); this.modelColor = MODEL_COLORS.get(decl.model); this.isSimpleMode = signal(isSimpleMode); diff --git a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts index ce7f1ab7..cbeefa5f 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts @@ -25,6 +25,7 @@ import {MagneticConnectionComponent} from './magnetic-connection/magnetic-connec import {AlgMetadata} from '../algnode/alg-metadata/alg-metadata.component'; import {Transform} from 'rete-area-plugin/_types/area'; import {PlanType} from '../../../models/information-page.model'; +import {OperatorModel} from '../models/polyalg-registry'; export type Schemes = GetSchemes>; type AreaExtra = AngularArea2D | ContextMenuExtra; @@ -35,7 +36,6 @@ export async function createEditor(container: HTMLElement, injector: Injector, r const readonlyPlugin = new ReadonlyPlugin(); - //const socket = new ClassicPreset.Socket('socket'); const editor = new NodeEditor(); const area = new AreaPlugin(container); const connection = new ConnectionPlugin; @@ -48,11 +48,13 @@ export async function createEditor(container: HTMLElement, injector: Injector, r ) }); + // make nodes selectable const selector = AreaExtensions.selector(); AreaExtensions.selectableNodes(area, selector, { accumulating: AreaExtensions.accumulateOnCtrl() }); + // customize rendering of nodes, controls, connections and sockets render.addPreset(Presets.classic.setup>({ customize: { node() { @@ -76,14 +78,14 @@ export async function createEditor(container: HTMLElement, injector: Injector, r }, socketPositionWatcher: getDOMSocketPosition({ offset({x, y}, nodeId, side, key) { - return {x, y}; + return {x, y}; // remove default shift of socket positions }, }) })); - render.addPreset(Presets.contextMenu.setup({delay: 100})); connection.addPreset(ConnectionPresets.classic.setup()); + // Customizing the arrangement of nodes arrange.addPreset(() => { return { port(n) { @@ -105,6 +107,7 @@ export async function createEditor(container: HTMLElement, injector: Injector, r const updateSizeFct = (a: AlgNode, delta: Position) => updateSize(a, delta, area, isReadOnly ? readonlyPlugin : null, () => arrange.layout({applier: undefined, options: layoutOpts}), $modifyEvent); + render.addPreset(Presets.contextMenu.setup({delay: 100})); // time in ms for context menu to close const contextMenu = new ContextMenuPlugin({ items: getContextMenuItems(registry, userMode, planType, isReadOnly, updateSizeFct) }); @@ -120,32 +123,29 @@ export async function createEditor(container: HTMLElement, injector: Injector, r if (!isReadOnly) { area.use(connection); // make connections editable area.use(contextMenu); // add context menu - useMagneticConnection(connection, getMagneticConnectionProps(editor)); } AreaExtensions.simpleNodesOrder(area); addCustomBackground(area); - AreaExtensions.restrictor(area, {scaling: {min: 0.02, max: 5}}); + AreaExtensions.restrictor(area, {scaling: {min: 0.03, max: 5}}); // Restrict Zoom let panningBoundary = null; if (!isReadOnly) { panningBoundary = setupPanningBoundary({area, selector, padding: 40, intensity: 2}); } + // Add nodes, connections and arrange them const [nodes, connections] = addNode(registry, planType, node, isReadOnly, updateSizeFct); for (const n of nodes) { await editor.addNode(n); } - for (const c of connections) { updateMultiConnAfterCreate(editor, c.source, c.target); await editor.addConnection(c); } - await arrange.layout({ applier: undefined, options: layoutOpts }); - if (oldTransform) { await area.area.zoom(oldTransform.k, oldTransform.x, oldTransform.y); } else { @@ -156,7 +156,6 @@ export async function createEditor(container: HTMLElement, injector: Injector, r editor.addPipe(context => { if (context.type === 'connectioncreate') { if (!canCreateConnection(editor, context.data)) { - //alert('Sockets are not compatible'); return; } updateMultiConnAfterCreate(editor, context.data.source, context.data.target); @@ -193,7 +192,7 @@ export async function createEditor(container: HTMLElement, injector: Injector, r area.destroy(); panningBoundary?.destroy(); }, - toPolyAlg: async () => { + toPolyAlg: async (): Promise<[string, OperatorModel]> => { if (editor.getNodes().length === 0) { return ['', null]; } diff --git a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts index 87553709..0647a8d2 100644 --- a/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts +++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts @@ -5,15 +5,26 @@ import {PolyAlgService} from '../polyalg.service'; import {EditorComponent} from '../../editor/editor.component'; import {AlgValidatorService, trimLines} from './alg-validator.service'; import {ToasterService} from '../../toast-exposer/toaster.service'; -import {Subscription, timer} from 'rxjs'; +import {Observable, Subscription, timer} from 'rxjs'; import {switchMap} from 'rxjs/operators'; import {ActivatedRoute, Router} from '@angular/router'; import {PlanType} from '../../../models/information-page.model'; import {OperatorModel} from '../models/polyalg-registry'; import {AccordionItemComponent} from '@coreui/angular'; +import {Transform} from 'rete-area-plugin/_types/area'; type editorState = 'SYNCHRONIZED' | 'CHANGED' | 'INVALID' | 'READONLY'; +interface NodeEditor { + layout: () => Promise; + destroy: () => void; + toPolyAlg: () => Promise<[string | null, OperatorModel | null]>; + onModify: Observable; + showMetadata: (b: boolean) => boolean; + getTransform: () => Transform; + hasUnregisteredNodes: boolean; +} + @Component({ selector: 'app-alg-viewer', templateUrl: './alg-viewer.component.html', @@ -53,7 +64,7 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { showEditModal = signal(false); private modifySubscription: Subscription; - nodeEditor: { getTransform: any; onModify: any; destroy: any; toPolyAlg: any; showMetadata: any; layout?: () => Promise; hasUnregisteredNodes?: boolean; }; + nodeEditor: NodeEditor; showNodeEditor = computed(() => this._registry.registryLoaded()); private isNodeFocused = false; // If a node is focused we must assume that a control has changed. Thus, the nodeEditor cannot be 'SYNCHRONIZED'. showMetadata = false; @@ -158,7 +169,7 @@ export class AlgViewerComponent implements AfterViewInit, OnChanges, OnDestroy { } generateTextFromNodeEditor(validatePlanWithBackend = false) { - this.getPolyAlgFromTree().then(([str, model]) => { + this.getPolyAlgFromTree().then(([str]) => { if (str != null) { if (!this.initialPolyAlg) { this.initialPolyAlg = str; diff --git a/src/app/views/querying/polyalg/polyalg.component.ts b/src/app/views/querying/polyalg/polyalg.component.ts index b67ae30d..cfcba644 100644 --- a/src/app/views/querying/polyalg/polyalg.component.ts +++ b/src/app/views/querying/polyalg/polyalg.component.ts @@ -254,6 +254,9 @@ export class PolyalgComponent implements OnInit, OnDestroy { this.showPlanTypeModal.set(false); if (hasChanged) { this.polyAlg = SAMPLE_PLANS[this.planType]; + this._leftSidebar.setNodes([]); + this._leftSidebar.close(); + this.result.set(null); } } diff --git a/src/scss/style.scss b/src/scss/style.scss index 0bc48d1e..6d29fa04 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -80,9 +80,8 @@ } } - +/* Node Editor for query plans */ [rete-context-menu] { - // TODO: adjust style of context menu width: 200px !important; border-radius: 4px; From 2935f3dcb74831991aad2f52bbae6e433cc0d633 Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Tue, 16 Jul 2024 17:46:53 +0200 Subject: [PATCH 53/54] Small fixes --- .../components/polyalg/controls/rex-arg/rex-arg.component.ts | 3 +-- src/app/views/querying/polyalg/polyalg.component.html | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts index df4e8fdd..914560d4 100644 --- a/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts +++ b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts @@ -71,10 +71,9 @@ export class RexControl extends ArgControl { const polyAlg = `${values.operator}(${values.r1}, ${values.r2})`; this.rex.set(polyAlg); } else if (this.simpleType() === SimpleType.REX_UINT) { - this.rex.set(this.simpleValues.REX_UINT.i?.toString(10)); + this.rex.set(this.simpleValues.REX_UINT.i?.toString(10) || ''); } else if (this.isPolyNode) { return this.rex().startsWith(NODE_PREFIX) ? this.rex() : NODE_PREFIX + this.rex(); - } else if (this.isPolyPath) { return this.rex().startsWith(PATH_PREFIX) ? this.rex() : PATH_PREFIX + this.rex(); } else { diff --git a/src/app/views/querying/polyalg/polyalg.component.html b/src/app/views/querying/polyalg/polyalg.component.html index b4b30020..08c767ab 100644 --- a/src/app/views/querying/polyalg/polyalg.component.html +++ b/src/app/views/querying/polyalg/polyalg.component.html @@ -89,7 +89,7 @@
    Help
    -

    The PolyPlan Builder can be used to create or edit plans using either the Algebra Editor or the Node Editor.

    +

    The Plan Builder can be used to create or edit plans using either the Algebra Editor or the Node Editor.

    The color around an editor indicates its state:

    • From 1fa101e7b69162c686e15356c89dd050f994f6dc Mon Sep 17 00:00:00 2001 From: Tobias Weber Date: Wed, 17 Jul 2024 12:03:56 +0200 Subject: [PATCH 54/54] Rename sidebar node --- src/app/views/querying/polyalg/polyalg.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/views/querying/polyalg/polyalg.component.ts b/src/app/views/querying/polyalg/polyalg.component.ts index cfcba644..1ffff708 100644 --- a/src/app/views/querying/polyalg/polyalg.component.ts +++ b/src/app/views/querying/polyalg/polyalg.component.ts @@ -207,7 +207,7 @@ export class PolyalgComponent implements OnInit, OnDestroy { }); } - sidebarNodes.unshift(new SidebarNode('polyPlanBuilder', 'PolyPlan Builder', 'fa fa-cubes').setAction(nodeBehavior)); + sidebarNodes.unshift(new SidebarNode('polyPlanBuilder', 'Plan Builder', 'fa fa-cubes').setAction(nodeBehavior)); this._leftSidebar.setNodes(sidebarNodes); if (sidebarNodes.length > 0) {