diff --git a/README.md b/README.md
index ed413ef4..f5943e19 100644
--- a/README.md
+++ b/README.md
@@ -29,8 +29,8 @@ Please note that we have a [code of conduct](https://github.com/polypheny/Admin/
## Credits ##
-Polypheny-UI is based on the beautiful [CoreUI](https://coreui.io/angular/) template. The query plan visualization is based on the [Postgres Explain Visualizer](https://github.com/AlexTatiyants/pev) which we have updated to be compatible with Angular 8 and modified to be used with Polypheny-DB.
+Polypheny-UI is based on the beautiful [CoreUI](https://coreui.io/angular/) template.
## License ##
The MIT License (MIT)
diff --git a/package-lock.json b/package-lock.json
index 640a0a45..2a646992 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,16 @@
"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-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",
"rxjs-compat": "^6.5.5",
"sass": "^1.26.3",
@@ -72,6 +83,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",
@@ -1724,6 +1736,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 +3711,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"
},
@@ -5332,6 +5363,13 @@
"@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,
+ "license": "MIT"
+ },
"node_modules/@types/d3-scale": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz",
@@ -5347,6 +5385,16 @@
"integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==",
"optional": true
},
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
+ "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
+ "dev": true,
+ "license": "MIT",
+ "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",
@@ -7943,6 +7991,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",
@@ -7990,11 +8047,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": {
@@ -8090,17 +8158,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",
@@ -8465,10 +8522,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 +11516,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 +14454,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 +14678,137 @@
"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-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",
+ "integrity": "sha512-KE1IcjeOQtHgkByODtWS5hgRJDGhR3Z9sZyJAEd7YMgI6o+KUIflcNjbkvhJvPeIAv6WlEAh7ZkwdLhF9bkr4w==",
+ "dependencies": {
+ "@babel/runtime": "^7.21.0"
+ },
+ "peerDependencies": {
+ "rete": "^2.0.1",
+ "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-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",
+ "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==",
+ "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 +16591,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..dfc71054 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,17 @@
"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",
+ "rete-readonly-plugin": "^2.0.1",
+ "rete-connection-path-plugin": "^2.0.3",
+ "rete-context-menu-plugin": "^2.0.3",
+ "rete-engine": "^2.0.1"
},
"devDependencies": {
"@angular-builders/custom-webpack": "^17.0.1",
@@ -97,7 +108,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/components.module.ts b/src/app/components/components.module.ts
index 0be0561c..2b80036b 100644
--- a/src/app/components/components.module.ts
+++ b/src/app/components/components.module.ts
@@ -4,23 +4,31 @@ import {GraphComponent} from './graph/graph.component';
import {NgChartsModule} from 'ng2-charts';
import {
+ AccordionButtonDirective,
+ AccordionComponent,
+ AccordionItemComponent,
+ BadgeComponent,
BgColorDirective,
BreadcrumbComponent as BreadCrumb,
ButtonCloseDirective,
ButtonDirective,
ButtonGroupComponent,
+ ButtonToolbarComponent,
CardBodyComponent,
CardComponent,
CardFooterComponent,
CardHeaderComponent,
ColComponent,
ColDirective,
+ CollapseDirective,
ContainerComponent,
DropdownComponent,
DropdownDividerDirective,
DropdownItemDirective,
DropdownMenuDirective,
- DropdownToggleDirective, FormCheckComponent, FormCheckInputDirective, FormCheckLabelDirective,
+ DropdownToggleDirective,
+ FormCheckInputDirective,
+ FormCheckLabelDirective,
FormControlDirective,
FormDirective,
FormFeedbackComponent,
@@ -42,6 +50,7 @@ import {
PageItemDirective,
PageLinkDirective,
PaginationComponent,
+ PopoverDirective,
ProgressBarComponent,
ProgressComponent,
RowComponent,
@@ -51,6 +60,7 @@ import {
TabContentRefDirective,
TableDirective,
TabPaneComponent,
+ TemplateIdDirective,
TextColorDirective,
ToastBodyComponent,
ToastCloseDirective,
@@ -71,7 +81,6 @@ import {RenderItemComponent} from './information-manager/render-item/render-item
import {InformationManagerComponent} from './information-manager/information-manager.component';
import {InputComponent} from './data-view/input/input.component';
import {EditorComponent} from './editor/editor.component';
-import {ExplainVisualizerModule} from '../explain-visualizer/explain-visualizer.module';
import {CollapseModule} from 'ngx-bootstrap/collapse';
import {TooltipModule} from 'ngx-bootstrap/tooltip';
import {ProgressbarModule} from 'ngx-bootstrap/progressbar';
@@ -99,6 +108,29 @@ 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 {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 {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 {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 {WindowArgComponent} from './polyalg/controls/window-arg/window-arg.component';
//import 'hammerjs';
@@ -119,7 +151,6 @@ import {DockerInstanceComponent} from './docker/dockerinstance/dockerinstance.co
CollapseModule,
TooltipModule,
ProgressbarModule.forRoot(),
- ExplainVisualizerModule,
ModalModule.forRoot(),
CarouselModule,
NgxJsonViewerModule,
@@ -168,7 +199,26 @@ import {DockerInstanceComponent} from './docker/dockerinstance/dockerinstance.co
DropdownMenuDirective,
DropdownItemDirective,
DropdownDividerDirective,
- DropdownToggleDirective, ModalTitleDirective, FormDirective, RowDirective, DropdownComponent, FormSelectDirective, TooltipDirective, ContainerComponent, FormCheckComponent, FormCheckInputDirective, FormCheckLabelDirective
+ DropdownToggleDirective,
+ ModalTitleDirective,
+ FormDirective,
+ RowDirective,
+ DropdownComponent,
+ FormSelectDirective,
+ TooltipDirective,
+ ContainerComponent,
+ ReteModule,
+ AutocompleteLibModule,
+ FormCheckLabelDirective,
+ AccordionComponent,
+ AccordionItemComponent,
+ AccordionButtonDirective,
+ TemplateIdDirective,
+ CollapseDirective,
+ PopoverDirective,
+ PopoverModule,
+ ButtonToolbarComponent,
+ BadgeComponent
],
declarations: [
BreadcrumbComponent,
@@ -198,6 +248,26 @@ import {DockerInstanceComponent} from './docker/dockerinstance/dockerinstance.co
ReloadButtonComponent,
ViewComponent,
DockerInstanceComponent,
+ AlgNodeComponent,
+ EntityArgComponent,
+ ListArgComponent,
+ RexArgComponent,
+ StringArgComponent,
+ BooleanArgComponent,
+ CustomSocketComponent,
+ CustomConnectionComponent,
+ EnumArgComponent,
+ AlgViewerComponent,
+ IntArgComponent,
+ FieldArgComponent,
+ CorrelationArgComponent,
+ CollationArgComponent,
+ AggArgComponent,
+ LaxAggArgComponent,
+ MagneticConnectionComponent,
+ AlgMetadataComponent,
+ DoubleArgComponent,
+ WindowArgComponent
],
exports: [
BreadcrumbComponent,
@@ -221,6 +291,7 @@ import {DockerInstanceComponent} from './docker/dockerinstance/dockerinstance.co
Toast,
ReloadButtonComponent,
DockerInstanceComponent,
+ AlgViewerComponent
]
})
export class ComponentsModule {
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..b4f99189 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,9 +1,9 @@
-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';
-import * as d3 from "d3";
+import * as d3 from 'd3';
@Component({
selector: 'app-data-graph',
@@ -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));
@@ -135,15 +137,18 @@ export class DataGraphComponent extends DataTemplateComponent {
.force('center', d3.forceCenter(width / 2, height / 2))
.force('charge', d3.forceManyBody().strength(-this.initialNodeIds.size))
.force('collide', d3.forceCollide(100).strength(0.9).radius(40))
- .force('link', d3.forceLink().id(d => d.id).distance(160));
+ .force('link', d3.forceLink().id(d => d.index).distance(160));
// disable charge after initial setup
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
@@ -572,7 +577,7 @@ export class DataGraphComponent extends DataTemplateComponent {
const id = node['id'];
if (!nodeIds.has(id)) {
nodeIds.add(id);
- this.initialNodes.add(node)
+ this.initialNodes.add(node);
}
});
}
diff --git a/src/app/components/data-view/data-view.component.html b/src/app/components/data-view/data-view.component.html
index 6294ebf2..33b4aa31 100644
--- a/src/app/components/data-view/data-view.component.html
+++ b/src/app/components/data-view/data-view.component.html
@@ -18,7 +18,7 @@
[class.active]="$presentationType() === presentationTypes.CARD"
tooltip="cards" placement="top" delay="200">
diff --git a/src/app/components/data-view/data-view.component.ts b/src/app/components/data-view/data-view.component.ts
index 08dde93c..01739662 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);
}
diff --git a/src/app/components/editor/editor.component.ts b/src/app/components/editor/editor.component.ts
index 974faba3..68185d1f 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';
@@ -199,9 +187,21 @@ export class EditorComponent implements OnInit, AfterViewInit, OnChanges {
}
}
+ setReadOnly(isReadOnly: boolean) {
+ this.codeEditor.setReadOnly(isReadOnly);
+ }
+
setScrollMargin(top: number, bottom: number, left: number = 0, right: number = 0) {
// https://groups.google.com/g/ace-discuss/c/LmMRaYnLzCk
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 50a35002..b2004e17 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
@@ -30,7 +30,7 @@
-
+
@@ -59,8 +59,8 @@
[max]="li.max">
-
-
+
+
diff --git a/src/app/components/information-manager/render-item/render-item.component.ts b/src/app/components/information-manager/render-item/render-item.component.ts
index 567466a5..bf4cdce2 100644
--- a/src/app/components/information-manager/render-item/render-item.component.ts
+++ b/src/app/components/information-manager/render-item/render-item.component.ts
@@ -13,6 +13,7 @@ export class RenderItemComponent implements OnInit {
@Input() li: InformationObject;
executingInformationAction = false;
+ codeHeight = '20px';
constructor(
private _infoService: InformationService,
@@ -21,6 +22,14 @@ export class RenderItemComponent implements OnInit {
}
ngOnInit() {
+ if (this.li.code) {
+ const match = this.li.code.match(/\n/g);
+ let numberOfLines = 1;
+ if (Array.isArray(match)) {
+ numberOfLines = this.li.code.match(/\n/g).length;
+ }
+ this.codeHeight = numberOfLines * 16 + 60 + 'px';
+ }
}
displayProgressValue(li) {
@@ -65,21 +74,6 @@ export class RenderItemComponent implements OnInit {
}
}
- getCodeHeight() {
- if (!this.li.code) {
- return '20px';
- } else {
-
- const match = this.li.code.match(/\n/g);
- let numberOfLines = 1;
- if (Array.isArray(match)) {
- numberOfLines = this.li.code.match(/\n/g).length;
- }
- console.log(numberOfLines * 16 + 60 + 'px');
- return numberOfLines * 16 + 60 + 'px';
- }
- }
-
executeInformationAction(i: InformationObject) {
this.executingInformationAction = true;
this._infoService.executeAction(i).subscribe({
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/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..375a17e6
--- /dev/null
+++ b/src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.html
@@ -0,0 +1,12 @@
+
+
{{badge.content}}
+
Auxiliary node (not part of original plan)
+
+
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..5bf31e71
--- /dev/null
+++ b/src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.scss
@@ -0,0 +1,3 @@
+.meta-table {
+ font-size: 0.75rem;
+}
\ No newline at end of file
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..6652ade3
--- /dev/null
+++ b/src/app/components/polyalg/algnode/alg-metadata/alg-metadata.component.ts
@@ -0,0 +1,64 @@
+import {Component, Input, signal} from '@angular/core';
+import {BadgeLevel, MetadataBadge, MetadataConnection, MetadataTableEntry, PlanMetadata} from '../../models/polyalg-plan.model';
+
+@Component({
+ selector: 'app-alg-metadata',
+ templateUrl: './alg-metadata.component.html',
+ styleUrl: './alg-metadata.component.scss'
+})
+export class AlgMetadataComponent {
+ @Input() data: AlgMetadata;
+
+ BADGE_COLORS = {
+ [BadgeLevel.INFO]: 'info',
+ [BadgeLevel.WARN]: 'warning',
+ [BadgeLevel.DANGER]: 'danger'
+ };
+}
+
+export class AlgMetadata {
+ height = signal(0);
+ table: MetadataTableEntry[];
+ badges: MetadataBadge[];
+ isAuxiliary: boolean;
+ outConnection?: MetadataConnection;
+
+ displayTable: [string, string][] = [];
+
+ constructor(meta: PlanMetadata) {
+ this.table = meta.table || [];
+ this.badges = meta.badges || [];
+ this.isAuxiliary = meta.isAuxiliary;
+ this.outConnection = meta.outConnection;
+
+ for (const entry of this.table) {
+ const name = entry.calculated ? entry.displayName + '*' : entry.displayName;
+ const value = this.round(entry.value, 2).toString();
+ this.displayTable.push([name, value]);
+ if (entry.cumulativeValue && entry.cumulativeValue !== entry.value) {
+ this.displayTable.push([name + ' (total)', this.round(entry.cumulativeValue, 2).toString()]);
+ }
+ }
+ this.recomputeHeight();
+ }
+
+ 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.badges.length > 0) {
+ height += 24;
+ }
+ height += this.displayTable.length * 27;
+ 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
new file mode 100644
index 00000000..30aeaf3d
--- /dev/null
+++ b/src/app/components/polyalg/algnode/alg-node.component.html
@@ -0,0 +1,61 @@
+{{ data.multiConnIdx }}
+
+
+
+
{{ data.label }}
+
+ Simple
+
+ {{ data.modelBadge }}
+
+
+
+
+
+
(unknown operator)
+
+
+
+
+
+
+
+
+
+
+
+
+
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..43bd1f52
--- /dev/null
+++ b/src/app/components/polyalg/algnode/alg-node.component.scss
@@ -0,0 +1,133 @@
+@use "sass:math";
+@import "scss/style.scss";
+
+// the width also needs to be changed in alg-node.component.ts
+$node-base-width: 350px;
+$node-metadata-width: 250px;
+$collapse-width: 16px;
+$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: auto !important;
+ height: auto !important;
+ position: relative;
+ user-select: none;
+
+ .socket-label {
+ position: absolute;
+ top: -40px;
+ left: 50%;
+ transform: translateX(-50%);
+ }
+
+ .node-body {
+ display: flex;
+ }
+
+ .controls {
+ width: $node-base-width;
+ }
+
+ .metadata {
+ width: $node-metadata-width;
+ }
+
+ .collapse-button {
+ width: $collapse-width;
+ padding: 2px;
+ }
+
+ .collapse-button:hover {
+ color: $primary;
+ background: linear-gradient(to left, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0) 100%);
+ }
+
+ .title-wrapper {
+ margin: -4px -4px 0px;
+ border-radius: 4px 4px 0 0;
+ }
+
+ .title-wrapper.auxiliary {
+ background-color: $secondary;
+ }
+
+ .simple-badge:hover {
+ background-color: $danger !important;
+ }
+
+ &:hover {
+ border-color: $primary !important;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
+
+ .title-wrapper {
+ background-color: $primary !important;
+ }
+ }
+
+ &.selected {
+ border-color: $primary !important;
+
+ .title-wrapper {
+ background-color: $primary !important;
+ }
+ }
+
+ .outputs {
+ display: flex;
+ justify-content: space-around;
+ margin-top: -22px;
+ height: 24px;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ }
+
+ .inputs {
+ display: flex;
+ justify-content: space-around;
+ margin-bottom: -8px;
+ height: 24px;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ }
+
+ .output, .input {
+ text-align: left;
+ display: block;
+ }
+
+ .output-socket {
+ text-align: right;
+ display: inline-block;
+ }
+
+ .input-socket {
+ text-align: left;
+ display: inline-block;
+ }
+
+ .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;
+ }
+}
+
+: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
new file mode 100644
index 00000000..8e376cab
--- /dev/null
+++ b/src/app/components/polyalg/algnode/alg-node.component.ts
@@ -0,0 +1,224 @@
+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, 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 {AlgNodeSocket} from '../custom-socket/custom-socket.component';
+import {AlgMetadata} from './alg-metadata/alg-metadata.component';
+import {getModelPrefix} from '../polyalg-viewer/alg-editor-utils';
+import {PlanType} from '../../../models/information-page.model';
+
+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();
+
+ effect(() => this.data.recomputeSize());
+ }
+
+ 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;
+ }
+
+ toggleCollapse() {
+ this.data.isMetaVisible.update(b => !b);
+ }
+
+ deactivateSimpleMode() {
+ this.data.isSimpleMode.set(false);
+ }
+}
+
+const BASE_WIDTH = 350;
+const METADATA_WIDTH = 250;
+const METADATA_COLLAPSE_WIDTH = 16;
+const BASE_HEIGHT = 75;
+const TAB_SIZE = 2; // the indentation width when generating PolyAlg
+
+const SINGLE_SOCKET_PRESETS = {
+ [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 = {
+ [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([
+ [OperatorModel.RELATIONAL, 'warning'],
+ [OperatorModel.DOCUMENT, 'warning'],
+ [OperatorModel.GRAPH, 'warning'],
+ [OperatorModel.COMMON, 'warning']
+]);
+
+export class AlgNode extends ClassicPreset.Node {
+ 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;
+ readonly isSimpleMode: WritableSignal;
+ readonly hasSimpleParams: boolean; // true if at least one parameter has a simple variant (even if it's hidden)
+ 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
+
+
+ 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));
+ 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);
+ }
+ 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;
+ this.addOutput('out', output);
+
+ let hasSimpleParams = false;
+ let hiddenSimpleParamsCount = 0;
+ let paramsCount = 0;
+ for (const p of decl.posParams.concat(decl.kwParams)) {
+ paramsCount++;
+ if (p.simpleType) {
+ hasSimpleParams = true;
+ if (p.simpleType === SimpleType.HIDDEN) {
+ hiddenSimpleParamsCount++;
+ }
+ }
+ const arg = args?.[p.name] || null;
+ const c = getControl(p, arg, isReadOnly, 0, decl.model, planType, this.isSimpleMode);
+
+ this.controlHeights.push(c.visibleHeight);
+
+ this.addControl(p.name, c);
+ }
+ this.hasSimpleParams = hasSimpleParams;
+
+ this.hasVisibleControls = computed(() => this.isSimpleMode() ? paramsCount - hiddenSimpleParamsCount > 0 : paramsCount > 0);
+
+ this.hasVariableInputs = decl.numInputs === -1;
+
+ if (this.hasVariableInputs) {
+ const input = new ClassicPreset.Input(MULTI_SOCKET_PRESETS[decl.model]);
+ input.multipleConnections = true;
+ this.addInput('0', input);
+ } else {
+ for (let i = 0; i < decl.numInputs; i++) {
+ this.addInput(i.toString(), new ClassicPreset.Input(SINGLE_SOCKET_PRESETS[decl.model]));
+ }
+ }
+ }
+
+ 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 + (this.isMetaVisible() ? Math.max(sum, this.metadata.height()) : sum);
+ let deltaY = 0;
+ if (oldHeight) {
+ deltaY = this.height - oldHeight;
+ deltaY += deltaY > 0 ? 1 : -1; // slight adjustment is required for smooth behavior
+ }
+ 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
+
+ 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 !== '[]' && polyAlg !== p.defaultPolyAlg) {
+ args.push(`${p.name}=${polyAlg}`);
+ }
+ }
+
+ let values;
+ if (this.hasVariableInputs) {
+ values = inputs['0'] || [];
+ } else {
+ 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) {
+ const indented = values.join(',\n').replace(/^/gm, this.tabIndent);
+ children = `(\n${indented}\n)`;
+ }
+
+ const polyAlg = `${this.decl.name}[${args.join(', ')}]${children}`;
+
+ return {'out': polyAlg};
+ }
+
+ clone() {
+ 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, this.planType, args, null, this.isSimpleMode(), this.isReadOnly, this.updateArea);
+ }
+
+}
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..790a494b
--- /dev/null
+++ b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.html
@@ -0,0 +1,74 @@
+
+ {{data.name}}
+
+
+
+
+ Function
+
+
+ {{ choice }}
+
+
+
+
+ Function Arg
+
+
+
+
+
+
+
+
+
+ Function
+
+
+ {{ choice }}
+
+
+
+
+ Function Arg(s)
+
+
+
+
+
+
+ Distinct
+
+
+
+
+ Approximate
+
+
+
+ Filter
+
+
+
+
+
+
+ Alias
+
+
+
+
\ No newline at end of file
diff --git a/src/app/explain-visualizer/assets/.gitkeep b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.scss
similarity index 100%
rename from src/app/explain-visualizer/assets/.gitkeep
rename to src/app/components/polyalg/controls/agg-arg/agg-arg.component.scss
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..150804e7
--- /dev/null
+++ b/src/app/components/polyalg/controls/agg-arg/agg-arg.component.ts
@@ -0,0 +1,65 @@
+import {Component, computed, Input, OnInit, Signal, Type} from '@angular/core';
+import {ArgControl} from '../arg-control';
+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 {PlanType} from '../../../../models/information-page.model';
+import {sanitizeAlias} from '../arg-control-utils';
+
+@Component({
+ selector: 'app-agg-arg',
+ templateUrl: './agg-arg.component.html',
+ styleUrl: './agg-arg.component.scss'
+})
+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();
+
+ protected readonly SimpleType = SimpleType;
+
+ ngOnInit(): void {
+ this.fChoices = this._registry.getEnumValues('AggFunctionOperator').slice().sort(); // sort shallow copy
+ }
+}
+
+
+export class AggControl extends ArgControl {
+ height = computed(() => this.isSimpleMode() ? 101 : 188);
+ argsStr = this.value.argList.join(', ');
+
+ constructor(param: Parameter, public value: AggArg, model: OperatorModel, planType: PlanType,
+ isSimpleMode: Signal, isReadOnly: boolean) {
+ super(param, model, planType, isSimpleMode, isReadOnly);
+ }
+
+ getArgComponent(): Type {
+ return AggArgComponent;
+ }
+
+ toPolyAlg(): string {
+ this.value.argList = this.argsStr.split(',').map(a => a.trim());
+ 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 : '';
+
+ let alias = '';
+ if (this.value.alias) {
+ alias = ' AS ' + sanitizeAlias(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
new file mode 100644
index 00000000..34faeed6
--- /dev/null
+++ b/src/app/components/polyalg/controls/arg-control-utils.ts
@@ -0,0 +1,188 @@
+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 {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';
+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 {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 {
+ if (arg == null) {
+ arg = getInitialArg(param, depth);
+ }
+
+ if (arg.isEnum) {
+ return new EnumControl(param, arg.type, arg.value as EnumArg, model, planType, isSimpleMode, isReadOnly);
+ }
+
+ switch (arg.type) {
+ case 'ANY':
+ 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':
+ return new BooleanControl(param, arg.value as BooleanArg, model, planType, isSimpleMode, isReadOnly);
+ case 'REX':
+ return new RexControl(param, arg.value as RexArg, model, planType, isSimpleMode, isReadOnly);
+ case 'AGGREGATE':
+ return new AggControl(param, arg.value as AggArg, model, planType, isSimpleMode, isReadOnly);
+ case 'LAX_AGGREGATE':
+ return new LaxAggControl(param, arg.value as LaxAggArg, model, planType, isSimpleMode, isReadOnly);
+ case 'ENTITY':
+ return new EntityControl(param, arg.value as EntityArg, model, planType, isSimpleMode, isReadOnly);
+ case 'FIELD':
+ return new FieldControl(param, arg.value as FieldArg, model, planType, isSimpleMode, isReadOnly);
+ case 'LIST':
+ return new ListControl(param, arg.value as ListArg, depth, model, planType, isSimpleMode, isReadOnly);
+ case 'COLLATION':
+ 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);
+}
+
+
+export function getInitialArg(p: Parameter, depth: number): PlanArgument {
+ if (p.defaultValue && p.multiValued === 0) {
+ return p.defaultValue; // kwParams always have a defaultValue
+ }
+ const isListArg = depth < p.multiValued; // not yet on the depth of the actual argument
+
+ 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 {
+ type: isListArg ? ParamType.LIST : p.type,
+ value: (function () {
+ if (isListArg) {
+ const innerType = p.multiValued > depth + 1 ? ParamType.LIST : p.type;
+ return {innerType: innerType, args: [getInitialArg(p, depth + 1)]};
+ }
+ if (p.isEnum) {
+ return {arg: ''};
+ }
+ switch (p.type) {
+ case ParamType.ANY:
+ break;
+ case ParamType.INTEGER:
+ return {arg: 0};
+ case ParamType.DOUBLE:
+ return {arg: 0};
+ case ParamType.STRING:
+ return {arg: ''};
+ case ParamType.BOOLEAN:
+ return {arg: false};
+ case ParamType.REX:
+ return {rex: ''};
+ case ParamType.AGGREGATE:
+ return {function: 'COUNT', distinct: false, approximate: false, argList: [], collList: [], alias: ''};
+ case ParamType.LAX_AGGREGATE:
+ return {function: 'COUNT', input: '', alias: ''};
+ case ParamType.ENTITY:
+ return {arg: ''};
+ case ParamType.FIELD:
+ return {arg: ''};
+ case ParamType.COLLATION:
+ return {
+ field: '',
+ direction: CollDirection.ASCENDING,
+ nullDirection: defaultNullDirection(CollDirection.ASCENDING)
+ };
+ case ParamType.CORR_ID:
+ return {arg: 0};
+ case ParamType.WINDOW_GROUP:
+ return {isRows: false, lowerBound: '', upperBound: '', aggCalls: [], orderKeys: []};
+ }
+ return null;
+ })(),
+ 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 === '[' || 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 (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;
+}
+
+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/arg-control.ts b/src/app/components/polyalg/controls/arg-control.ts
new file mode 100644
index 00000000..fce03583
--- /dev/null
+++ b/src/app/components/polyalg/controls/arg-control.ts
@@ -0,0 +1,29 @@
+import {ClassicPreset} from 'rete';
+import {computed, Signal, signal, Type} from '@angular/core';
+import {OperatorModel, Parameter, SimpleType} from '../models/polyalg-registry';
+import {PlanArgument} from '../models/polyalg-plan.model';
+import {PlanType} from '../../../models/information-page.model';
+
+export abstract class ArgControl extends ClassicPreset.Control {
+ readonly name: string;
+ isTrivial: Signal = signal(false);
+ height: Signal;
+ readonly visibleHeight: Signal;
+ 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: OperatorModel, public readonly planType: PlanType,
+ public readonly isSimpleMode: Signal, public isReadOnly: boolean, enforceName = false) {
+ super();
+ this.name = param.multiValued > 0 && !enforceName ? null : param.name;
+ this.isHidden = computed(() => param.simpleType === SimpleType.HIDDEN && this.isSimpleMode());
+ this.visibleHeight = computed(() => this.isHidden() ? 0 : this.height());
+ this.simpleType = computed(() => this.isSimpleMode() ? param.simpleType : null);
+ }
+
+ 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
new file mode 100644
index 00000000..36d7b6f9
--- /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..62999594
--- /dev/null
+++ b/src/app/components/polyalg/controls/boolean-arg/boolean-arg.component.ts
@@ -0,0 +1,36 @@
+import {Component, Input, Signal, signal, Type} from '@angular/core';
+import {BooleanArg, PlanArgument} from '../../models/polyalg-plan.model';
+import {ArgControl} from '../arg-control';
+import {OperatorModel, Parameter, ParamType} from '../../models/polyalg-registry';
+import {PlanType} from '../../../../models/information-page.model';
+
+@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 {
+ height = signal(50);
+
+ constructor(param: Parameter, public value: BooleanArg, model: OperatorModel, planType: PlanType,
+ isSimpleMode: Signal, isReadOnly: boolean) {
+ super(param, model, planType, isSimpleMode, isReadOnly);
+ }
+
+ 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/collation-arg/collation-arg.component.html b/src/app/components/polyalg/controls/collation-arg/collation-arg.component.html
new file mode 100644
index 00000000..13c6ae68
--- /dev/null
+++ b/src/app/components/polyalg/controls/collation-arg/collation-arg.component.html
@@ -0,0 +1,39 @@
+
+ {{data.name}}
+
+
+
+
+
+
+ Direction
+
+ {{ choice }}
+
+
+
+
+
+
+
+
+ Direction
+
+ {{ choice }}
+
+
+
+
+ Null Direction
+
+ {{ choice }}
+
+
+
+
+
\ 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..320b763d
--- /dev/null
+++ b/src/app/components/polyalg/controls/collation-arg/collation-arg.component.ts
@@ -0,0 +1,57 @@
+import {Component, computed, Input, Signal, Type} from '@angular/core';
+import {ArgControl} from '../arg-control';
+import {OperatorModel, Parameter, ParamType, SimpleType} from '../../models/polyalg-registry';
+import {CollationArg, CollDirection, CollNullDirection, defaultNullDirection, PlanArgument} from '../../models/polyalg-plan.model';
+import {PlanType} from '../../../../models/information-page.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);
+ simpleDirChoices = this.dirChoices.filter(dir => dir.startsWith('ASC') || dir.startsWith('DESC'));
+ nullDirChoices = Object.keys(CollNullDirection);
+ protected readonly CollNullDirection = CollNullDirection;
+ protected readonly CollDirection = CollDirection;
+ protected readonly SimpleType = SimpleType;
+}
+
+export class CollationControl extends ArgControl {
+ height = computed(() => this.isSimpleMode() ? 66 : 101);
+
+ constructor(param: Parameter, public value: CollationArg, model: OperatorModel, planType: PlanType,
+ isSimpleMode: Signal, isReadOnly: boolean) {
+ super(param, model, planType, isSimpleMode, 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;
+ }
+
+ getArgComponent(): Type {
+ return CollationArgComponent;
+ }
+
+ toPolyAlg(): string {
+ if (this.simpleType() === SimpleType.SIMPLE_COLLATION) {
+ this.value.nullDirection = defaultNullDirection(this.value.direction);
+ }
+ 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 @@
+{{data.name}}
+
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..f792f0d7
--- /dev/null
+++ b/src/app/components/polyalg/controls/correlation-arg/correlation-arg.component.ts
@@ -0,0 +1,36 @@
+import {Component, Input, Signal, signal, Type} from '@angular/core';
+import {ArgControl} from '../arg-control';
+import {OperatorModel, Parameter, ParamType} from '../../models/polyalg-registry';
+import {CorrelationArg, PlanArgument} from '../../models/polyalg-plan.model';
+import {PlanType} from '../../../../models/information-page.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 {
+ height = signal(this.name ? 55 : 31);
+
+ constructor(param: Parameter, public value: CorrelationArg, model: OperatorModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) {
+ super(param, model, planType, isSimpleMode, isReadOnly);
+ }
+
+ 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/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 @@
+{{data.name}}
+
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/entity-arg/entity-arg.component.html b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.html
new file mode 100644
index 00000000..c75ab93e
--- /dev/null
+++ b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.html
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
\ 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..8935bdf3
--- /dev/null
+++ 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
new file mode 100644
index 00000000..31051644
--- /dev/null
+++ b/src/app/components/polyalg/controls/entity-arg/entity-arg.component.ts
@@ -0,0 +1,85 @@
+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';
+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',
+ templateUrl: './entity-arg.component.html',
+ 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);
+
+ ngOnInit(): void {
+ if (this.data.model === OperatorModel.GRAPH) {
+ 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.data.isPhysical) {
+ 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.planType === 'ALLOCATION';
+ readonly isPhysical = this.planType === 'PHYSICAL';
+ height = signal((this.name ? 55 : 31) + (this.isAllocation ? 2 * 31 : 0));
+
+ constructor(param: Parameter, public value: EntityArg, model: OperatorModel, planType: PlanType,
+ isSimpleMode: Signal, isReadOnly: boolean) {
+ super(param, model, planType, isSimpleMode, isReadOnly);
+ }
+
+ getArgComponent(): Type {
+ return EntityArgComponent;
+ }
+
+ toPolyAlg(): string {
+ if (this.isAllocation) {
+ let polyAlg = `${this.value.fullName}@${this.value.adapterName}`;
+ if (this.value.partitionId) {
+ polyAlg += '.' + this.value.partitionId;
+ }
+ return polyAlg;
+ } else if (this.isPhysical) {
+ return `${this.value.adapterName}.${this.value.physicalId}`;
+ }
+ return this.value.fullName;
+ }
+
+ 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.html b/src/app/components/polyalg/controls/enum-arg/enum-arg.component.html
new file mode 100644
index 00000000..e78890e2
--- /dev/null
+++ b/src/app/components/polyalg/controls/enum-arg/enum-arg.component.html
@@ -0,0 +1,10 @@
+
+ {{data.name}}
+
+
+
+ {{ choice }}
+
+
+
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..5a2533aa
--- /dev/null
+++ b/src/app/components/polyalg/controls/enum-arg/enum-arg.component.ts
@@ -0,0 +1,48 @@
+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 {OperatorModel, Parameter} from '../../models/polyalg-registry';
+import {PlanType} from '../../../../models/information-page.model';
+
+@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);
+ if (!this.data.value.arg) {
+ this.data.value.arg = this.choices[0];
+ }
+ }
+
+}
+
+export class EnumControl extends ArgControl {
+ height = signal(this.name ? 55 : 31);
+
+ constructor(param: Parameter, public type: string, public value: EnumArg, model: OperatorModel, planType: PlanType,
+ isSimpleMode: Signal, isReadOnly: boolean) {
+ super(param, model, planType, isSimpleMode, isReadOnly);
+ }
+
+ getArgComponent(): Type {
+ 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/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 @@
+{{data.name}}
+
\ 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..76c4d260
--- /dev/null
+++ b/src/app/components/polyalg/controls/field-arg/field-arg.component.ts
@@ -0,0 +1,36 @@
+import {Component, Input, Signal, signal, Type} from '@angular/core';
+import {ArgControl} from '../arg-control';
+import {OperatorModel, Parameter, ParamType} from '../../models/polyalg-registry';
+import {FieldArg, PlanArgument} from '../../models/polyalg-plan.model';
+import {PlanType} from '../../../../models/information-page.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 {
+ height = signal(this.name ? 55 : 31);
+
+ constructor(param: Parameter, public value: FieldArg, model: OperatorModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) {
+ super(param, model, planType, isSimpleMode, isReadOnly);
+ }
+
+ 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 @@
+{{data.name}}
+
\ 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..33e2f9d6
--- /dev/null
+++ b/src/app/components/polyalg/controls/int-arg/int-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 {IntArg, PlanArgument} from '../../models/polyalg-plan.model';
+import {PlanType} from '../../../../models/information-page.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; };
+ height = signal(this.name ? 55 : 31);
+
+ 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};
+ if (param.tags.includes(ParamTag.NON_NEGATIVE)) {
+ this.valueRange.min = 0;
+ }
+ }
+
+ getArgComponent(): Type {
+ return IntArgComponent;
+ }
+
+ toPolyAlg(): string {
+ if (this.value.arg == null) {
+ return '';
+ }
+ 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 @@
+{{data.name}}
+
+
+
+ Function
+
+
+ {{ choice }}
+
+
+
+
+ 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..b620a3ad
--- /dev/null
+++ b/src/app/components/polyalg/controls/lax-agg/lax-agg-arg.component.ts
@@ -0,0 +1,50 @@
+import {Component, Input, OnInit, Signal, signal, Type} from '@angular/core';
+import {ArgControl} from '../arg-control';
+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',
+ 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 {
+ height = signal(this.name ? 125 : 101);
+
+ constructor(param: Parameter, public value: LaxAggArg, model: OperatorModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) {
+ super(param, model, planType, isSimpleMode, isReadOnly);
+ }
+
+ getArgComponent(): Type {
+ return LaxAggArgComponent;
+ }
+
+ toPolyAlg(): string {
+ 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 {
+ 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
new file mode 100644
index 00000000..c65944c1
--- /dev/null
+++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.html
@@ -0,0 +1,83 @@
+{{data.name}}
+
+
+
+ Hide trivial {{data.name}}
+
+
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..a67814f5
--- /dev/null
+++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.scss
@@ -0,0 +1,16 @@
+.remove-element {
+ //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;
+}
+
+.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
new file mode 100644
index 00000000..3c6eccfe
--- /dev/null
+++ b/src/app/components/polyalg/controls/list-arg/list-arg.component.ts
@@ -0,0 +1,92 @@
+import {Component, computed, Input, Signal, signal, Type, WritableSignal} from '@angular/core';
+import {ListArg, PlanArgument} from '../../models/polyalg-plan.model';
+import {ArgControl} from '../arg-control';
+import {getControl} from '../arg-control-utils';
+import {OperatorModel, Parameter, ParamTag, ParamType} from '../../models/polyalg-registry';
+import {PlanType} from '../../../../models/information-page.model';
+
+@Component({
+ selector: 'app-list-arg',
+ templateUrl: './list-arg.component.html',
+ styleUrl: './list-arg.component.scss'
+})
+export class ListArgComponent {
+ @Input() data: ListControl;
+
+
+ protected readonly ParamType = ParamType;
+}
+
+export class ListControl extends ArgControl {
+ children: WritableSignal;
+ canHideTrivial = this.param.tags.includes(ParamTag.HIDE_TRIVIAL);
+ hideTrivial: WritableSignal;
+ height = computed(() => this.computeHeight());
+
+ 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) {
+ // 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, planType, isSimpleMode)));
+ if (this.children().length === 0 && value.innerType === ParamType.LIST && depth === param.multiValued - 1) {
+ value.innerType = param.type;
+ }
+ this.hideTrivial = signal(this.isReadOnly && this.canHideTrivial && this.children().filter(c => c.isTrivial()).length > 2);
+
+ }
+
+ computeHeight(): number {
+ let height = this.children().filter(c => !(this.hideTrivial() && c.isTrivial()))
+ .reduce((total, child) => total + child.height() + 16, 0);
+ if (!this.isReadOnly) {
+ height += 33; // add button
+ }
+ if (this.canHideTrivial) {
+ height += 24;
+ }
+ return 36 + height; // 36: title
+ }
+
+ addElement() {
+ this.hideTrivial.set(false);
+ this.children.update(values =>
+ [...values, getControl(this.param, null, this.isReadOnly, this.depth + 1, this.model, this.planType, this.isSimpleMode)]);
+ }
+
+ removeElement(child: ArgControl) {
+ this.children.update(values => values.filter(c => c !== child));
+
+ }
+
+ toggleHideTrivial() {
+ this.hideTrivial.update(old => !old);
+ }
+
+ getArgComponent(): Type {
+ return ListArgComponent;
+ }
+
+ toPolyAlg(): string {
+ if (this.children().length === 0) {
+ return '[]';
+ }
+
+ const args = this.children().map(arg => arg.toPolyAlg()).filter(s => s.length > 0).join(', ');
+ if (this.param.multiValued === 1 && (this.children().length === 1 || this.param.canUnpackValues)) {
+ return args;
+ }
+ return `[${args}]`;
+ }
+
+ copyArg(): PlanArgument {
+ 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
new file mode 100644
index 00000000..54102973
--- /dev/null
+++ b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.html
@@ -0,0 +1,40 @@
+
+ {{data.name}}
+
+
+
+
+ =
+ <>
+ <
+ >
+ ≤
+ ≥
+
+
+
+
+
+
+
+
+
+ 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..68a7bb13
--- /dev/null
+++ b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.scss
@@ -0,0 +1,8 @@
+#select-operator {
+ max-width: fit-content;
+ padding-right: 32px;
+}
+
+#alias {
+ max-width: 35%;
+}
\ 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
new file mode 100644
index 00000000..914560d4
--- /dev/null
+++ b/src/app/components/polyalg/controls/rex-arg/rex-arg.component.ts
@@ -0,0 +1,94 @@
+import {Component, computed, Input, Signal, signal, Type} from '@angular/core';
+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, sanitizeAlias} from '../arg-control-utils';
+
+@Component({
+ selector: 'app-rex-arg',
+ templateUrl: './rex-arg.component.html',
+ styleUrl: './rex-arg.component.scss'
+})
+export class RexArgComponent {
+ @Input() data: RexControl;
+
+ 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);
+
+ // 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;
+ });
+ simpleValues = {
+ 'REX_PREDICATE': {
+ r1: '',
+ r2: '',
+ operator: '=',
+ is1Valid: true,
+ is2Valid: true
+ },
+ 'REX_UINT': {
+ i: null
+ }
+ };
+ 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 {
+ return RexArgComponent;
+ }
+
+ 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 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()) {
+ return `${this.rex()} AS ${sanitizeAlias(this.alias())}`;
+ }
+ 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
new file mode 100644
index 00000000..a0eff948
--- /dev/null
+++ b/src/app/components/polyalg/controls/string-arg/string-arg.component.html
@@ -0,0 +1,15 @@
+
+ {{data.name}}
+
+
+
+
+ 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..5d3fd3dd
--- /dev/null
+++ 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/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..3c62f795
--- /dev/null
+++ b/src/app/components/polyalg/controls/string-arg/string-arg.component.ts
@@ -0,0 +1,54 @@
+import {Component, computed, Input, Signal, signal, Type} from '@angular/core';
+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, sanitizeAlias} from '../arg-control-utils';
+
+@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 {
+ readonly showAlias: boolean;
+ height = signal(this.name ? 55 : 31);
+
+ // 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, model: OperatorModel, planType: PlanType, isSimpleMode: Signal, isReadOnly: boolean) {
+ super(param, model, planType, isSimpleMode, isReadOnly);
+ this.showAlias = param.tags.includes(ParamTag.ALIAS);
+ }
+
+ getArgComponent(): Type {
+ return StringArgComponent;
+ }
+
+ toPolyAlg(): string {
+ const cleanedArg = hasValidStructure(this.arg()) ? this.arg() : `'${this.arg()}'`;
+ if (this.showAlias && this.alias() !== '' && this.alias() !== this.arg()) {
+ return `${cleanedArg} AS ${sanitizeAlias(this.alias())}`;
+ }
+ return cleanedArg;
+ }
+
+ 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/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 @@
+{{data.name}}
+(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/custom-connection/custom-connection.component.scss b/src/app/components/polyalg/custom-connection/custom-connection.component.scss
new file mode 100644
index 00000000..de433d2c
--- /dev/null
+++ b/src/app/components/polyalg/custom-connection/custom-connection.component.scss
@@ -0,0 +1,15 @@
+@import "scss/style.scss";
+
+svg {
+ overflow: visible !important;
+ position: absolute;
+ pointer-events: none;
+ width: 9999px;
+ height: 9999px;
+
+ path {
+ fill: none;
+ stroke: $primary;
+ pointer-events: auto;
+ }
+}
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..94880bb7
--- /dev/null
+++ b/src/app/components/polyalg/custom-connection/custom-connection.component.ts
@@ -0,0 +1,37 @@
+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!: CustomConnection;
+ @Input() start: Position;
+ @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;
+
+ constructor(source: N, sourceOutput: keyof N['outputs'], target: N, targetInput: keyof N['inputs'], width = 0) {
+ super(source, sourceOutput, target, targetInput);
+ this.width = DEFAULT_WIDTH + (MAX_WIDTH - DEFAULT_WIDTH) * Math.max(0, Math.min(width, 1));
+ }
+}
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..37a401f5
--- /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: calc($socket-size / 2);
+ 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;
+ }
+
+ &.multi {
+ width: 2*$socket-size;
+ }
+
+ &.output {
+ margin-right: calc(-1 * $socket-size / 2);
+ }
+
+ &.input {
+ margin-left: calc(-1 * $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..ba1c63b6
--- /dev/null
+++ b/src/app/components/polyalg/custom-socket/custom-socket.component.ts
@@ -0,0 +1,52 @@
+import {ChangeDetectorRef, Component, ElementRef, HostBinding, Input, OnChanges, OnInit} from '@angular/core';
+import {ClassicPreset} from 'rete';
+import {OperatorModel} from '../models/polyalg-registry';
+
+@Component({
+ selector: 'app-custom-socket',
+ template: ``,
+ styleUrl: './custom-socket.component.scss'
+})
+export class CustomSocketComponent implements OnInit, OnChanges {
+ @Input() data!: AlgNodeSocket;
+ @Input() rendered!: any;
+
+ @HostBinding('title') get title() {
+ return this.data.name;
+ }
+
+ constructor(private cdr: ChangeDetectorRef, private elementRef: ElementRef) {
+ this.cdr.detach();
+ }
+
+ ngOnInit(): void {
+ if (this.data.isMultiValued) {
+ this.elementRef.nativeElement.classList.add('multi');
+ }
+
+ }
+
+ ngOnChanges(): void {
+ this.cdr.detectChanges();
+ requestAnimationFrame(() => this.rendered());
+ }
+
+}
+
+export class AlgNodeSocket extends ClassicPreset.Socket {
+
+ /**
+ *
+ * @param model the OperatorModel this socket expects
+ * @param isMultiValued true if this socket supports multiple incoming connections
+ */
+ constructor(public readonly model: OperatorModel | null, public readonly isMultiValued = false) {
+ super(isMultiValued ? 'multi-valued input' : '');
+ }
+
+ isCompatibleWith(socket: AlgNodeSocket) {
+ return socket.model === this.model ||
+ socket.model === OperatorModel.COMMON ||
+ this.model === OperatorModel.COMMON;
+ }
+}
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..05d99efd
--- /dev/null
+++ b/src/app/components/polyalg/models/polyalg-plan.model.ts
@@ -0,0 +1,154 @@
+import {ParamType} from './polyalg-registry';
+
+export interface PlanNode {
+ opName: string;
+ arguments: {
+ [key: string]: PlanArgument
+ };
+ metadata: PlanMetadata;
+ inputs: PlanNode[];
+ defaultValue: string;
+}
+
+export interface PlanMetadata {
+ isAuxiliary: boolean;
+ table?: MetadataTableEntry[];
+ badges?: MetadataBadge[];
+ outConnection?: MetadataConnection;
+}
+
+export interface MetadataConnection {
+ width: number; // 0 <= width <= 1
+ forKey: string;
+}
+
+export interface MetadataBadge {
+ content: string;
+ forKey: string;
+ level: BadgeLevel;
+}
+
+export enum BadgeLevel {
+ INFO = 'INFO',
+ WARN = 'WARN',
+ DANGER = 'DANGER'
+}
+
+export interface MetadataTableEntry {
+ displayName: string;
+ value: number;
+ cumulativeValue?: number;
+ calculated: boolean;
+}
+
+export interface PlanArgument {
+ type: ParamType | string; // if isEnum, then type identifies the type of enum and is not a ParamType
+ value: ArgType;
+ isEnum?: boolean;
+}
+
+export interface EntityArg {
+ fullName: string;
+ adapterName?: string; // not null in case of an AllocationEntity or PhysicalEntity
+ partitionId?: number; // not null in case of an AllocationEntity
+ partitionName?: string; // might be null
+ physicalId?: number; // not null in case of a PhysicalEntity
+}
+
+export interface RexArg {
+ rex: string;
+ alias?: string;
+}
+
+export interface BooleanArg {
+ arg: boolean;
+}
+
+export interface ListArg {
+ innerType: ParamType | string;
+ args: PlanArgument[];
+}
+
+export interface StringArg {
+ arg: string;
+ alias?: string;
+}
+
+export interface EnumArg {
+ arg: string;
+}
+
+export interface IntArg {
+ arg: number;
+}
+
+export interface DoubleArg {
+ arg: number;
+}
+
+export interface LaxAggArg {
+ function: string;
+ input: string;
+ alias: string;
+}
+
+export interface AggArg {
+ 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;
+}
+
+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',
+ 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
new file mode 100644
index 00000000..2cc2ff08
--- /dev/null
+++ b/src/app/components/polyalg/models/polyalg-registry.ts
@@ -0,0 +1,82 @@
+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: OperatorModel;
+ numInputs: number;
+ 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.
+}
+
+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
+ simpleType: SimpleType | null;
+ isEnum: boolean;
+ multiValued: number; // how deeply nested arguments can be in lists (0 = not nested at all)
+ requiresAlias: boolean;
+ defaultValue?: PlanArgument;
+ defaultPolyAlg?: string;
+ canUnpackValues?: boolean;
+}
+
+export enum ParamType {
+ ANY = 'ANY',
+ INTEGER = 'INTEGER',
+ DOUBLE = 'DOUBLE',
+ STRING = 'STRING',
+ BOOLEAN = 'BOOLEAN',
+ REX = 'REX',
+ AGGREGATE = 'AGGREGATE',
+ LAX_AGGREGATE = 'LAX_AGGREGATE',
+ ENTITY = 'ENTITY',
+ FIELD = 'FIELD',
+ LIST = 'LIST',
+ COLLATION = 'COLLATION',
+ CORR_ID = 'CORR_ID',
+ WINDOW_GROUP = 'WINDOW_GROUP'
+}
+
+export enum OperatorTag {
+ LOGICAL = 'LOGICAL',
+ PHYSICAL = 'PHYSICAL',
+ ALLOCATION = 'ALLOCATION',
+ ADVANCED = 'ADVANCED'
+}
+
+export enum ParamTag {
+ ALIAS = 'ALIAS',
+ NON_NEGATIVE = 'NON_NEGATIVE',
+ HIDE_TRIVIAL = 'HIDE_TRIVIAL',
+ POLY_NODE = 'POLY_NODE',
+ POLY_PATH = 'POLY_PATH',
+}
+
+export enum SimpleType {
+ HIDDEN = 'HIDDEN',
+ REX_PREDICATE = 'REX_PREDICATE',
+ REX_UINT = 'REX_UINT',
+ 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
new file mode 100644
index 00000000..060fdd0e
--- /dev/null
+++ b/src/app/components/polyalg/polyalg-viewer/alg-editor-utils.ts
@@ -0,0 +1,231 @@
+import {AlgNodeSocket} from '../custom-socket/custom-socket.component';
+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 {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;
+type Output = ClassicPreset.Output;
+
+function getConnectionSockets(source: AlgNode, target: AlgNode, connection: Schemes['Connection']) {
+
+ const output = source && (source.outputs as Record)[connection.sourceOutput];
+
+ // @ts-ignore
+ const input = target && (target.inputs as Record)[connection.targetInput];
+
+ return {
+ source: output?.socket,
+ target: input?.socket
+ };
+}
+
+export function canCreateConnection(editor: NodeEditor, connection: Schemes['Connection']) {
+ const sourceNode = editor.getNode(connection.source);
+ const targetNode = editor.getNode(connection.target);
+ const connections = editor.getConnections();
+ let successor = targetNode.id;
+ while (successor) {
+ if (successor === sourceNode.id) {
+ console.log('detected recursion!');
+ return false;
+ }
+ successor = getSuccessor(successor, connections);
+ }
+
+ const {source, target} = getConnectionSockets(sourceNode, targetNode, connection);
+
+ return source && target && source.isCompatibleWith(target);
+}
+
+export function areSocketsCompatible(editor: NodeEditor, from: SocketData, to: SocketData) {
+ 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 {
+ 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;
+}
+
+export function getModelPrefix(model: OperatorModel) {
+ switch (model) {
+ case OperatorModel.DOCUMENT:
+ return 'DOC';
+ case OperatorModel.RELATIONAL:
+ return 'REL';
+ case OperatorModel.GRAPH:
+ return 'LPG';
+ default:
+ return '';
+ }
+}
+
+export function removeModelPrefix(name: string, model: OperatorModel) {
+ if (model === OperatorModel.COMMON) {
+ return name;
+ }
+ return name.substring(name.indexOf('_') + 1);
+}
+
+export function updateMultiConnAfterCreate(editor: NodeEditor, sourceId: string, targetId: string) {
+ const target = editor.getNode(targetId);
+ if (target.hasVariableInputs) {
+ const nodeIds = getPredecessors(targetId, editor.getConnections());
+ if (nodeIds.length > 0) {
+ if (nodeIds.length === 1) {
+ editor.getNode(nodeIds[0]).setMultiConnIdx(0);
+ }
+ editor.getNode(sourceId).setMultiConnIdx(nodeIds.length);
+ }
+ }
+}
+
+export function updateMultiConnAfterRemove(editor: NodeEditor, sourceId: string, targetId: string) {
+ const source = editor.getNode(sourceId);
+ const target = editor.getNode(targetId);
+ if (target.hasVariableInputs) {
+ let i = 0;
+ const nodeIds = getPredecessors(targetId, editor.getConnections());
+ for (const nodeId of nodeIds) {
+ if (nodeId === sourceId) {
+ 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++;
+ }
+ }
+ }
+ }
+}
+
+export function getPredecessors(nodeId: string, connections: CustomConnection[]): string[] {
+ return connections.filter(c => c.target === nodeId).map(c => c.source);
+}
+
+export function getMagneticConnectionProps(editor: NodeEditor) {
+ return {
+ async createConnection(from: SocketData, to: SocketData) {
+ if (from.side === to.side) {
+ return;
+ }
+ const [source, target] = from.side === 'output' ? [from, to] : [to, from];
+ const sourceNode = editor.getNode(source.nodeId);
+ const targetNode = editor.getNode(target.nodeId);
+
+ const connection = new CustomConnection(
+ sourceNode,
+ source.key as never,
+ targetNode,
+ target.key as never,
+ 0
+ );
+
+ if (!canCreateConnection(editor, connection)) {
+ return;
+ }
+
+ const connectionsToRemove = editor.getConnections().filter(c => {
+ return (c.target === targetNode.id && c.targetInput === target.key && !targetNode.hasVariableInputs) || (c.source === sourceNode.id);
+ });
+
+ for (const c of connectionsToRemove) {
+ await editor.removeConnection(c.id);
+ }
+
+ await editor.addConnection(
+ connection
+ );
+ },
+ display(from: SocketData, to: SocketData) {
+ return from.side !== to.side && areSocketsCompatible(editor, from, to);
+ },
+ offset(socket: SocketData, position: Position) {
+
+ return {
+ x: position.x + (socket.side === 'input' ? 3 : -3),
+ y: position.y + (socket.side === 'input' ? 12 : -12)
+ };
+ },
+ distance: 75
+ };
+}
+
+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)) &&
+ !decl.notRegistered) {
+ const displayName = decl.convention ? decl.name : 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) {
+ return null;
+ }
+ return outgoing[0].target;
+}
diff --git a/src/app/components/polyalg/polyalg-viewer/alg-editor.ts b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts
new file mode 100644
index 00000000..cbeefa5f
--- /dev/null
+++ b/src/app/components/polyalg/polyalg-viewer/alg-editor.ts
@@ -0,0 +1,306 @@
+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';
+import {AngularArea2D, AngularPlugin, Presets} from 'rete-angular-plugin/17';
+import {PlanNode} from '../models/polyalg-plan.model';
+import {AutoArrangePlugin} from 'rete-auto-arrange-plugin';
+import {AlgNode, AlgNodeComponent} from '../algnode/alg-node.component';
+import {addCustomBackground} from './background';
+import {ArgControl} from '../controls/arg-control';
+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 {DataflowEngine} from 'rete-engine';
+import {Position} from 'rete-angular-plugin/17/types';
+import {Subject} from 'rxjs';
+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 {OperatorModel} from '../models/polyalg-registry';
+
+export type Schemes = GetSchemes>;
+type AreaExtra = AngularArea2D | ContextMenuExtra;
+
+export async function createEditor(container: HTMLElement, injector: Injector, registry: PolyAlgService, node: PlanNode | null,
+ planType: PlanType, isReadOnly: boolean, userMode: WritableSignal,
+ oldTransform: Transform | null) {
+
+ const readonlyPlugin = new ReadonlyPlugin();
+
+ 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: () => (
+ (p) => Transformers.classic({vertical: true})(p.reverse()) // reverse for correct UP direction
+ )
+ });
+
+ // 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() {
+ return AlgNodeComponent;
+ },
+ control(data) {
+ if (data.payload instanceof ArgControl) {
+ return data.payload.getArgComponent();
+ }
+ return null;
+ },
+ connection(data) {
+ if (data.payload.isMagnetic) {
+ return MagneticConnectionComponent;
+ }
+ return CustomConnectionComponent;
+ },
+ socket() {
+ return CustomSocketComponent;
+ }
+ },
+ socketPositionWatcher: getDOMSocketPosition({
+ offset({x, y}, nodeId, side, key) {
+ return {x, y}; // remove default shift of socket positions
+ },
+ })
+ }));
+
+ connection.addPreset(ConnectionPresets.classic.setup());
+
+ // Customizing the arrangement of nodes
+ 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'
+ };
+ }
+ };
+ });
+ const layoutOpts = {
+ 'elk.direction': 'UP'
+ };
+
+ const $modifyEvent = new Subject();
+ 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)
+ });
+
+ editor.use(readonlyPlugin.root);
+ editor.use(area);
+ editor.use(engine);
+ area.use(readonlyPlugin.area);
+ area.use(render);
+ area.use(arrange);
+ render.use(pathPlugin);
+
+ 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.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 {
+ AreaExtensions.zoomAt(area, editor.getNodes());
+ }
+
+ const modifyingEventTypes = new Set(['nodecreated', 'noderemoved', 'connectioncreated', 'connectionremoved']);
+ editor.addPipe(context => {
+ if (context.type === 'connectioncreate') {
+ if (!canCreateConnection(editor, context.data)) {
+ return;
+ }
+ updateMultiConnAfterCreate(editor, context.data.source, context.data.target);
+ } else if (context.type === 'connectionremove') {
+ updateMultiConnAfterRemove(editor, context.data.source, context.data.target);
+ }
+ if (modifyingEventTypes.has(context.type)) {
+ if (!(context.type === 'nodecreated' || context.type === 'noderemoved') || editor.getNodes().length === 1) {
+ $modifyEvent.next();
+ }
+ }
+ 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)
+ }
+
+
+ return {
+ layout: async () => {
+ await arrange.layout({
+ applier: undefined, options: layoutOpts
+ });
+ AreaExtensions.zoomAt(area, editor.getNodes());
+ },
+ destroy: () => {
+ area.destroy();
+ panningBoundary?.destroy();
+ },
+ toPolyAlg: async (): Promise<[string, OperatorModel]> => {
+ if (editor.getNodes().length === 0) {
+ return ['', null];
+ }
+ const rootId = findRootNodeId(editor.getNodes(), editor.getConnections());
+ if (rootId) {
+ engine.reset(); // clear cache
+ return await engine.fetch(rootId).then(res => [res['out'], editor.getNode(rootId).decl.model]);
+ }
+ return [null, null];
+ },
+ onModify: $modifyEvent.asObservable(),
+ showMetadata: (b: boolean) => showMetadata(editor, b),
+ getTransform: () => area.area.transform,
+ hasUnregisteredNodes: nodes.some(n => n.decl.notRegistered)
+ };
+}
+
+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 decl = registry.getDeclaration(node.opName) || registry.createDeclarationForUndef(node.opName, node.inputs, planType);
+ const algNode = new AlgNode(decl, planType, node.arguments, metadata, false, isReadOnly, updateSize);
+
+ for (let i = 0; i < node.inputs.length; i++) {
+ const [childNodes, childConnections] = addNode(registry, planType, node.inputs[i], isReadOnly, updateSize);
+ const childNode = childNodes[childNodes.length - 1];
+ nodes.push(...childNodes);
+ connections.push(...childConnections);
+
+ const targetIn = algNode.hasVariableInputs ? '0' : i.toString();
+ connections.push(new CustomConnection(childNode, 'out', algNode, targetIn, childNode.metadata?.outConnection?.width || 0));
+ }
+ nodes.push(algNode);
+ return [nodes, connections];
+}
+
+function getContextMenuItems(registry: PolyAlgService, userMode: WritableSignal, planType: PlanType,
+ isReadOnly: boolean, updateSize: (a: AlgNode, delta: Position) => void) {
+ 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;
+
+ 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);
+ }
+ };
+ }
+ return result;
+ };
+}
+
+function updateSize(algNode: AlgNode, {x, y}: Position, area: AreaPlugin,
+ 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 != null;
+ if (isReadOnly) {
+ 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();
+ 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-validator.service.ts b/src/app/components/polyalg/polyalg-viewer/alg-validator.service.ts
new file mode 100644
index 00000000..ff8d9584
--- /dev/null
+++ b/src/app/components/polyalg/polyalg-viewer/alg-validator.service.ts
@@ -0,0 +1,120 @@
+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';
+import {PlanType} from '../../../models/information-page.model';
+
+
+@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.
+ */
+ buildPlan(str: string, planType: PlanType) {
+ const cachedPlan = this.getCachedPlan(str);
+ if (cachedPlan) {
+ return of(cachedPlan);
+ }
+ if (str.trim().length === 0) {
+ return of(null);
+ }
+ return this._crud.buildTreeFromPolyAlg(str, planType).pipe(
+ tap({
+ next: (plan) => this.setValid(str, plan),
+ error: () => this.removeValid(str)
+ })
+ );
+ }
+
+ areParenthesesBalanced(str: string) {
+ const stack: string[] = [];
+
+ 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;
+ }
+ }
+ }
+ }
+
+ 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
new file mode 100644
index 00000000..28e58852
--- /dev/null
+++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.html
@@ -0,0 +1,125 @@
+
+
+
+
+ Algebra {{isReadOnly ? 'Viewer' : 'Editor'}} {{stateText[textEditorState()]}}
+
+
+ {{textEditorError()}}
+
+
+
+
+
+ PolyAlgebra can only be edited in advanced mode .
+
+
+
+
+
+
+ Edit Plan
+
+
+
+
+ Toggle Stats
+
+
+
+
+
+
+
+
+ Simple
+
+
+
+
+ Advanced
+
+
+
+
+ Arrange Nodes
+
+
+ Synchronize
+
+
+ Clear Plan
+
+
+
+ Execute Plan
+
+
+
+
+
+
+
+
+
+ Node {{isReadOnly ? 'Viewer' : 'Editor'}} {{stateText[nodeEditorState()]}}
+
+ {{nodeEditorError()}}
+
+
+
+
+
+
+
+ Open the context menu to add or remove nodes
+
+
+
+
+
+
+
+
+
+ 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.
+
+
+ Close
+
+
+ Edit in New Tab
+
+ Edit Plan
+
+
+
\ 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
new file mode 100644
index 00000000..c8c52d39
--- /dev/null
+++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.scss
@@ -0,0 +1,56 @@
+@import "scss/style.scss";
+
+.rete {
+ min-height: 800px;
+ border: 2px solid;
+}
+
+.text-editor-wrapper {
+ border: 2px solid;
+}
+
+
+.SYNCHRONIZED {
+ border-color: lightblue;
+}
+
+// READONLY must be above CHANGED and INVALID. This way CHANGED and INVALID take priority
+.READONLY {
+ border-color: $secondary;
+}
+
+button.accordion-button.READONLY {
+ background-color: $secondary !important;
+}
+
+.CHANGED {
+ border-color: $warning-50;
+}
+
+button.accordion-button.CHANGED {
+ background-color: $warning-50 !important;
+}
+
+.INVALID {
+ 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;
+}
+
+.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
new file mode 100644
index 00000000..0647a8d2
--- /dev/null
+++ b/src/app/components/polyalg/polyalg-viewer/alg-viewer.component.ts
@@ -0,0 +1,367 @@
+import {AfterViewInit, Component, computed, effect, ElementRef, EventEmitter, Injector, Input, OnChanges, OnDestroy, Output, signal, SimpleChanges, untracked, ViewChild} from '@angular/core';
+import {createEditor, UserMode} 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';
+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',
+ styleUrl: './alg-viewer.component.scss'
+})
+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]>();
+ @ViewChild('rete') container!: ElementRef;
+ @ViewChild('textEditor') textEditor: EditorComponent;
+ @ViewChild('itemText') textAccordionItem: AccordionItemComponent;
+
+ private polyAlgPlan = signal(undefined); // null: empty plan
+ private planTypeSignal = signal(undefined); // we need an additional signal to automatically execute the effect
+ userMode = signal(UserMode.SIMPLE);
+ textEditorState = signal('SYNCHRONIZED');
+ textEditorError = signal('');
+ textEditorIsLocked = computed(() => this.userMode() === UserMode.SIMPLE && !this.isReadOnly); // only relevant when plan is editable
+ 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')
+ );
+ isSynchronized = computed(() => this.nodeEditorState() === 'SYNCHRONIZED' && this.textEditorState() === 'SYNCHRONIZED');
+ showEditButton: boolean;
+ showEditModal = signal(false);
+
+ private modifySubscription: Subscription;
+ 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;
+ 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
+ readonly textEditorOpts = {
+ minLines: 12,
+ maxLines: 12,
+ showLineNumbers: false,
+ highlightGutterLine: false,
+ highlightActiveLine: true,
+ 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.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(() => {
+ this.nodeEditorState.set('CHANGED');
+ return timer(500);
+ })
+ ).subscribe(() => this.generateTextFromNodeEditor(true));
+ });
+ });
+ }
+ });
+ }
+
+ ngAfterViewInit(): void {
+ this.showEditButton = this.isReadOnly;
+ this.showMetadata = this.isReadOnly;
+
+ if (this.initialUserMode) {
+ this.userMode.set(this.initialUserMode);
+ this.textAccordionItem.visible = this.initialUserMode === UserMode.ADVANCED;
+ }
+
+ this.textEditor.setScrollMargin(5, 5);
+ if (!this.isReadOnly) {
+ this.textEditor.setReadOnly(this.userMode() === UserMode.SIMPLE);
+ this.textEditor.onBlur(() => this.onTextEditorBlur());
+ this.textEditor.onChange(() => this.onTextEditorChange());
+ }
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes.polyAlg) {
+ if (this.polyAlg == null) {
+ return;
+ }
+ this.skipOldTransformReuse = true;
+ this._validator.buildPlan(this.polyAlg, this.planType).subscribe({
+ next: (plan) => this.polyAlgPlan.set(plan),
+ 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.');
+ }
+ });
+ }
+
+ 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);
+ }
+
+ if (changes.initialPlan && !this.polyAlg && !this.polyAlgPlan()) {
+ this.polyAlgPlan.set(JSON.parse(this.initialPlan));
+ }
+ }
+
+ ngOnDestroy() {
+ this.modifySubscription?.unsubscribe();
+ this.nodeEditor?.destroy();
+ }
+
+ generateTextFromNodeEditor(validatePlanWithBackend = false) {
+ this.getPolyAlgFromTree().then(([str]) => {
+ if (str != null) {
+ if (!this.initialPolyAlg) {
+ this.initialPolyAlg = str;
+ }
+ if (validatePlanWithBackend && !this._validator.isConfirmedValid(str)) {
+ this._validator.buildPlan(str, this.planTypeSignal()).subscribe({
+ next: () => this.updateTextEditor(str),
+ error: (err) => {
+ this.nodeEditorState.set('INVALID');
+ this.nodeEditorError.set(err.error.errorMsg);
+ }
+ });
+ } else {
+ this.updateTextEditor(str);
+ }
+ } else {
+ this.nodeEditorState.set('INVALID');
+ this.nodeEditorError.set('Invalid plan structure');
+ }
+ });
+ }
+
+ 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, this.planTypeSignal()).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.textEditorError.set(err.error.errorMsg);
+ }
+ });
+
+ }
+
+ getPolyAlgFromTree(): Promise<[string, OperatorModel]> {
+ return this.nodeEditor.toPolyAlg();
+ }
+
+ private onTextEditorBlur() {
+ if (this.isReadOnly || !this.textAccordionItem.visible) {
+ 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 Algebra');
+ return;
+ }
+
+ this.textEditorState.set('CHANGED');
+ this.generateNodesFromTextEditor(updatedPolyAlg);
+
+ this.polyAlgSnapshot = trimmed;
+ }
+
+ private onTextEditorChange() {
+ if (this.isReadOnly || !this.textAccordionItem.visible) {
+ return;
+ }
+ const trimmed = trimLines(this.textEditor.getCode());
+ if (trimmed === '' && this.polyAlgSnapshot !== '') {
+ return; // this happens when the textAccordion is expanded. We do not want to override the state
+ }
+ if (trimmed !== this.polyAlgSnapshot) {
+ this.textEditorState.set('CHANGED');
+ } else if (this.textEditorState() !== 'INVALID') {
+ this.textEditorState.set('SYNCHRONIZED');
+ }
+ }
+
+
+ onNodeEditorBlur() {
+ if (this.isReadOnly) {
+ return;
+ }
+ this.isNodeFocused = false;
+
+ // Wait a short amount of time for ng-autocomplete to update the value when selecting a suggestion
+ setTimeout(() => this.generateTextFromNodeEditor(true), 100);
+ }
+
+ onNodeEditorFocus() {
+ if (this.isReadOnly) {
+ return;
+ }
+ this.isNodeFocused = true;
+ this.nodeEditorState.set('CHANGED');
+ }
+
+ synchronizeEditors() {
+ if (this.textEditorState() === 'INVALID') {
+ this.generateTextFromNodeEditor();
+ } else {
+ this.generateNodesFromTextEditor();
+ }
+ }
+
+ openInPlanEditor(forced = false, newTab = false) {
+ if (!this.isReadOnly) {
+ return;
+ }
+ if (this.nodeEditor.hasUnregisteredNodes) {
+ this._toast.warn('Plan contains unregistered operators', 'Cannot edit plan');
+ 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());
+ 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() {
+ this.getPolyAlgFromTree().then(([str, model]) => {
+ if (str.length > 0 && model) {
+ this.execute.emit([str, model]);
+ }
+ });
+ }
+
+ setUserMode(mode: UserMode) {
+ this.userMode.set(mode);
+ if (!this.isReadOnly) {
+ const isSimple = mode === UserMode.SIMPLE;
+ this.textEditor.setReadOnly(isSimple);
+ this.textAccordionItem.visible = !isSimple;
+ this.refreshTextEditor();
+ }
+ }
+
+ toggleTextEditor() {
+ this.textAccordionItem.visible = !this.textAccordionItem.visible;
+ this.refreshTextEditor();
+ }
+
+ refreshTextEditor() {
+ // If the editor is hidden when setCode is used, the visible text is not correctly updated.
+ // We fix this by forcing it to render again.
+ this.textEditor.setCode(this.textEditor.getCode());
+ }
+
+ toggleMetadata() {
+ // 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/components/polyalg/polyalg-viewer/background.ts b/src/app/components/polyalg/polyalg-viewer/background.ts
new file mode 100644
index 00000000..9824db80
--- /dev/null
+++ b/src/app/components/polyalg/polyalg-viewer/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);
+}
diff --git a/src/app/components/polyalg/polyalg-viewer/magnetic-connection/index.ts b/src/app/components/polyalg/polyalg-viewer/magnetic-connection/index.ts
new file mode 100644
index 00000000..82203ef5
--- /dev/null
+++ b/src/app/components/polyalg/polyalg-viewer/magnetic-connection/index.ts
@@ -0,0 +1,109 @@
+import {NodeEditor} from 'rete';
+import {Area2D, AreaPlugin} from 'rete-area-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';
+import {getNodeRect} from './utils';
+
+interface Props {
+ createConnection: (from: SocketData, to: SocketData) => Promise;
+ display: (from: SocketData, to: SocketData) => boolean;
+ offset: (socket: SocketData, position: Position) => Position;
+ margin?: number;
+ distance?: number;
+}
+
+// https://retejs.org/examples/magnetic-connection
+export function useMagneticConnection(connection: ConnectionPlugin, props: Props) {
+ const area = connection.parentScope>>(AreaPlugin);
+ const editor = area.parentScope>(NodeEditor);
+ const sockets = new Map();
+ const magneticConnection = createPseudoconnection>({
+ isMagnetic: true
+ });
+ const margin = typeof props.margin !== 'undefined' ? props.margin : 50;
+ const distance = typeof props.distance !== 'undefined' ? props.distance : 50;
+
+ let picked: null | SocketData = null;
+ let nearestSocket: null | (SocketData & Position) = null;
+
+ (connection as ConnectionPlugin).addPipe(async (context) => {
+ if (!context || typeof context !== 'object' || !('type' in context)) {
+ return context;
+ }
+
+ if (context.type === 'connectionpick') {
+ picked = context.data.socket;
+ } else if (context.type === 'connectiondrop') {
+ if (nearestSocket && !context.data.created) {
+ await props.createConnection(context.data.initial, nearestSocket);
+ }
+
+ picked = null;
+ magneticConnection.unmount(area);
+ } else if (context.type === 'pointermove') {
+ if (!picked) {
+ return context;
+ }
+ const point = context.data.position;
+ const nodes = Array.from(area.nodeViews.entries());
+ const socketsList = Array.from(sockets.values());
+
+ const rects = nodes.map(([id, view]) => ({
+ id,
+ ...getNodeRect(editor.getNode(id), view)
+ }));
+ const nearestRects = rects.filter((rect) =>
+ isInsideRect(rect, point, margin)
+ );
+ const nearestNodes = nearestRects.map(({id}) => id);
+ const nearestSockets = socketsList.filter((item) =>
+ nearestNodes.includes(item.nodeId)
+ );
+ const socketsPositions = await Promise.all(
+ nearestSockets.map(async (socket) => {
+ const nodeView = area.nodeViews.get(socket.nodeId);
+
+ if (!nodeView) {
+ throw new Error('node view');
+ }
+
+ const {x, y} = await getElementCenter(
+ socket.element,
+ nodeView.element
+ );
+
+ return {
+ ...socket,
+ x: x + nodeView.position.x,
+ y: y + nodeView.position.y
+ };
+ })
+ );
+ nearestSocket =
+ findNearestPoint(socketsPositions, point, distance) || null;
+ if (nearestSocket && props.display(picked, nearestSocket)) {
+ if (!magneticConnection.isMounted()) {
+ magneticConnection.mount(area);
+ }
+ const {x, y} = nearestSocket;
+
+ magneticConnection.render(
+ area,
+ props.offset(nearestSocket, {x, y}),
+ picked
+ );
+ } else if (magneticConnection.isMounted()) {
+ magneticConnection.unmount(area);
+ }
+ } else if (context.type === 'render' && context.data.type === 'socket') {
+ const {element} = context.data;
+
+ sockets.set(element, context.data);
+ } else if (context.type === 'unmount') {
+ sockets.delete(context.data.element);
+ }
+ return context;
+ });
+}
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
new file mode 100644
index 00000000..a484b50f
--- /dev/null
+++ b/src/app/components/polyalg/polyalg-viewer/magnetic-connection/magnetic-connection.component.scss
@@ -0,0 +1,17 @@
+@import "scss/style.scss";
+
+svg {
+ overflow: visible !important;
+ position: absolute;
+ pointer-events: none;
+ width: 9999px;
+ height: 9999px;
+
+ path {
+ fill: none;
+ stroke-width: 5px;
+ stroke: rgba($primary, 0.5);
+ stroke-dasharray: 10 10;
+ pointer-events: auto;
+ }
+}
diff --git a/src/app/components/polyalg/polyalg-viewer/magnetic-connection/magnetic-connection.component.ts b/src/app/components/polyalg/polyalg-viewer/magnetic-connection/magnetic-connection.component.ts
new file mode 100644
index 00000000..34b680e7
--- /dev/null
+++ b/src/app/components/polyalg/polyalg-viewer/magnetic-connection/magnetic-connection.component.ts
@@ -0,0 +1,19 @@
+import {Component, Input} from '@angular/core';
+import Popper from 'popper.js';
+import Position = Popper.Position;
+
+@Component({
+ selector: 'app-magnetic-connection',
+ template: `
+
+
+
+ `,
+ styleUrl: './magnetic-connection.component.scss'
+})
+export class MagneticConnectionComponent {
+ @Input() data: any;
+ @Input() start: Position;
+ @Input() end: Position;
+ @Input() path: string;
+}
diff --git a/src/app/components/polyalg/polyalg-viewer/magnetic-connection/math.ts b/src/app/components/polyalg/polyalg-viewer/magnetic-connection/math.ts
new file mode 100644
index 00000000..640aaf83
--- /dev/null
+++ b/src/app/components/polyalg/polyalg-viewer/magnetic-connection/math.ts
@@ -0,0 +1,25 @@
+import {Position, Rect} from './types';
+
+export function findNearestPoint(
+ points: T[],
+ target: Position,
+ maxDistance: number
+) {
+ return points.reduce((nearestPoint, point) => {
+ const distance = Math.sqrt(
+ (point.x - target.x) ** 2 + (point.y - target.y) ** 2
+ );
+
+ if (distance > maxDistance) return nearestPoint;
+ if (nearestPoint === null || distance < nearestPoint.distance)
+ return { point, distance };
+ return nearestPoint;
+ }, null as null | { point: T; distance: number })?.point;
+}
+
+export function isInsideRect(rect: Rect, point: Position, margin: number) {
+ return point.y > rect.top - margin &&
+ point.x > rect.left - margin &&
+ point.x < rect.right + margin &&
+ point.y < rect.bottom + margin;
+}
diff --git a/src/app/components/polyalg/polyalg-viewer/magnetic-connection/types.ts b/src/app/components/polyalg/polyalg-viewer/magnetic-connection/types.ts
new file mode 100644
index 00000000..bc863a15
--- /dev/null
+++ b/src/app/components/polyalg/polyalg-viewer/magnetic-connection/types.ts
@@ -0,0 +1,12 @@
+import { ClassicPreset, GetSchemes } from 'rete';
+
+export interface Position { x: number; y: number; }
+export interface Rect { left: number; top: number; right: number; bottom: number; }
+
+export type Node = ClassicPreset.Node & { width: number; height: number };
+export type Connection<
+ A extends Node,
+ B extends Node
+> = ClassicPreset.Connection & { isMagnetic?: boolean };
+
+export type Schemes = GetSchemes>;
diff --git a/src/app/components/polyalg/polyalg-viewer/magnetic-connection/utils.ts b/src/app/components/polyalg/polyalg-viewer/magnetic-connection/utils.ts
new file mode 100644
index 00000000..e7b86386
--- /dev/null
+++ b/src/app/components/polyalg/polyalg-viewer/magnetic-connection/utils.ts
@@ -0,0 +1,15 @@
+import { NodeView } from 'rete-area-plugin';
+import { Node } from './types';
+
+export function getNodeRect(node: Node, view: NodeView) {
+ const {
+ position: { x, y }
+ } = view;
+
+ return {
+ left: x,
+ top: y,
+ right: x + node.width,
+ bottom: y + node.height
+ };
+}
diff --git a/src/app/components/polyalg/polyalg-viewer/panning-boundary/frame.ts b/src/app/components/polyalg/polyalg-viewer/panning-boundary/frame.ts
new file mode 100644
index 00000000..8a3bab1d
--- /dev/null
+++ b/src/app/components/polyalg/polyalg-viewer/panning-boundary/frame.ts
@@ -0,0 +1,22 @@
+function getWeight(value: number, padding: number) {
+ return Math.min(1, -Math.min(0, value / padding - 1));
+}
+
+export function getFrameWeight(
+ x: number,
+ y: number,
+ frame: DOMRect,
+ padding: number
+) {
+ const top = getWeight(y - frame.top, padding);
+ const bottom = getWeight(frame.bottom - y, padding);
+ const left = getWeight(x - frame.left, padding);
+ const right = getWeight(frame.right - x, padding);
+
+ return {
+ top,
+ bottom,
+ left,
+ right
+ };
+}
diff --git a/src/app/components/polyalg/polyalg-viewer/panning-boundary/index.ts b/src/app/components/polyalg/polyalg-viewer/panning-boundary/index.ts
new file mode 100644
index 00000000..d60638a5
--- /dev/null
+++ b/src/app/components/polyalg/polyalg-viewer/panning-boundary/index.ts
@@ -0,0 +1,79 @@
+import {NodeEditor} from 'rete';
+import {AreaExtensions, AreaPlugin} from 'rete-area-plugin';
+import {getFrameWeight} from './frame';
+import {animate, watchPointerMove} from './utils';
+import {Schemes} from '../alg-editor';
+
+interface Props {
+ area: AreaPlugin;
+ selector: AreaExtensions.Selector;
+ intensity?: number;
+ padding?: number;
+}
+
+// from https://retejs.org/examples/panning-boundary
+export function setupPanningBoundary(props: Props) {
+ const selector = props.selector;
+ const padding = props.padding ?? 30;
+ const intensity = props.intensity ?? 2;
+ const area = props.area;
+ const editor = area.parentScope(NodeEditor);
+ const pointermove = watchPointerMove();
+ const ticker = animate(async () => {
+ const {clientX, clientY, pageX, pageY} = pointermove.getEvent();
+ const weights = getFrameWeight(
+ clientX,
+ clientY,
+ area.container.getBoundingClientRect(),
+ padding
+ );
+ const velocity = {
+ x: (weights.left - weights.right) * intensity,
+ y: (weights.top - weights.bottom) * intensity,
+ };
+
+ const pickedNode = editor
+ .getNodes()
+ .find((n) => selector.isPicked({label: 'node', id: n.id}));
+ const view = pickedNode && area.nodeViews.get(pickedNode.id);
+
+ if (!view) {
+ return;
+ }
+
+ const {dragHandler, position} = view;
+
+ (dragHandler as any).pointerStart = {
+ x: pageX + velocity.x,
+ y: pageY + velocity.y,
+ };
+ (dragHandler as any).startPosition = {
+ ...(dragHandler as any).config.getCurrentPosition(),
+ };
+
+ const {transform} = area.area;
+ const x = position.x - velocity.x / transform.k;
+ const y = position.y - velocity.y / transform.k;
+
+ await Promise.all([
+ area.area.translate(transform.x + velocity.x, transform.y + velocity.y),
+ area.translate(pickedNode.id, {x, y}),
+ ]);
+ });
+
+ area.addPipe((context) => {
+ if (context.type === 'nodepicked') {
+ ticker.start();
+ }
+ if (context.type === 'nodedragged') {
+ ticker.stop();
+ }
+ return context;
+ });
+
+ return {
+ destroy() {
+ pointermove.destroy();
+ },
+ };
+}
diff --git a/src/app/components/polyalg/polyalg-viewer/panning-boundary/utils.ts b/src/app/components/polyalg/polyalg-viewer/panning-boundary/utils.ts
new file mode 100644
index 00000000..2c72b65b
--- /dev/null
+++ b/src/app/components/polyalg/polyalg-viewer/panning-boundary/utils.ts
@@ -0,0 +1,46 @@
+export function watchPointerMove() {
+ let moveEvent: PointerEvent | null = null;
+
+ function pointermove(e: PointerEvent) {
+ moveEvent = e;
+ }
+
+ window.addEventListener('pointermove', pointermove);
+
+ return {
+ getEvent() {
+ if (!moveEvent) {
+ throw new Error('no event captured');
+ }
+ return moveEvent;
+ },
+ destroy() {
+ window.removeEventListener('pointermove', pointermove);
+ }
+ };
+}
+
+export function animate(handle: () => void | Promise) {
+ let id = 0;
+
+ function start() {
+ id = requestAnimationFrame(async () => {
+ try {
+ await handle();
+ } catch (e) {
+ console.error(e);
+ } finally {
+ start();
+ }
+ });
+ }
+
+ function stop() {
+ cancelAnimationFrame(id);
+ }
+
+ return {
+ start,
+ stop
+ };
+}
diff --git a/src/app/components/polyalg/polyalg.service.ts b/src/app/components/polyalg/polyalg.service.ts
new file mode 100644
index 00000000..4dff2332
--- /dev/null
+++ b/src/app/components/polyalg/polyalg.service.ts
@@ -0,0 +1,88 @@
+import {Injectable, signal} from '@angular/core';
+import {CrudService} from '../../services/crud.service';
+import {Declaration, OperatorModel, OperatorTag, Parameter, PolyAlgRegistry} from './models/polyalg-registry';
+import {PlanNode} from './models/polyalg-plan.model';
+import {PlanType} from '../../models/information-page.model';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class PolyAlgService {
+
+ 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() {
+ 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);
+ }
+
+ createDeclarationForUndef(opName: string, inputs: PlanNode[], planType: PlanType): Declaration {
+ return {
+ name: opName,
+ aliases: [],
+ model: OperatorModel.COMMON,
+ numInputs: inputs.length,
+ tags: [OperatorTag.ADVANCED, OperatorTag[planType]],
+ posParams: [],
+ kwParams: [],
+ notRegistered: true
+ };
+ }
+
+ getSortedDeclarations(model: OperatorModel = 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);
+ }
+
+ isSimpleOperator(opName: string) {
+ return !this.declarations.get(opName).tags.includes(OperatorTag.ADVANCED);
+ }
+}
diff --git a/src/app/containers/default-layout/default-layout.component.html b/src/app/containers/default-layout/default-layout.component.html
index e0bd30c0..68064b21 100644
--- a/src/app/containers/default-layout/default-layout.component.html
+++ b/src/app/containers/default-layout/default-layout.component.html
@@ -52,7 +52,7 @@
-
Plan Builder
diff --git a/src/app/explain-visualizer/assets/css/font-awesome.css b/src/app/explain-visualizer/assets/css/font-awesome.css
deleted file mode 100644
index a499a1ac..00000000
--- a/src/app/explain-visualizer/assets/css/font-awesome.css
+++ /dev/null
@@ -1,2721 +0,0 @@
-/*!
- * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome
- * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
- */
-
-@font-face {
- font-family: 'FontAwesome';
- src: url("../fonts/fontawesome-webfont.eot?v=4.5.0");
- src: url("../fonts/fontawesome-webfont.eot?#iefix&v=4.5.0") format("embedded-opentype"), url("../fonts/fontawesome-webfont.woff2?v=4.5.0") format("woff2"), url("../fonts/fontawesome-webfont.woff?v=4.5.0") format("woff"), url("../fonts/fontawesome-webfont.ttf?v=4.5.0") format("truetype");
- font-weight: normal;
- font-style: normal
-}
-
-.fa {
- display: inline-block;
- font: normal normal normal 14px/1 FontAwesome;
- font-size: inherit;
- text-rendering: auto;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-.fa-lg {
- font-size: 1.33333em;
- line-height: .75em;
- vertical-align: -15%
-}
-
-.fa-2x {
- font-size: 2em
-}
-
-.fa-3x {
- font-size: 3em
-}
-
-.fa-4x {
- font-size: 4em
-}
-
-.fa-5x {
- font-size: 5em
-}
-
-.fa-fw {
- width: 1.28571em;
- text-align: center
-}
-
-.fa-ul {
- padding-left: 0;
- margin-left: 2.14286em;
- list-style-type: none
-}
-
-.fa-ul > li {
- position: relative
-}
-
-.fa-li {
- position: absolute;
- left: -2.14286em;
- width: 2.14286em;
- top: .14286em;
- text-align: center
-}
-
-.fa-li.fa-lg {
- left: -1.85714em
-}
-
-.fa-border {
- padding: .2em .25em .15em;
- border: solid 0.08em #eee;
- border-radius: .1em
-}
-
-.fa-pull-left {
- float: left
-}
-
-.fa-pull-right {
- float: right
-}
-
-.fa.fa-pull-left {
- margin-right: .3em
-}
-
-.fa.fa-pull-right {
- margin-left: .3em
-}
-
-.pull-right {
- float: right
-}
-
-.pull-left {
- float: left
-}
-
-.fa.pull-left {
- margin-right: .3em
-}
-
-.fa.pull-right {
- margin-left: .3em
-}
-
-.fa-spin {
- -webkit-animation: fa-spin 2s infinite linear;
- animation: fa-spin 2s infinite linear
-}
-
-.fa-pulse {
- -webkit-animation: fa-spin 1s infinite steps(8);
- animation: fa-spin 1s infinite steps(8)
-}
-
-@-webkit-keyframes fa-spin {
- 0% {
- -webkit-transform: rotate(0deg);
- transform: rotate(0deg)
- }
- 100% {
- -webkit-transform: rotate(359deg);
- transform: rotate(359deg)
- }
-}
-
-@keyframes fa-spin {
- 0% {
- -webkit-transform: rotate(0deg);
- transform: rotate(0deg)
- }
- 100% {
- -webkit-transform: rotate(359deg);
- transform: rotate(359deg)
- }
-}
-
-.fa-rotate-90 {
- filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1);
- -webkit-transform: rotate(90deg);
- -ms-transform: rotate(90deg);
- transform: rotate(90deg)
-}
-
-.fa-rotate-180 {
- filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2);
- -webkit-transform: rotate(180deg);
- -ms-transform: rotate(180deg);
- transform: rotate(180deg)
-}
-
-.fa-rotate-270 {
- filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3);
- -webkit-transform: rotate(270deg);
- -ms-transform: rotate(270deg);
- transform: rotate(270deg)
-}
-
-.fa-flip-horizontal {
- filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0);
- -webkit-transform: scale(-1, 1);
- -ms-transform: scale(-1, 1);
- transform: scale(-1, 1)
-}
-
-.fa-flip-vertical {
- filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2);
- -webkit-transform: scale(1, -1);
- -ms-transform: scale(1, -1);
- transform: scale(1, -1)
-}
-
-:root .fa-rotate-90,
-:root .fa-rotate-180,
-:root .fa-rotate-270,
-:root .fa-flip-horizontal,
-:root .fa-flip-vertical {
- filter: none
-}
-
-.fa-stack {
- position: relative;
- display: inline-block;
- width: 2em;
- height: 2em;
- line-height: 2em;
- vertical-align: middle
-}
-
-.fa-stack-1x,
-.fa-stack-2x {
- position: absolute;
- left: 0;
- width: 100%;
- text-align: center
-}
-
-.fa-stack-1x {
- line-height: inherit
-}
-
-.fa-stack-2x {
- font-size: 2em
-}
-
-.fa-inverse {
- color: #fff
-}
-
-.fa-glass:before {
- content: ""
-}
-
-.fa-music:before {
- content: ""
-}
-
-.fa-search:before {
- content: ""
-}
-
-.fa-envelope-o:before {
- content: ""
-}
-
-.fa-heart:before {
- content: ""
-}
-
-.fa-star:before {
- content: ""
-}
-
-.fa-star-o:before {
- content: ""
-}
-
-.fa-user:before {
- content: ""
-}
-
-.fa-film:before {
- content: ""
-}
-
-.fa-th-large:before {
- content: ""
-}
-
-.fa-th:before {
- content: ""
-}
-
-.fa-th-list:before {
- content: ""
-}
-
-.fa-check:before {
- content: ""
-}
-
-.fa-remove:before,
-.fa-close:before,
-.fa-times:before {
- content: ""
-}
-
-.fa-search-plus:before {
- content: ""
-}
-
-.fa-search-minus:before {
- content: ""
-}
-
-.fa-power-off:before {
- content: ""
-}
-
-.fa-signal:before {
- content: ""
-}
-
-.fa-gear:before,
-.fa-cog:before {
- content: ""
-}
-
-.fa-trash-o:before {
- content: ""
-}
-
-.fa-home:before {
- content: ""
-}
-
-.fa-file-o:before {
- content: ""
-}
-
-.fa-clock-o:before {
- content: ""
-}
-
-.fa-road:before {
- content: ""
-}
-
-.fa-download:before {
- content: ""
-}
-
-.fa-arrow-circle-o-down:before {
- content: ""
-}
-
-.fa-arrow-circle-o-up:before {
- content: ""
-}
-
-.fa-inbox:before {
- content: ""
-}
-
-.fa-play-circle-o:before {
- content: ""
-}
-
-.fa-rotate-right:before,
-.fa-repeat:before {
- content: ""
-}
-
-.fa-refresh:before {
- content: ""
-}
-
-.fa-list-alt:before {
- content: ""
-}
-
-.fa-lock:before {
- content: ""
-}
-
-.fa-flag:before {
- content: ""
-}
-
-.fa-headphones:before {
- content: ""
-}
-
-.fa-volume-off:before {
- content: ""
-}
-
-.fa-volume-down:before {
- content: ""
-}
-
-.fa-volume-up:before {
- content: ""
-}
-
-.fa-qrcode:before {
- content: ""
-}
-
-.fa-barcode:before {
- content: ""
-}
-
-.fa-tag:before {
- content: ""
-}
-
-.fa-tags:before {
- content: ""
-}
-
-.fa-book:before {
- content: ""
-}
-
-.fa-bookmark:before {
- content: ""
-}
-
-.fa-print:before {
- content: ""
-}
-
-.fa-camera:before {
- content: ""
-}
-
-.fa-font:before {
- content: ""
-}
-
-.fa-bold:before {
- content: ""
-}
-
-.fa-italic:before {
- content: ""
-}
-
-.fa-text-height:before {
- content: ""
-}
-
-.fa-text-width:before {
- content: ""
-}
-
-.fa-align-left:before {
- content: ""
-}
-
-.fa-align-center:before {
- content: ""
-}
-
-.fa-align-right:before {
- content: ""
-}
-
-.fa-align-justify:before {
- content: ""
-}
-
-.fa-list:before {
- content: ""
-}
-
-.fa-dedent:before,
-.fa-outdent:before {
- content: ""
-}
-
-.fa-indent:before {
- content: ""
-}
-
-.fa-video-camera:before {
- content: ""
-}
-
-.fa-photo:before,
-.fa-image:before,
-.fa-picture-o:before {
- content: ""
-}
-
-.fa-pencil:before {
- content: ""
-}
-
-.fa-map-marker:before {
- content: ""
-}
-
-.fa-adjust:before {
- content: ""
-}
-
-.fa-tint:before {
- content: ""
-}
-
-.fa-edit:before,
-.fa-pencil-square-o:before {
- content: ""
-}
-
-.fa-share-square-o:before {
- content: ""
-}
-
-.fa-check-square-o:before {
- content: ""
-}
-
-.fa-arrows:before {
- content: ""
-}
-
-.fa-step-backward:before {
- content: ""
-}
-
-.fa-fast-backward:before {
- content: ""
-}
-
-.fa-backward:before {
- content: ""
-}
-
-.fa-play:before {
- content: ""
-}
-
-.fa-pause:before {
- content: ""
-}
-
-.fa-stop:before {
- content: ""
-}
-
-.fa-forward:before {
- content: ""
-}
-
-.fa-fast-forward:before {
- content: ""
-}
-
-.fa-step-forward:before {
- content: ""
-}
-
-.fa-eject:before {
- content: ""
-}
-
-.fa-chevron-left:before {
- content: ""
-}
-
-.fa-chevron-right:before {
- content: ""
-}
-
-.fa-plus-circle:before {
- content: ""
-}
-
-.fa-minus-circle:before {
- content: ""
-}
-
-.fa-times-circle:before {
- content: ""
-}
-
-.fa-check-circle:before {
- content: ""
-}
-
-.fa-question-circle:before {
- content: ""
-}
-
-.fa-info-circle:before {
- content: ""
-}
-
-.fa-crosshairs:before {
- content: ""
-}
-
-.fa-times-circle-o:before {
- content: ""
-}
-
-.fa-check-circle-o:before {
- content: ""
-}
-
-.fa-ban:before {
- content: ""
-}
-
-.fa-arrow-left:before {
- content: ""
-}
-
-.fa-arrow-right:before {
- content: ""
-}
-
-.fa-arrow-up:before {
- content: ""
-}
-
-.fa-arrow-down:before {
- content: ""
-}
-
-.fa-mail-forward:before,
-.fa-share:before {
- content: ""
-}
-
-.fa-expand:before {
- content: ""
-}
-
-.fa-compress:before {
- content: ""
-}
-
-.fa-plus:before {
- content: ""
-}
-
-.fa-minus:before {
- content: ""
-}
-
-.fa-asterisk:before {
- content: ""
-}
-
-.fa-exclamation-circle:before {
- content: ""
-}
-
-.fa-gift:before {
- content: ""
-}
-
-.fa-leaf:before {
- content: ""
-}
-
-.fa-fire:before {
- content: ""
-}
-
-.fa-eye:before {
- content: ""
-}
-
-.fa-eye-slash:before {
- content: ""
-}
-
-.fa-warning:before,
-.fa-exclamation-triangle:before {
- content: ""
-}
-
-.fa-plane:before {
- content: ""
-}
-
-.fa-calendar:before {
- content: ""
-}
-
-.fa-random:before {
- content: ""
-}
-
-.fa-comment:before {
- content: ""
-}
-
-.fa-magnet:before {
- content: ""
-}
-
-.fa-chevron-up:before {
- content: ""
-}
-
-.fa-chevron-down:before {
- content: ""
-}
-
-.fa-retweet:before {
- content: ""
-}
-
-.fa-shopping-cart:before {
- content: ""
-}
-
-.fa-folder:before {
- content: ""
-}
-
-.fa-folder-open:before {
- content: ""
-}
-
-.fa-arrows-v:before {
- content: ""
-}
-
-.fa-arrows-h:before {
- content: ""
-}
-
-.fa-bar-chart-o:before,
-.fa-bar-chart:before {
- content: ""
-}
-
-.fa-twitter-square:before {
- content: ""
-}
-
-.fa-facebook-square:before {
- content: ""
-}
-
-.fa-camera-retro:before {
- content: ""
-}
-
-.fa-key:before {
- content: ""
-}
-
-.fa-gears:before,
-.fa-cogs:before {
- content: ""
-}
-
-.fa-comments:before {
- content: ""
-}
-
-.fa-thumbs-o-up:before {
- content: ""
-}
-
-.fa-thumbs-o-down:before {
- content: ""
-}
-
-.fa-star-half:before {
- content: ""
-}
-
-.fa-heart-o:before {
- content: ""
-}
-
-.fa-sign-out:before {
- content: ""
-}
-
-.fa-linkedin-square:before {
- content: ""
-}
-
-.fa-thumb-tack:before {
- content: ""
-}
-
-.fa-external-link:before {
- content: ""
-}
-
-.fa-sign-in:before {
- content: ""
-}
-
-.fa-trophy:before {
- content: ""
-}
-
-.fa-github-square:before {
- content: ""
-}
-
-.fa-upload:before {
- content: ""
-}
-
-.fa-lemon-o:before {
- content: ""
-}
-
-.fa-phone:before {
- content: ""
-}
-
-.fa-square-o:before {
- content: ""
-}
-
-.fa-bookmark-o:before {
- content: ""
-}
-
-.fa-phone-square:before {
- content: ""
-}
-
-.fa-twitter:before {
- content: ""
-}
-
-.fa-facebook-f:before,
-.fa-facebook:before {
- content: ""
-}
-
-.fa-github:before {
- content: ""
-}
-
-.fa-unlock:before {
- content: ""
-}
-
-.fa-credit-card:before {
- content: ""
-}
-
-.fa-feed:before,
-.fa-rss:before {
- content: ""
-}
-
-.fa-hdd-o:before {
- content: ""
-}
-
-.fa-bullhorn:before {
- content: ""
-}
-
-.fa-bell:before {
- content: ""
-}
-
-.fa-certificate:before {
- content: ""
-}
-
-.fa-hand-o-right:before {
- content: ""
-}
-
-.fa-hand-o-left:before {
- content: ""
-}
-
-.fa-hand-o-up:before {
- content: ""
-}
-
-.fa-hand-o-down:before {
- content: ""
-}
-
-.fa-arrow-circle-left:before {
- content: ""
-}
-
-.fa-arrow-circle-right:before {
- content: ""
-}
-
-.fa-arrow-circle-up:before {
- content: ""
-}
-
-.fa-arrow-circle-down:before {
- content: ""
-}
-
-.fa-globe:before {
- content: ""
-}
-
-.fa-wrench:before {
- content: ""
-}
-
-.fa-tasks:before {
- content: ""
-}
-
-.fa-filter:before {
- content: ""
-}
-
-.fa-briefcase:before {
- content: ""
-}
-
-.fa-arrows-alt:before {
- content: ""
-}
-
-.fa-group:before,
-.fa-users:before {
- content: ""
-}
-
-.fa-chain:before,
-.fa-link:before {
- content: ""
-}
-
-.fa-cloud:before {
- content: ""
-}
-
-.fa-flask:before {
- content: ""
-}
-
-.fa-cut:before,
-.fa-scissors:before {
- content: ""
-}
-
-.fa-copy:before,
-.fa-files-o:before {
- content: ""
-}
-
-.fa-paperclip:before {
- content: ""
-}
-
-.fa-save:before,
-.fa-floppy-o:before {
- content: ""
-}
-
-.fa-square:before {
- content: ""
-}
-
-.fa-navicon:before,
-.fa-reorder:before,
-.fa-bars:before {
- content: ""
-}
-
-.fa-list-ul:before {
- content: ""
-}
-
-.fa-list-ol:before {
- content: ""
-}
-
-.fa-strikethrough:before {
- content: ""
-}
-
-.fa-underline:before {
- content: ""
-}
-
-.fa-table:before {
- content: ""
-}
-
-.fa-magic:before {
- content: ""
-}
-
-.fa-truck:before {
- content: ""
-}
-
-.fa-pinterest:before {
- content: ""
-}
-
-.fa-pinterest-square:before {
- content: ""
-}
-
-.fa-google-plus-square:before {
- content: ""
-}
-
-.fa-google-plus:before {
- content: ""
-}
-
-.fa-money:before {
- content: ""
-}
-
-.fa-caret-down:before {
- content: ""
-}
-
-.fa-caret-up:before {
- content: ""
-}
-
-.fa-caret-left:before {
- content: ""
-}
-
-.fa-caret-right:before {
- content: ""
-}
-
-.fa-columns:before {
- content: ""
-}
-
-.fa-unsorted:before,
-.fa-sort:before {
- content: ""
-}
-
-.fa-sort-down:before,
-.fa-sort-desc:before {
- content: ""
-}
-
-.fa-sort-up:before,
-.fa-sort-asc:before {
- content: ""
-}
-
-.fa-envelope:before {
- content: ""
-}
-
-.fa-linkedin:before {
- content: ""
-}
-
-.fa-rotate-left:before,
-.fa-undo:before {
- content: ""
-}
-
-.fa-legal:before,
-.fa-gavel:before {
- content: ""
-}
-
-.fa-dashboard:before,
-.fa-tachometer:before {
- content: ""
-}
-
-.fa-comment-o:before {
- content: ""
-}
-
-.fa-comments-o:before {
- content: ""
-}
-
-.fa-flash:before,
-.fa-bolt:before {
- content: ""
-}
-
-.fa-sitemap:before {
- content: ""
-}
-
-.fa-umbrella:before {
- content: ""
-}
-
-.fa-paste:before,
-.fa-clipboard:before {
- content: ""
-}
-
-.fa-lightbulb-o:before {
- content: ""
-}
-
-.fa-exchange:before {
- content: ""
-}
-
-.fa-cloud-download:before {
- content: ""
-}
-
-.fa-cloud-upload:before {
- content: ""
-}
-
-.fa-user-md:before {
- content: ""
-}
-
-.fa-stethoscope:before {
- content: ""
-}
-
-.fa-suitcase:before {
- content: ""
-}
-
-.fa-bell-o:before {
- content: ""
-}
-
-.fa-coffee:before {
- content: ""
-}
-
-.fa-cutlery:before {
- content: ""
-}
-
-.fa-file-text-o:before {
- content: ""
-}
-
-.fa-building-o:before {
- content: ""
-}
-
-.fa-hospital-o:before {
- content: ""
-}
-
-.fa-ambulance:before {
- content: ""
-}
-
-.fa-medkit:before {
- content: ""
-}
-
-.fa-fighter-jet:before {
- content: ""
-}
-
-.fa-beer:before {
- content: ""
-}
-
-.fa-h-square:before {
- content: ""
-}
-
-.fa-plus-square:before {
- content: ""
-}
-
-.fa-angle-double-left:before {
- content: ""
-}
-
-.fa-angle-double-right:before {
- content: ""
-}
-
-.fa-angle-double-up:before {
- content: ""
-}
-
-.fa-angle-double-down:before {
- content: ""
-}
-
-.fa-angle-left:before {
- content: ""
-}
-
-.fa-angle-right:before {
- content: ""
-}
-
-.fa-angle-up:before {
- content: ""
-}
-
-.fa-angle-down:before {
- content: ""
-}
-
-.fa-desktop:before {
- content: ""
-}
-
-.fa-laptop:before {
- content: ""
-}
-
-.fa-tablet:before {
- content: ""
-}
-
-.fa-mobile-phone:before,
-.fa-mobile:before {
- content: ""
-}
-
-.fa-circle-o:before {
- content: ""
-}
-
-.fa-quote-left:before {
- content: ""
-}
-
-.fa-quote-right:before {
- content: ""
-}
-
-.fa-spinner:before {
- content: ""
-}
-
-.fa-circle:before {
- content: ""
-}
-
-.fa-mail-reply:before,
-.fa-reply:before {
- content: ""
-}
-
-.fa-github-alt:before {
- content: ""
-}
-
-.fa-folder-o:before {
- content: ""
-}
-
-.fa-folder-open-o:before {
- content: ""
-}
-
-.fa-smile-o:before {
- content: ""
-}
-
-.fa-frown-o:before {
- content: ""
-}
-
-.fa-meh-o:before {
- content: ""
-}
-
-.fa-gamepad:before {
- content: ""
-}
-
-.fa-keyboard-o:before {
- content: ""
-}
-
-.fa-flag-o:before {
- content: ""
-}
-
-.fa-flag-checkered:before {
- content: ""
-}
-
-.fa-terminal:before {
- content: ""
-}
-
-.fa-code:before {
- content: ""
-}
-
-.fa-mail-reply-all:before,
-.fa-reply-all:before {
- content: ""
-}
-
-.fa-star-half-empty:before,
-.fa-star-half-full:before,
-.fa-star-half-o:before {
- content: ""
-}
-
-.fa-location-arrow:before {
- content: ""
-}
-
-.fa-crop:before {
- content: ""
-}
-
-.fa-code-fork:before {
- content: ""
-}
-
-.fa-unlink:before,
-.fa-chain-broken:before {
- content: ""
-}
-
-.fa-question:before {
- content: ""
-}
-
-.fa-info:before {
- content: ""
-}
-
-.fa-exclamation:before {
- content: ""
-}
-
-.fa-superscript:before {
- content: ""
-}
-
-.fa-subscript:before {
- content: ""
-}
-
-.fa-eraser:before {
- content: ""
-}
-
-.fa-puzzle-piece:before {
- content: ""
-}
-
-.fa-microphone:before {
- content: ""
-}
-
-.fa-microphone-slash:before {
- content: ""
-}
-
-.fa-shield:before {
- content: ""
-}
-
-.fa-calendar-o:before {
- content: ""
-}
-
-.fa-fire-extinguisher:before {
- content: ""
-}
-
-.fa-rocket:before {
- content: ""
-}
-
-.fa-maxcdn:before {
- content: ""
-}
-
-.fa-chevron-circle-left:before {
- content: ""
-}
-
-.fa-chevron-circle-right:before {
- content: ""
-}
-
-.fa-chevron-circle-up:before {
- content: ""
-}
-
-.fa-chevron-circle-down:before {
- content: ""
-}
-
-.fa-html5:before {
- content: ""
-}
-
-.fa-css3:before {
- content: ""
-}
-
-.fa-anchor:before {
- content: ""
-}
-
-.fa-unlock-alt:before {
- content: ""
-}
-
-.fa-bullseye:before {
- content: ""
-}
-
-.fa-ellipsis-h:before {
- content: ""
-}
-
-.fa-ellipsis-v:before {
- content: ""
-}
-
-.fa-rss-square:before {
- content: ""
-}
-
-.fa-play-circle:before {
- content: ""
-}
-
-.fa-ticket:before {
- content: ""
-}
-
-.fa-minus-square:before {
- content: ""
-}
-
-.fa-minus-square-o:before {
- content: ""
-}
-
-.fa-level-up:before {
- content: ""
-}
-
-.fa-level-down:before {
- content: ""
-}
-
-.fa-check-square:before {
- content: ""
-}
-
-.fa-pencil-square:before {
- content: ""
-}
-
-.fa-external-link-square:before {
- content: ""
-}
-
-.fa-share-square:before {
- content: ""
-}
-
-.fa-compass:before {
- content: ""
-}
-
-.fa-toggle-down:before,
-.fa-caret-square-o-down:before {
- content: ""
-}
-
-.fa-toggle-up:before,
-.fa-caret-square-o-up:before {
- content: ""
-}
-
-.fa-toggle-right:before,
-.fa-caret-square-o-right:before {
- content: ""
-}
-
-.fa-euro:before,
-.fa-eur:before {
- content: ""
-}
-
-.fa-gbp:before {
- content: ""
-}
-
-.fa-dollar:before,
-.fa-usd:before {
- content: ""
-}
-
-.fa-rupee:before,
-.fa-inr:before {
- content: ""
-}
-
-.fa-cny:before,
-.fa-rmb:before,
-.fa-yen:before,
-.fa-jpy:before {
- content: ""
-}
-
-.fa-ruble:before,
-.fa-rouble:before,
-.fa-rub:before {
- content: ""
-}
-
-.fa-won:before,
-.fa-krw:before {
- content: ""
-}
-
-.fa-bitcoin:before,
-.fa-btc:before {
- content: ""
-}
-
-.fa-file:before {
- content: ""
-}
-
-.fa-file-text:before {
- content: ""
-}
-
-.fa-sort-alpha-asc:before {
- content: ""
-}
-
-.fa-sort-alpha-desc:before {
- content: ""
-}
-
-.fa-sort-amount-asc:before {
- content: ""
-}
-
-.fa-sort-amount-desc:before {
- content: ""
-}
-
-.fa-sort-numeric-asc:before {
- content: ""
-}
-
-.fa-sort-numeric-desc:before {
- content: ""
-}
-
-.fa-thumbs-up:before {
- content: ""
-}
-
-.fa-thumbs-down:before {
- content: ""
-}
-
-.fa-youtube-square:before {
- content: ""
-}
-
-.fa-youtube:before {
- content: ""
-}
-
-.fa-xing:before {
- content: ""
-}
-
-.fa-xing-square:before {
- content: ""
-}
-
-.fa-youtube-play:before {
- content: ""
-}
-
-.fa-dropbox:before {
- content: ""
-}
-
-.fa-stack-overflow:before {
- content: ""
-}
-
-.fa-instagram:before {
- content: ""
-}
-
-.fa-flickr:before {
- content: ""
-}
-
-.fa-adn:before {
- content: ""
-}
-
-.fa-bitbucket:before {
- content: ""
-}
-
-.fa-bitbucket-square:before {
- content: ""
-}
-
-.fa-tumblr:before {
- content: ""
-}
-
-.fa-tumblr-square:before {
- content: ""
-}
-
-.fa-long-arrow-down:before {
- content: ""
-}
-
-.fa-long-arrow-up:before {
- content: ""
-}
-
-.fa-long-arrow-left:before {
- content: ""
-}
-
-.fa-long-arrow-right:before {
- content: ""
-}
-
-.fa-apple:before {
- content: ""
-}
-
-.fa-windows:before {
- content: ""
-}
-
-.fa-android:before {
- content: ""
-}
-
-.fa-linux:before {
- content: ""
-}
-
-.fa-dribbble:before {
- content: ""
-}
-
-.fa-skype:before {
- content: ""
-}
-
-.fa-foursquare:before {
- content: ""
-}
-
-.fa-trello:before {
- content: ""
-}
-
-.fa-female:before {
- content: ""
-}
-
-.fa-male:before {
- content: ""
-}
-
-.fa-gittip:before,
-.fa-gratipay:before {
- content: ""
-}
-
-.fa-sun-o:before {
- content: ""
-}
-
-.fa-moon-o:before {
- content: ""
-}
-
-.fa-archive:before {
- content: ""
-}
-
-.fa-bug:before {
- content: ""
-}
-
-.fa-vk:before {
- content: ""
-}
-
-.fa-weibo:before {
- content: ""
-}
-
-.fa-renren:before {
- content: ""
-}
-
-.fa-pagelines:before {
- content: ""
-}
-
-.fa-stack-exchange:before {
- content: ""
-}
-
-.fa-arrow-circle-o-right:before {
- content: ""
-}
-
-.fa-arrow-circle-o-left:before {
- content: ""
-}
-
-.fa-toggle-left:before,
-.fa-caret-square-o-left:before {
- content: ""
-}
-
-.fa-dot-circle-o:before {
- content: ""
-}
-
-.fa-wheelchair:before {
- content: ""
-}
-
-.fa-vimeo-square:before {
- content: ""
-}
-
-.fa-turkish-lira:before,
-.fa-try:before {
- content: ""
-}
-
-.fa-plus-square-o:before {
- content: ""
-}
-
-.fa-space-shuttle:before {
- content: ""
-}
-
-.fa-slack:before {
- content: ""
-}
-
-.fa-envelope-square:before {
- content: ""
-}
-
-.fa-wordpress:before {
- content: ""
-}
-
-.fa-openid:before {
- content: ""
-}
-
-.fa-institution:before,
-.fa-bank:before,
-.fa-university:before {
- content: ""
-}
-
-.fa-mortar-board:before,
-.fa-graduation-cap:before {
- content: ""
-}
-
-.fa-yahoo:before {
- content: ""
-}
-
-.fa-google:before {
- content: ""
-}
-
-.fa-reddit:before {
- content: ""
-}
-
-.fa-reddit-square:before {
- content: ""
-}
-
-.fa-stumbleupon-circle:before {
- content: ""
-}
-
-.fa-stumbleupon:before {
- content: ""
-}
-
-.fa-delicious:before {
- content: ""
-}
-
-.fa-digg:before {
- content: ""
-}
-
-.fa-pied-piper:before {
- content: ""
-}
-
-.fa-pied-piper-alt:before {
- content: ""
-}
-
-.fa-drupal:before {
- content: ""
-}
-
-.fa-joomla:before {
- content: ""
-}
-
-.fa-language:before {
- content: ""
-}
-
-.fa-fax:before {
- content: ""
-}
-
-.fa-building:before {
- content: ""
-}
-
-.fa-child:before {
- content: ""
-}
-
-.fa-paw:before {
- content: ""
-}
-
-.fa-spoon:before {
- content: ""
-}
-
-.fa-cube:before {
- content: ""
-}
-
-.fa-cubes:before {
- content: ""
-}
-
-.fa-behance:before {
- content: ""
-}
-
-.fa-behance-square:before {
- content: ""
-}
-
-.fa-steam:before {
- content: ""
-}
-
-.fa-steam-square:before {
- content: ""
-}
-
-.fa-recycle:before {
- content: ""
-}
-
-.fa-automobile:before,
-.fa-car:before {
- content: ""
-}
-
-.fa-cab:before,
-.fa-taxi:before {
- content: ""
-}
-
-.fa-tree:before {
- content: ""
-}
-
-.fa-spotify:before {
- content: ""
-}
-
-.fa-deviantart:before {
- content: ""
-}
-
-.fa-soundcloud:before {
- content: ""
-}
-
-.fa-database:before {
- content: ""
-}
-
-.fa-file-pdf-o:before {
- content: ""
-}
-
-.fa-file-word-o:before {
- content: ""
-}
-
-.fa-file-excel-o:before {
- content: ""
-}
-
-.fa-file-powerpoint-o:before {
- content: ""
-}
-
-.fa-file-photo-o:before,
-.fa-file-picture-o:before,
-.fa-file-image-o:before {
- content: ""
-}
-
-.fa-file-zip-o:before,
-.fa-file-archive-o:before {
- content: ""
-}
-
-.fa-file-sound-o:before,
-.fa-file-audio-o:before {
- content: ""
-}
-
-.fa-file-movie-o:before,
-.fa-file-video-o:before {
- content: ""
-}
-
-.fa-file-code-o:before {
- content: ""
-}
-
-.fa-vine:before {
- content: ""
-}
-
-.fa-codepen:before {
- content: ""
-}
-
-.fa-jsfiddle:before {
- content: ""
-}
-
-.fa-life-bouy:before,
-.fa-life-buoy:before,
-.fa-life-saver:before,
-.fa-support:before,
-.fa-life-ring:before {
- content: ""
-}
-
-.fa-circle-o-notch:before {
- content: ""
-}
-
-.fa-ra:before,
-.fa-rebel:before {
- content: ""
-}
-
-.fa-ge:before,
-.fa-empire:before {
- content: ""
-}
-
-.fa-git-square:before {
- content: ""
-}
-
-.fa-git:before {
- content: ""
-}
-
-.fa-y-combinator-square:before,
-.fa-yc-square:before,
-.fa-hacker-news:before {
- content: ""
-}
-
-.fa-tencent-weibo:before {
- content: ""
-}
-
-.fa-qq:before {
- content: ""
-}
-
-.fa-wechat:before,
-.fa-weixin:before {
- content: ""
-}
-
-.fa-send:before,
-.fa-paper-plane:before {
- content: ""
-}
-
-.fa-send-o:before,
-.fa-paper-plane-o:before {
- content: ""
-}
-
-.fa-history:before {
- content: ""
-}
-
-.fa-circle-thin:before {
- content: ""
-}
-
-.fa-header:before {
- content: ""
-}
-
-.fa-paragraph:before {
- content: ""
-}
-
-.fa-sliders:before {
- content: ""
-}
-
-.fa-share-alt:before {
- content: ""
-}
-
-.fa-share-alt-square:before {
- content: ""
-}
-
-.fa-bomb:before {
- content: ""
-}
-
-.fa-soccer-ball-o:before,
-.fa-futbol-o:before {
- content: ""
-}
-
-.fa-tty:before {
- content: ""
-}
-
-.fa-binoculars:before {
- content: ""
-}
-
-.fa-plug:before {
- content: ""
-}
-
-.fa-slideshare:before {
- content: ""
-}
-
-.fa-twitch:before {
- content: ""
-}
-
-.fa-yelp:before {
- content: ""
-}
-
-.fa-newspaper-o:before {
- content: ""
-}
-
-.fa-wifi:before {
- content: ""
-}
-
-.fa-calculator:before {
- content: ""
-}
-
-.fa-paypal:before {
- content: ""
-}
-
-.fa-google-wallet:before {
- content: ""
-}
-
-.fa-cc-visa:before {
- content: ""
-}
-
-.fa-cc-mastercard:before {
- content: ""
-}
-
-.fa-cc-discover:before {
- content: ""
-}
-
-.fa-cc-amex:before {
- content: ""
-}
-
-.fa-cc-paypal:before {
- content: ""
-}
-
-.fa-cc-stripe:before {
- content: ""
-}
-
-.fa-bell-slash:before {
- content: ""
-}
-
-.fa-bell-slash-o:before {
- content: ""
-}
-
-.fa-trash:before {
- content: ""
-}
-
-.fa-copyright:before {
- content: ""
-}
-
-.fa-at:before {
- content: ""
-}
-
-.fa-eyedropper:before {
- content: ""
-}
-
-.fa-paint-brush:before {
- content: ""
-}
-
-.fa-birthday-cake:before {
- content: ""
-}
-
-.fa-area-chart:before {
- content: ""
-}
-
-.fa-pie-chart:before {
- content: ""
-}
-
-.fa-line-chart:before {
- content: ""
-}
-
-.fa-lastfm:before {
- content: ""
-}
-
-.fa-lastfm-square:before {
- content: ""
-}
-
-.fa-toggle-off:before {
- content: ""
-}
-
-.fa-toggle-on:before {
- content: ""
-}
-
-.fa-bicycle:before {
- content: ""
-}
-
-.fa-bus:before {
- content: ""
-}
-
-.fa-ioxhost:before {
- content: ""
-}
-
-.fa-angellist:before {
- content: ""
-}
-
-.fa-cc:before {
- content: ""
-}
-
-.fa-shekel:before,
-.fa-sheqel:before,
-.fa-ils:before {
- content: ""
-}
-
-.fa-meanpath:before {
- content: ""
-}
-
-.fa-buysellads:before {
- content: ""
-}
-
-.fa-connectdevelop:before {
- content: ""
-}
-
-.fa-dashcube:before {
- content: ""
-}
-
-.fa-forumbee:before {
- content: ""
-}
-
-.fa-leanpub:before {
- content: ""
-}
-
-.fa-sellsy:before {
- content: ""
-}
-
-.fa-shirtsinbulk:before {
- content: ""
-}
-
-.fa-simplybuilt:before {
- content: ""
-}
-
-.fa-skyatlas:before {
- content: ""
-}
-
-.fa-cart-plus:before {
- content: ""
-}
-
-.fa-cart-arrow-down:before {
- content: ""
-}
-
-.fa-diamond:before {
- content: ""
-}
-
-.fa-ship:before {
- content: ""
-}
-
-.fa-user-secret:before {
- content: ""
-}
-
-.fa-motorcycle:before {
- content: ""
-}
-
-.fa-street-view:before {
- content: ""
-}
-
-.fa-heartbeat:before {
- content: ""
-}
-
-.fa-venus:before {
- content: ""
-}
-
-.fa-mars:before {
- content: ""
-}
-
-.fa-mercury:before {
- content: ""
-}
-
-.fa-intersex:before,
-.fa-transgender:before {
- content: ""
-}
-
-.fa-transgender-alt:before {
- content: ""
-}
-
-.fa-venus-double:before {
- content: ""
-}
-
-.fa-mars-double:before {
- content: ""
-}
-
-.fa-venus-mars:before {
- content: ""
-}
-
-.fa-mars-stroke:before {
- content: ""
-}
-
-.fa-mars-stroke-v:before {
- content: ""
-}
-
-.fa-mars-stroke-h:before {
- content: ""
-}
-
-.fa-neuter:before {
- content: ""
-}
-
-.fa-genderless:before {
- content: ""
-}
-
-.fa-facebook-official:before {
- content: ""
-}
-
-.fa-pinterest-p:before {
- content: ""
-}
-
-.fa-whatsapp:before {
- content: ""
-}
-
-.fa-server:before {
- content: ""
-}
-
-.fa-user-plus:before {
- content: ""
-}
-
-.fa-user-times:before {
- content: ""
-}
-
-.fa-hotel:before,
-.fa-bed:before {
- content: ""
-}
-
-.fa-viacoin:before {
- content: ""
-}
-
-.fa-train:before {
- content: ""
-}
-
-.fa-subway:before {
- content: ""
-}
-
-.fa-medium:before {
- content: ""
-}
-
-.fa-yc:before,
-.fa-y-combinator:before {
- content: ""
-}
-
-.fa-optin-monster:before {
- content: ""
-}
-
-.fa-opencart:before {
- content: ""
-}
-
-.fa-expeditedssl:before {
- content: ""
-}
-
-.fa-battery-4:before,
-.fa-battery-full:before {
- content: ""
-}
-
-.fa-battery-3:before,
-.fa-battery-three-quarters:before {
- content: ""
-}
-
-.fa-battery-2:before,
-.fa-battery-half:before {
- content: ""
-}
-
-.fa-battery-1:before,
-.fa-battery-quarter:before {
- content: ""
-}
-
-.fa-battery-0:before,
-.fa-battery-empty:before {
- content: ""
-}
-
-.fa-mouse-pointer:before {
- content: ""
-}
-
-.fa-i-cursor:before {
- content: ""
-}
-
-.fa-object-group:before {
- content: ""
-}
-
-.fa-object-ungroup:before {
- content: ""
-}
-
-.fa-sticky-note:before {
- content: ""
-}
-
-.fa-sticky-note-o:before {
- content: ""
-}
-
-.fa-cc-jcb:before {
- content: ""
-}
-
-.fa-cc-diners-club:before {
- content: ""
-}
-
-.fa-clone:before {
- content: ""
-}
-
-.fa-balance-scale:before {
- content: ""
-}
-
-.fa-hourglass-o:before {
- content: ""
-}
-
-.fa-hourglass-1:before,
-.fa-hourglass-start:before {
- content: ""
-}
-
-.fa-hourglass-2:before,
-.fa-hourglass-half:before {
- content: ""
-}
-
-.fa-hourglass-3:before,
-.fa-hourglass-end:before {
- content: ""
-}
-
-.fa-hourglass:before {
- content: ""
-}
-
-.fa-hand-grab-o:before,
-.fa-hand-rock-o:before {
- content: ""
-}
-
-.fa-hand-stop-o:before,
-.fa-hand-paper-o:before {
- content: ""
-}
-
-.fa-hand-scissors-o:before {
- content: ""
-}
-
-.fa-hand-lizard-o:before {
- content: ""
-}
-
-.fa-hand-spock-o:before {
- content: ""
-}
-
-.fa-hand-pointer-o:before {
- content: ""
-}
-
-.fa-hand-peace-o:before {
- content: ""
-}
-
-.fa-trademark:before {
- content: ""
-}
-
-.fa-registered:before {
- content: ""
-}
-
-.fa-creative-commons:before {
- content: ""
-}
-
-.fa-gg:before {
- content: ""
-}
-
-.fa-gg-circle:before {
- content: ""
-}
-
-.fa-tripadvisor:before {
- content: ""
-}
-
-.fa-odnoklassniki:before {
- content: ""
-}
-
-.fa-odnoklassniki-square:before {
- content: ""
-}
-
-.fa-get-pocket:before {
- content: ""
-}
-
-.fa-wikipedia-w:before {
- content: ""
-}
-
-.fa-safari:before {
- content: ""
-}
-
-.fa-chrome:before {
- content: ""
-}
-
-.fa-firefox:before {
- content: ""
-}
-
-.fa-opera:before {
- content: ""
-}
-
-.fa-internet-explorer:before {
- content: ""
-}
-
-.fa-tv:before,
-.fa-television:before {
- content: ""
-}
-
-.fa-contao:before {
- content: ""
-}
-
-.fa-500px:before {
- content: ""
-}
-
-.fa-amazon:before {
- content: ""
-}
-
-.fa-calendar-plus-o:before {
- content: ""
-}
-
-.fa-calendar-minus-o:before {
- content: ""
-}
-
-.fa-calendar-times-o:before {
- content: ""
-}
-
-.fa-calendar-check-o:before {
- content: ""
-}
-
-.fa-industry:before {
- content: ""
-}
-
-.fa-map-pin:before {
- content: ""
-}
-
-.fa-map-signs:before {
- content: ""
-}
-
-.fa-map-o:before {
- content: ""
-}
-
-.fa-map:before {
- content: ""
-}
-
-.fa-commenting:before {
- content: ""
-}
-
-.fa-commenting-o:before {
- content: ""
-}
-
-.fa-houzz:before {
- content: ""
-}
-
-.fa-vimeo:before {
- content: ""
-}
-
-.fa-black-tie:before {
- content: ""
-}
-
-.fa-fonticons:before {
- content: ""
-}
-
-.fa-reddit-alien:before {
- content: ""
-}
-
-.fa-edge:before {
- content: ""
-}
-
-.fa-credit-card-alt:before {
- content: ""
-}
-
-.fa-codiepie:before {
- content: ""
-}
-
-.fa-modx:before {
- content: ""
-}
-
-.fa-fort-awesome:before {
- content: ""
-}
-
-.fa-usb:before {
- content: ""
-}
-
-.fa-product-hunt:before {
- content: ""
-}
-
-.fa-mixcloud:before {
- content: ""
-}
-
-.fa-scribd:before {
- content: ""
-}
-
-.fa-pause-circle:before {
- content: ""
-}
-
-.fa-pause-circle-o:before {
- content: ""
-}
-
-.fa-stop-circle:before {
- content: ""
-}
-
-.fa-stop-circle-o:before {
- content: ""
-}
-
-.fa-shopping-bag:before {
- content: ""
-}
-
-.fa-shopping-basket:before {
- content: ""
-}
-
-.fa-hashtag:before {
- content: ""
-}
-
-.fa-bluetooth:before {
- content: ""
-}
-
-.fa-bluetooth-b:before {
- content: ""
-}
-
-.fa-percent:before {
- content: ""
-}
diff --git a/src/app/explain-visualizer/assets/css/query-container.css b/src/app/explain-visualizer/assets/css/query-container.css
deleted file mode 100644
index 1e254cb0..00000000
--- a/src/app/explain-visualizer/assets/css/query-container.css
+++ /dev/null
@@ -1,106 +0,0 @@
-.plan-query-container {
- border: 1px solid #dedede;
- padding: 18px;
- background-color: #fff;
- position: absolute;
- box-shadow: 0px 0px 10px 2px rgba(0, 0, 0, 0.3);
- border-radius: 3px;
- margin-bottom: 18px;
- z-index: 6;
- left: 0
-}
-
-.plan-query-container code {
- font-weight: 300
-}
-
-.plan-query-container .plan-query-text {
- background-color: #fff;
- font-family: "source code";
- text-align: left
-}
-
-.plan-query-container .plan-query-text .code-key-item {
- background-color: #F8E400;
- font-weight: 600;
- padding: 1px
-}
-
-.plan-query-container h3 {
- font-size: 17px;
- width: 93%;
- text-align: left;
- border-bottom: 1px solid #dedede;
- padding-bottom: 6px;
- margin-bottom: 10px
-}
-
-.hljs,
-.hljs-subst {
- color: #4d525a
-}
-
-.hljs-keyword,
-.hljs-attribute,
-.hljs-selector-tag,
-.hljs-meta-keyword,
-.hljs-doctag,
-.hljs-name {
- color: #008CAF;
- font-weight: bold
-}
-
-.hljs-built_in,
-.hljs-literal,
-.hljs-bullet,
-.hljs-code,
-.hljs-addition {
- color: #008CAF;
- font-weight: bold
-}
-
-.hljs-regexp,
-.hljs-symbol,
-.hljs-variable,
-.hljs-template-variable,
-.hljs-link,
-.hljs-selector-attr,
-.hljs-selector-pseudo {
- color: #008CAF;
- font-weight: bold
-}
-
-.hljs-type,
-.hljs-string,
-.hljs-number,
-.hljs-selector-id,
-.hljs-selector-class,
-.hljs-quote,
-.hljs-template-tag,
-.hljs-deletion {
- color: #279404;
- font-weight: 600
-}
-
-.hljs-title,
-.hljs-section {
- color: #008CAF;
- font-weight: bold
-}
-
-.hljs-comment {
- color: #999ea7;
- font-style: italic
-}
-
-.hljs-meta {
- color: #999ea7
-}
-
-.hljs-emphasis {
- font-style: italic
-}
-
-.hljs-strong {
- font-weight: 600
-}
diff --git a/src/app/explain-visualizer/assets/css/styles.css b/src/app/explain-visualizer/assets/css/styles.css
deleted file mode 100644
index d8bcc661..00000000
--- a/src/app/explain-visualizer/assets/css/styles.css
+++ /dev/null
@@ -1,125 +0,0 @@
-/* include if using query container */
-/* @import url(query-container.css); */
-/* @import url(font-awesome.css); */
-
-.text-muted,
-.plan-stats .btn-close {
- color: #999ea7
-}
-
-.text-warning {
- color: #FB4418
-}
-
-.hero-container {
- margin: 30px;
- font-size: 22px;
- text-align: center
-}
-
-.pull-right {
- float: right
-}
-
-.align-right {
- text-align: right
-}
-
-a {
- color: #00B5E2;
- text-decoration: none
-}
-
-.clickable {
- cursor: pointer
-}
-
-code,
-.code {
- /*font-family: "source code";*/
- font-weight: 600
-}
-
-.pad-left {
- margin-left: 10px
-}
-
-.pad-top {
- margin-top: 10px
-}
-
-.pad-bottom {
- margin-bottom: 10px
-}
-
-[tooltip]:before {
- width: 150px;
- text-transform: none;
- text-align: left;
- content: attr(tooltip);
- position: absolute;
- opacity: 0;
- transition: all 0.15s ease;
- padding: 10px;
- color: #fff;
- border-radius: 6px;
- margin-top: -10px;
- margin-left: 20px;
- z-index: 5;
- pointer-events: none
-}
-
-[tooltip]:hover:before {
- opacity: 1;
- background: #008CAF
-}
-
-.banner {
- font-size: 17px;
- padding: 10px;
- margin: auto;
- margin-top: 10px;
- text-align: center;
- background-color: #008CAF;
- color: #65DDFB
-}
-
-.banner a {
- color: white
-}
-
-.btn {
- border-radius: 3px;
- padding: 6px 10px;
- font-size: 13px;
- line-height: 1.2;
- text-decoration: none;
- text-transform: uppercase;
- cursor: pointer;
- margin-left: 6px
-}
-
-.btn-slim .fa,
-.btn-close .fa {
- margin: 0
-}
-
-.btn-close {
- border-radius: 50%;
- box-shadow: none;
- line-height: 1.5;
- border: 1px solid #dedede;
- background-color: #fff
-}
-
-.error-message {
- background-color: #FB4418;
- padding: 6px;
- color: #fff
-}
-
-.plan {
- padding-bottom: 30px;
- margin-left: 100px
-}
-
diff --git a/src/app/explain-visualizer/assets/fonts/FontAwesome.otf b/src/app/explain-visualizer/assets/fonts/FontAwesome.otf
deleted file mode 100644
index 3ed7f8b4..00000000
Binary files a/src/app/explain-visualizer/assets/fonts/FontAwesome.otf and /dev/null differ
diff --git a/src/app/explain-visualizer/assets/fonts/fontawesome-webfont.eot b/src/app/explain-visualizer/assets/fonts/fontawesome-webfont.eot
deleted file mode 100644
index 9b6afaed..00000000
Binary files a/src/app/explain-visualizer/assets/fonts/fontawesome-webfont.eot and /dev/null differ
diff --git a/src/app/explain-visualizer/assets/fonts/fontawesome-webfont.ttf b/src/app/explain-visualizer/assets/fonts/fontawesome-webfont.ttf
deleted file mode 100644
index 26dea795..00000000
Binary files a/src/app/explain-visualizer/assets/fonts/fontawesome-webfont.ttf and /dev/null differ
diff --git a/src/app/explain-visualizer/assets/fonts/fontawesome-webfont.woff b/src/app/explain-visualizer/assets/fonts/fontawesome-webfont.woff
deleted file mode 100644
index dc35ce3c..00000000
Binary files a/src/app/explain-visualizer/assets/fonts/fontawesome-webfont.woff and /dev/null differ
diff --git a/src/app/explain-visualizer/assets/fonts/fontawesome-webfont.woff2 b/src/app/explain-visualizer/assets/fonts/fontawesome-webfont.woff2
deleted file mode 100644
index 500e5172..00000000
Binary files a/src/app/explain-visualizer/assets/fonts/fontawesome-webfont.woff2 and /dev/null differ
diff --git a/src/app/explain-visualizer/assets/fonts/noto-sans-700.ttf b/src/app/explain-visualizer/assets/fonts/noto-sans-700.ttf
deleted file mode 100644
index 6e00cdce..00000000
Binary files a/src/app/explain-visualizer/assets/fonts/noto-sans-700.ttf and /dev/null differ
diff --git a/src/app/explain-visualizer/assets/fonts/noto-sans-700italic.ttf b/src/app/explain-visualizer/assets/fonts/noto-sans-700italic.ttf
deleted file mode 100644
index 51b7b295..00000000
Binary files a/src/app/explain-visualizer/assets/fonts/noto-sans-700italic.ttf and /dev/null differ
diff --git a/src/app/explain-visualizer/assets/fonts/noto-sans-italic.ttf b/src/app/explain-visualizer/assets/fonts/noto-sans-italic.ttf
deleted file mode 100644
index dc93fea6..00000000
Binary files a/src/app/explain-visualizer/assets/fonts/noto-sans-italic.ttf and /dev/null differ
diff --git a/src/app/explain-visualizer/assets/fonts/noto-sans-regular.ttf b/src/app/explain-visualizer/assets/fonts/noto-sans-regular.ttf
deleted file mode 100644
index 9dd10199..00000000
Binary files a/src/app/explain-visualizer/assets/fonts/noto-sans-regular.ttf and /dev/null differ
diff --git a/src/app/explain-visualizer/assets/fonts/source-code-pro-300.ttf b/src/app/explain-visualizer/assets/fonts/source-code-pro-300.ttf
deleted file mode 100644
index 51eb9630..00000000
Binary files a/src/app/explain-visualizer/assets/fonts/source-code-pro-300.ttf and /dev/null differ
diff --git a/src/app/explain-visualizer/assets/fonts/source-code-pro-500.ttf b/src/app/explain-visualizer/assets/fonts/source-code-pro-500.ttf
deleted file mode 100644
index 1ee45ebd..00000000
Binary files a/src/app/explain-visualizer/assets/fonts/source-code-pro-500.ttf and /dev/null differ
diff --git a/src/app/explain-visualizer/components/plan-node/plan-node.component.html b/src/app/explain-visualizer/components/plan-node/plan-node.component.html
deleted file mode 100644
index dee12181..00000000
--- a/src/app/explain-visualizer/components/plan-node/plan-node.component.html
+++ /dev/null
@@ -1,140 +0,0 @@
-
-
-
-
-
{{getDataModelShort()}}
-
-
-
-
-
-
- join:
- {{node[_planService.JOIN_TYPE_PROP]}}
-
-
- group:
- {{node[_planService.GROUP_KEY_PROP]}}
-
-
-
- fields:
- {{node[_planService.FIELDS]}}
-
-
- expressions:
- {{node[_planService.EXPRESSIONS]}}
-
-
- condition:
- {{node[_planService.CONDITION]}}
-
-
- transformation:
- {{node[_planService.TRANSFORMATION]}}
-
-
- aggs:
- {{node[_planService.AGGREGATIONS]}}
-
-
- table:
- {{node[_planService.TABLE]}}
-
-
- rowcount:
- {{node[_planService.ROW_COUNT]}}
-
-
-
- on
- {{node[_planService.SCHEMA_PROP]}}
- . {{node[_planService.RELATION_NAME_PROP]}}
- ({{node[_planService.ALIAS_PROP]}})
-
-
-
- by {{node[_planService.SORT_KEY_PROP]}}
-
- using {{node[_planService.INDEX_NAME_PROP]}}
-
- on {{node[_planService.HASH_CONDITION_PROP]}}
-
- CTE {{node[_planService.CTE_NAME_PROP]}}
-
-
-
-
0">
- {{getTagName(tag)}}
-
-
-
-
-
-
-
- {{viewOptions.highlightType}}: {{highlightValue | number:'.0-2'}}
-
-
-
-
- over estimated rows
- under estimated rows
- correctly estimated rows
- by {{plannerRowEstimateValue | number:'.0-1'}} x
-
-
-
-
- {{node[_planService.NODE_TYPE_PROP]}} Node
-
-
-
-
- {{prop.key}}
- {{prop.value}}
-
-
-
* calculated value
-
-
-
-
-
-
-
-
-
-
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 3dda1635..66e560aa 100644
--- a/src/app/models/information-page.model.ts
+++ b/src/app/models/information-page.model.ts
@@ -41,8 +41,9 @@ export interface InformationObject extends Duration {
labels?: string[];
colors?: string[];
graphType?: string;
- //debugger
- queryPlan: string;
+ //PolyAlg plans
+ jsonPolyAlg: string;
+ planType: PlanType;
//code
code?: string;
language?: string;
@@ -60,6 +61,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 67586c55..1c0717c2 100644
--- a/src/app/models/ui-request.model.ts
+++ b/src/app/models/ui-request.model.ts
@@ -1,7 +1,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;
@@ -33,32 +33,22 @@ 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;
+export class PolyAlgRequest extends UIRequest {
+ type = 'PolyAlgRequest';
+ polyAlg: string;
+ model: DataModel;
+ planType: PlanType;
+ dynamicValues: string[];
+ dynamicTypes: string[];
+ noLimit = false; // TODO: handle queries with large results
- constructor(node: Node, cache: boolean, analyzeQuery: boolean, createView?: boolean, tableType?: string, viewName?: string, store?: string, freshness?: string, interval?, timeUnit?: string) {
+ constructor(polyAlg: string, model: DataModel, planType: PlanType, dynamicValues = null, dynamicTypes = null) {
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;
+ 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 00644109..7c5365a8 100644
--- a/src/app/services/crud.service.ts
+++ b/src/app/services/crud.service.ts
@@ -1,38 +1,12 @@
import {EventEmitter, inject, Injectable} from '@angular/core';
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {WebuiSettingsService} from './webui-settings.service';
-import {
- EntityMeta,
- IndexModel,
- ModifyPartitionRequest,
- PartitionFunctionModel,
- PartitioningRequest,
- PathAccessRequest,
- PlacementFieldsModel,
+import {EntityMeta, IndexModel, ModifyPartitionRequest, PartitionFunctionModel, PartitioningRequest, PathAccessRequest, PlacementFieldsModel,
RelationalResult
} 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 {
- AutoDockerResult,
- AutoDockerStatus,
- CreateDockerResponse,
+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,
@@ -43,10 +17,11 @@ import {ForeignKey, Uml} from '../views/uml/uml.model';
import {Validators} from '@angular/forms';
import {AdapterModel} from '../views/adapters/adapter.model';
import {QueryInterface, QueryInterfaceTemplate} 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';
@Injectable({
@@ -383,7 +358,7 @@ export class CrudService {
code = `db.${collection}.deletePlacement( "${store}" )`;
break;
}
- const request = new QueryRequest(code, false, true, 'cypher', namespace);
+ const request = new QueryRequest(code, false, true, 'mongo', namespace);
return this.anyQueryBlocking(request);
}
@@ -469,24 +444,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);
@@ -542,7 +499,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);
}
@@ -571,13 +528,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);
}
@@ -689,6 +639,25 @@ export class CrudService {
return this._http.patch(`${this.httpUrl}/docker/settings`, settings, this.httpOptions);
}
+ getPolyAlgRegistry() {
+ return this._http.get(`${this.httpUrl}/getPolyAlgRegistry`);
+ }
+
+ executePolyAlg(socket: WebSocket, polyAlg: string, model: DataModel, planType: PlanType) {
+ const request = new PolyAlgRequest(polyAlg, model, planType);
+ 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);
+ }
+
getNameValidator(required: boolean = false) {
if (required) {
return [Validators.pattern('^[a-zA-Z_][a-zA-Z0-9_]*$'), Validators.required, Validators.max(100)];
@@ -742,5 +711,4 @@ export class CrudService {
return this._http.post(`${this.httpUrl}/loadPlugins`, formData);
}
-
}
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 @@
-
-
-
-
-
-
-
-
- export
- import
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ 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 @@
-
-
-
-
-
-
-
-
- Entity
- View
-
-
-
-
-
-
-
- Join Type
-
- INNER
- OUTER
-
-
-
-
-
-
- Operator
-
- =
- <>
- <
- >
- ≤
- ≥
-
-
-
-
-
-
- Col 1
-
-
-
-
-
- Col 2
-
-
-
-
-
-
-
-
- Operator
-
- =
- <>
- <
- >
- ≤
- ≥
-
-
-
-
- Field
-
-
-
-
-
- Filter
-
-
-
-
-
-
-
- + add field
-
-
-
-
-
-
-
-
-
-
-
-
-
- Group By
-
-
-
-
-
- Function
-
- SUM
- COUNT
- AVG
- MIN
- MAX
-
-
-
-
-
- Field
-
-
-
-
-
- As
-
-
-
-
-
-
-
- + add column
-
-
-
-
-
-
-
{{col.value.direction}}
-
-
-
-
-
-
-
- union all
-
-
-
-
-
-
- except all
-
-
-
-
-
-
- intersect all
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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 a9e9bb98..00000000
--- a/src/app/views/querying/algebra/node/node.component.ts
+++ /dev/null
@@ -1,89 +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 {
- isView = false;
-
- constructor() {
- }
-
- @ViewChild('element', {read: ElementRef}) public element: ElementRef;
- @Input() node: Node;
- @Output() autocompleteChanged = new EventEmitter();
-
- 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;
- }
-
-
- protected readonly AlgType = AlgType;
-}
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 )}}
+ *ngIf="queryAnalysis && showingAnalysis"
+ [data]="queryAnalysis">
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..08c767ab
--- /dev/null
+++ b/src/app/views/querying/polyalg/polyalg.component.html
@@ -0,0 +1,167 @@
+
+
+
+
+
+
+
+
+ {{ planType ? ' Plan Type: ' + planType : 'Select Plan Type' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ result().query }}
+
+ !
+
+
+
+
+
+
Error:
+
{{ result().error }}
+
+
+
+
+ Successfully executed
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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:' }}
+
+
+
+ Logical Plan
+ Allocation Plan
+ Physical Plan
+
+
+
+
+
+ Close
+
+ Create Plan
+
+
+
+
+
+ Help
+
+
+
+ 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:
+
+
+ Blue
+ indicates that the plan is valid.
+
+
+ Yellow
+ indicates that the content was (possibly) 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.
+
+
+
+
+
+ Close
+
+
+
+
+
+ Execute Plan
+
+
+
+ Please specify the dynamic parameters:
+
+ {{param[0]}}:{{param[1]}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Clear all
+
+
+
+ Close
+ Execute
+
+
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..1c3b0551
--- /dev/null
+++ 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
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..1ffff708
--- /dev/null
+++ b/src/app/views/querying/polyalg/polyalg.component.ts
@@ -0,0 +1,293 @@
+import {AfterViewInit, Component, Directive, ElementRef, 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, 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';
+import {UserMode} from '../../../components/polyalg/polyalg-viewer/alg-editor';
+
+// Make sure to change these plans in case the name of operators changes!
+const SAMPLE_PLANS = {
+ LOGICAL: 'REL_PROJECT[name, salary](REL_SCAN[public.emps])',
+ ALLOCATION: 'REL_PROJECT[name, salary](REL_SCAN[public.emps@hr.0])',
+ PHYSICAL: `E_CALC[exprs=[empid, deptno, name, salary, commission], projects=[$t2:VARCHAR(255) AS name, $t3:INTEGER AS salary]](
+ E_INTERPRETER[0.5]( BINDABLE_SCAN[hr.0, projects=[0, 1, 2, 3, 4]] ))`
+};
+
+@Component({
+ selector: 'app-polyalg',
+ templateUrl: './polyalg.component.html',
+ styleUrl: './polyalg.component.scss'
+})
+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;
+
+ planType: PlanType = 'LOGICAL';
+ selectedPlanType: PlanType = 'LOGICAL'; // the plan type selected in the modal
+ initialUserMode = UserMode.SIMPLE;
+ showPlanTypeModal = signal(false);
+ showHelpModal = signal(false);
+ showParamsModal = signal(false);
+ polyAlg: string;
+ physicalExecForm: { polyAlg: string, model: DataModel, params: string[][], values: (string | boolean)[] }
+ = {polyAlg: null, model: null, params: null, values: null};
+
+ constructor(
+ private _crud: CrudService,
+ private _leftSidebar: LeftSidebarService,
+ private _toast: ToasterService,
+ private _breadcrumb: BreadcrumbService,
+ private _settings: WebuiSettingsService) {
+
+ const polyAlgToEdit = localStorage.getItem('polyalg.polyAlg');
+ localStorage.removeItem('polyalg.polyAlg'); // only open the plan the first time this component is shown
+
+ if (polyAlgToEdit) {
+ this.polyAlg = polyAlgToEdit;
+ this.planType = localStorage.getItem('polyalg.planType') as PlanType;
+ this.selectedPlanType = this.planType;
+ localStorage.removeItem('polyalg.planType');
+ this.initialUserMode = UserMode.ADVANCED;
+ } else {
+ this.polyAlg = SAMPLE_PLANS[this.planType || 'LOGICAL'];
+ }
+
+ if (!this.planType) {
+ this.showPlanTypeModal.set(true);
+ }
+
+ this.websocket = new WebSocket();
+ this.initWebsocket();
+ }
+
+ ngOnInit(): void {
+ this._leftSidebar.close();
+ }
+
+ ngOnDestroy() {
+ this._leftSidebar.close();
+ this.subscriptions.unsubscribe();
+ this.websocket.close();
+ }
+
+ executePolyAlg([polyAlg, model]: [string, OperatorModel]) {
+ if (polyAlg == null) {
+ 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)) {
+ values = this.physicalExecForm.values; // keep existing values
+ }
+ 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);
+
+ this.loading.set(true);
+ 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.'));
+ }
+ }
+
+ 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) => {
+ 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 => {
+ 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', 'Plan 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);
+ }
+
+ 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 = SAMPLE_PLANS[this.planType];
+ this._leftSidebar.setNodes([]);
+ this._leftSidebar.close();
+ this.result.set(null);
+ }
+ }
+
+ handleHelpModalChange($event: boolean) {
+ this.showHelpModal.set($event);
+ }
+
+ toggleHelpModal() {
+ this.showHelpModal.update(b => !b);
+ }
+
+ handleParamsModalChange($event: boolean) {
+ this.showParamsModal.set($event);
+ }
+
+ toggleParamsModal() {
+ this.showParamsModal.update(b => !b);
+ }
+
+ clearParamsModal() {
+ this.physicalExecForm.values = this.physicalExecForm.params.map(p => p[1] === 'BOOLEAN' ? false : '');
+ }
+}
+
+// https://stackoverflow.com/a/42820432
+@Directive({selector: '[scrollTo]'})
+export class ScrollToDirective implements AfterViewInit {
+ constructor(private elRef: ElementRef) {
+ }
+
+ ngAfterViewInit() {
+ this.elRef.nativeElement.scrollIntoView({behavior: 'smooth', block: 'start'});
+ }
+}
diff --git a/src/app/views/querying/querying.component.html b/src/app/views/querying/querying.component.html
index a84c68f2..fbd01b1c 100644
--- a/src/app/views/querying/querying.component.html
+++ b/src/app/views/querying/querying.component.html
@@ -1,6 +1,6 @@
-
+
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/app/views/views.module.ts b/src/app/views/views.module.ts
index f7830668..057b8f2d 100644
--- a/src/app/views/views.module.ts
+++ b/src/app/views/views.module.ts
@@ -15,14 +15,10 @@ 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';
+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 +31,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';
@@ -51,6 +43,7 @@ import {
ButtonCloseDirective,
ButtonDirective,
ButtonGroupComponent,
+ ButtonToolbarComponent,
CardBodyComponent,
CardComponent,
CardFooterComponent,
@@ -95,6 +88,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, ScrollToDirective} from './querying/polyalg/polyalg.component';
@NgModule({
@@ -163,7 +157,8 @@ import {TreeModule} from '@ali-hm/angular-tree-component';
PlaceholderDirective,
ProgressComponent,
ProgressBarComponent,
- CollapseDirective
+ CollapseDirective,
+ ButtonToolbarComponent
],
declarations: [
EditColumnsComponent,
@@ -179,9 +174,7 @@ import {TreeModule} from '@ali-hm/angular-tree-component';
GraphEditGraphComponent,
MonitoringComponent,
DashboardComponent,
- AlgebraComponent,
QueryingComponent,
- NodeComponent,
AdaptersComponent,
RefinementOptionsComponent,
AboutComponent,
@@ -193,6 +186,8 @@ import {TreeModule} from '@ali-hm/angular-tree-component';
FileUploaderComponent,
DockerconfigComponent,
EditEntityComponent,
+ PolyalgComponent,
+ ScrollToDirective
],
exports: []
})
diff --git a/src/scss/style.scss b/src/scss/style.scss
index 85d8aa41..6d29fa04 100644
--- a/src/scss/style.scss
+++ b/src/scss/style.scss
@@ -79,3 +79,29 @@
transform: rotate(0deg);
}
}
+
+/* Node Editor for query plans */
+[rete-context-menu] {
+ width: 200px !important;
+ border-radius: 4px;
+
+ .block {
+ color: $body-color !important;
+ background: $light !important;
+ border-bottom: 1px solid #c8c9cb !important;
+ padding-top: 2px !important;
+ padding-bottom: 2px !important;
+ }
+
+ .block:hover {
+ background: #c8c9cb !important;
+ }
+
+ .block:last-child {
+ border: none !important;
+ }
+
+ .subitems {
+ width: 200px !important;
+ }
+}
\ No newline at end of file
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
-}