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)

+ + + + + + + +
{{row[0]}}{{row[1]}}
+
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 @@ + + + + + + + + + + + Function Arg + + + + + + + + + + + + + + Function Arg(s) + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + 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 @@ + + 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 @@ +
+ + + + + +
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 @@ + + \ 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 @@ + + \ 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 @@ + + + + + + + + + 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.value.innerType}}

    +

    {{child}}

    +
    +
    +
    + +
    + +
    +
  • +
    +
  • +
    + + +
    +
  • +
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 @@ +
+ + + + + + + + + + + + + + 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 @@ +
+ + + + + + 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 @@ + +(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 @@ + + + + + + +
+ + +
+

PolyAlgebra can only be edited in advanced mode.

+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + +
+ + + +
+
+ + + + + + + + +
+ +

+ 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. + + + +
+ + +
+
+
\ 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()}} -
      -

      {{getNodeName()}}

      - -
      - - - -
      -
      - 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]}} -
      -
      - -
      - {{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
      -
      - -
      - -

      query

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

    {{ $result().error }}

    -
    - -
    -

    - Successfully executed -

    -
    - - - - - - -
    - -
    -
    \ No newline at end of file diff --git a/src/app/views/querying/algebra/algebra.component.scss b/src/app/views/querying/algebra/algebra.component.scss deleted file mode 100644 index 2c7cdd6c..00000000 --- a/src/app/views/querying/algebra/algebra.component.scss +++ /dev/null @@ -1,169 +0,0 @@ -#operatorList .rel-op:hover { - background: rgba(0, 0, 0, 0.05); -} - -.rel-op { - cursor: grab; -} - -#drop { - height: 1000px; -} - -#drop .node, .cdk-drag-preview .node { - position: absolute; - min-width: 260px; - z-index: 1; - left: 0; - top: 0; - - .in, .out { - position: absolute; - left: 50%; - margin-left: -10px; - width: 20px; - height: 20px; - border-radius: 100%; - z-index: 1; - } - - .in { - bottom: -10px; - background: rgba(181, 229, 160, 0.3); - } - - .out { - top: -10px; - background: rgba(150, 216, 228, 0.3); - } - - .drag-handle { - cursor: move; - } - - .del { - margin-left: 1em; - color: #c8ced3; - } - - .del:hover { - color: #dc3545; - cursor: pointer; - } - - .form-group { - input { - margin-left: 1em; - } - } - - .list-group-item { - padding: 0.5rem 1.25rem; - } - - .param-wrapper { - display: flex; - justify-content: space-between; - - label { - margin-bottom: 0; - margin-right: 1em; - line-height: 2em; - } - } - - .param-input { - width: 100%; - } - - input[type=checkbox].param-input { - box-shadow: none; - } -} - -#drop:not(.connecting) .out:hover { - background: rgba(150, 216, 228, 0.7); - cursor: pointer; -} - -#drop.connecting .in:hover { - background: rgba(181, 229, 160, 0.7); -} - -#drop .cdk-drag-placeholder { - display: none; -} - -svg { - position: absolute; - top: 0; - left: 0; - stroke: black; - stroke-width: 1; - z-index: 0; - pointer-events: none; -} - -.r-0 { - right: 2rem; -} - -.t-0 { - top: 1rem; -} - -#result-wrapper { - padding-bottom: 1em; - padding-right: 0; -} - - -/* autocomplete package */ -.ng-autocomplete { - width: 100% !important; -} - -.autocomplete-container { - box-shadow: none !important; - border: 1px solid #e4e7ea; - border-radius: 0.25rem; - line-height: 2; - height: auto !important; -} - -.input-container { - height: initial; - line-height: initial; - - input { - background-color: transparent !important; - height: auto !important; - line-height: 1 !important; - padding: 0.25rem 0.5rem !important; - } -} - -.autocomplete-container .suggestions-container ul li a { - padding: 0 0.5em !important; -} - -.input-container input::placeholder { - font-size: 0.765625rem; -} - -/* bootstrap styles */ -.autocomplete-container { - transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; -} - -.autocomplete-container:focus-within { - border: 1px #8ad4ee solid !important; - border-radius: 0.25rem; - outline: 0 !important; - box-shadow: 0 0 0 0.2rem rgba(32, 168, 216, 0.25) !important; -} - -.analyze-query { - margin-top: 0.25rem; - font-size: 0.765625rem; -} diff --git a/src/app/views/querying/algebra/algebra.component.ts b/src/app/views/querying/algebra/algebra.component.ts deleted file mode 100644 index e9a2d593..00000000 --- a/src/app/views/querying/algebra/algebra.component.ts +++ /dev/null @@ -1,788 +0,0 @@ -import { - AfterViewInit, - Component, - computed, - effect, - ElementRef, - HostBinding, - inject, - OnDestroy, - OnInit, - Signal, - signal, - untracked, - ViewChild, - ViewEncapsulation, - WritableSignal -} from '@angular/core'; -import {AlgNodeModel, AlgType, Connection, Node} from './algebra.model'; -import {RelationalResult, Result} from '../../../components/data-view/models/result-set.model'; -import {CrudService} from '../../../services/crud.service'; -import {ToasterService} from '../../../components/toast-exposer/toaster.service'; -import * as $ from 'jquery'; -import 'jquery-ui/ui/widget'; -import 'jquery-ui/ui/widgets/draggable'; -import 'jquery-ui/ui/widgets/droppable'; -import {SvgLine} from '../../uml/uml.model'; -import {SidebarNode} from '../../../models/sidebar-node.model'; -import {LeftSidebarService} from '../../../components/left-sidebar/left-sidebar.service'; -import {InformationPage} from '../../../models/information-page.model'; -import {BreadcrumbItem} from '../../../components/breadcrumb/breadcrumb-item'; -import {BreadcrumbService} from '../../../components/breadcrumb/breadcrumb.service'; -import {WebuiSettingsService} from '../../../services/webui-settings.service'; -import {Subscription} from 'rxjs'; -import {WebSocket} from '../../../services/webSocket'; -import {UtilService} from '../../../services/util.service'; -import {ViewInformation} from '../../../components/data-view/data-view.component'; -import {CatalogService} from '../../../services/catalog.service'; -import {KeyValue} from '@angular/common'; - -@Component({ - selector: 'app-algebra', - templateUrl: './algebra.component.html', - styleUrls: ['./algebra.component.scss'], - encapsulation: ViewEncapsulation.None -}) -export class AlgebraComponent implements OnInit, AfterViewInit, OnDestroy { - - private readonly _crud = inject(CrudService); - private readonly _toast = inject(ToasterService); - private readonly _leftSidebar = inject(LeftSidebarService); - private readonly _breadcrumb = inject(BreadcrumbService); - private readonly _settings = inject(WebuiSettingsService); - private readonly _catalog = inject(CatalogService); - private readonly _util = inject(UtilService); - - @ViewChild('dropArea', {read: ElementRef}) dropArea: ElementRef; - @HostBinding('class.is-open') - public readonly $result: WritableSignal> = signal(null); - private counter = 0; - public connections = new Map(); - public nodes = new Map(); - public temporalLine: SvgLine; - operators = []; - autocomplete;// names of the schemas, tables and columns - sidebarNodes: SidebarNode[] = []; - private subscriptions = new Subscription(); - analyzerId: string; - showingAnalysis = false; - queryAnalysis: InformationPage; - webSocket: WebSocket; - - //temporal values while dragging - scrollTop: number; - scrollLeft: number; - draggingNodeX: number; - draggingNodeY: number; - socketOn: boolean; - - analyzeQuery = true; - private cache = true; - private $algModels: WritableSignal> = signal(new Map()); - private $algs: Signal>; - public $loading: WritableSignal = signal(false); - - constructor() { - this.socketOn = false; - this.webSocket = new WebSocket(); - this.initWebsocket(); - - this.$algs = computed(() => { - const map = new Map(); - for (let [k, v] of this.$algModels().entries()) { - for (let alg of v) { - map.set(alg.name, alg); - } - } - return map; - }); - - effect(() => { - const catalog = this._catalog.listener(); - - untracked(() => { - const autocomplete = {schemas: []}; - for (const schema of catalog.getSchemaTree('', true, 3)) { - autocomplete.schemas.push(schema.name); - autocomplete[schema.name] = {tables: []}; - for (const table of schema.children) { - autocomplete[schema.name].tables.push([table.name, table.tableType]); - autocomplete[schema.name][table.name] = {columns: []}; - for (const col of table.children) { - autocomplete[schema.name][table.name].columns.push(col.name); - } - } - } - this.autocomplete = autocomplete; - }); - }); - - effect(() => { - const nodes: SidebarNode[] = []; - for (const [k, v] of this.$algModels()?.entries()) { - nodes.push(new SidebarNode('operator_' + k, k, '', '').asSeparator()); - for (let alg of v) { - nodes.push(AlgebraComponent.toSidebarNode(alg)); - } - } - untracked(() => { - this.sidebarNodes = nodes; - this._leftSidebar.setNodes(nodes); - this._leftSidebar.open(); - }); - }); - } - - static toSidebarNode(nodeModel: AlgNodeModel): SidebarNode { - const node = new SidebarNode('operator_' + nodeModel.name, nodeModel.name, nodeModel.icon, null, true); - if (nodeModel.symbol) { - node.setAlgSymbol(nodeModel.symbol); - } - return node; - } - - - ngOnInit() { - this._leftSidebar.open(); - this.getOperators(); - - const sub2 = this.webSocket.reconnecting.subscribe( - b => { - if (b) { - this.getOperators(); - } - } - ); - this.subscriptions.add(sub2); - } - - ngAfterViewInit() { - this.initDraggable(); - //todo find solution without timeout - //setTimeout(() => this.initSidebar(), 200); - } - - ngOnDestroy() { - this.counter = 0; - $(document).off(); - $('#drop').off(); - this._leftSidebar.close(); - this.subscriptions.unsubscribe(); - this.webSocket.close(); - } - - initWebsocket() { - const sub = this.webSocket.onMessage().subscribe({ - next: msg => { - //if msg contains nodes of the sidebar - if (Array.isArray(msg)) { - const sidebarNodesTemp: SidebarNode[] = msg; - const backToPlanBuilder = new SidebarNode('back-to-plan-builder', 'plan-builder', 'fa fa-cubes').setAction((tree, node, $event) => { - this.showingAnalysis = false; - this._breadcrumb.hide(); - }); - const sidebarNodes: SidebarNode[] = [backToPlanBuilder]; - //set analyzerId to close it when leaving the page. - if (sidebarNodesTemp.length > 0) { - const split = sidebarNodesTemp[0].routerLink.split('/'); - this.analyzerId = split[0]; - for (const s of sidebarNodesTemp) { - const s2 = SidebarNode.fromJson(s, {allowRouting: false}); - sidebarNodes.push(s2.setAction((tree, node, $event) => { - //todo define behavior when clicking on analyzer-sidebarNode - const analyzerPage = s2.routerLink.split('/')[1]; - this._crud.getAnalyzerPage(this.analyzerId, analyzerPage).subscribe({ - next: res => { - this.queryAnalysis = res; - this.showingAnalysis = true; - this._breadcrumb.setBreadcrumbs([new BreadcrumbItem(node.data.name)]); - node.setIsActive(true); - }, error: err => { - console.log(err); - } - }); - })); - } - } - sidebarNodes.unshift(new SidebarNode('analyzer', 'analyzer').asSeparator()); - sidebarNodes.unshift(new SidebarNode('separator', ' ').asSeparator()); - this._leftSidebar.setNodes(this.sidebarNodes.concat(sidebarNodes)); - } - //a result - else { - $('#run i').removeClass().addClass('fa fa-play'); - this.$loading.set(false); - this.$result.set(msg); - } - }, error: err => { - setTimeout(() => { - this.initWebsocket(); - }, +this._settings.getSetting('reconnection.timeout')); - } - }); - this.subscriptions.add(sub); - } - - treeDrop(e) { - const id = 'node' + this.counter++; - const x = Math.max(0, Math.min(this.dropArea.nativeElement.offsetWidth - 270, e.event.offsetX)); - const y = Math.max(0, Math.min(this.dropArea.nativeElement.offsetHeight - 140, e.event.offsetY)); - const name = e.element.data.name; - const alg = this.$algs().get(name); - const node = new Node(id, e.element.data.name, x, y); - node.algSymbol = e.element.data.algSymbol; - node.icon = e.element.data.icon; - node.inputCount = alg.inputs; - node.type = alg.type; - node.class = alg.name; - - if (e.element.data.name.includes('Scan')) { - const ac = []; - const acType = []; - if (this.autocomplete) { - for (const v1 of this.autocomplete.schemas) { - for (const v2 of this.autocomplete[v1].tables) { - ac.push(v1 + '.' + v2[0]); - acType.push(v2[1]); - } - } - node.autocomplete = ac; - node.tableTypes = acType; - node.initialNames = ac; - } - } - this.nodes.set(id, node); - } - - /** - * initialize the functionality that nodes can be connected by drag and drop - */ - initDraggable() { - const self = this; - - let isDragging = false; - let source = ''; - $(document).on('mousedown', '#drop .node .out', e => { - isDragging = true; - $('#drop').addClass('connecting'); - const x = $(e.target).parents('.node').parent().position().left + $(e.target).parents('.node').outerWidth() / 2; - const y = $(e.target).parents('.node').parent().position().top; - self.temporalLine = {x1: x, x2: x, y1: y, y2: y}; - - source = $(e.target).parents('.node').attr('id'); - e.preventDefault(); - }).on('mousemove', e => { - if (isDragging) { - e.preventDefault(); - const dropContainer = $('svg#line'); - const x = e.pageX - dropContainer.offset().left; - const y = e.pageY - dropContainer.offset().top; - self.temporalLine.x2 = x; - self.temporalLine.y2 = y; - } - }).on('mouseup', e => { - if (!isDragging) { - return; - } - if ($(e.target).hasClass('in')) { - const target = $(e.target).parents('.node').attr('id'); - if (source !== target) {//don't allow to connect with own node - self.addConnection(source, target); - } - } - isDragging = false; - $('#drop').removeClass('connecting'); - self.temporalLine = null; - }); - - } - - /** - * Delete a node and all connections that belong to it - */ - deleteNode(node: Node) { - const id = node.id; - this.connections.forEach((v, k) => { - if (v.target.id === id || v.source.id === id) { - this.connections.delete(k); - } - }); - this.nodes.delete(id); - this.connections.delete(id); - } - - /** - * Delete every single node - */ - deleteAll() { - this.nodes = new Map(); - this.connections = new Map(); - } - - /** - * When two nodes are connected: - * if there was a connection: delete it - * if not: create a new connection - */ - addConnection(source, target) { - if (this.connections.has(source + target)) { - this.connections.delete(source + target); - } else { - this.connections.set(source + target, { - id: source + target, - source: this.nodes.get(source), - target: this.nodes.get(target) - }); - } - this.setAutocomplete(); - } - - /** - * List LogicalOperators for the select menu - */ - getOperators() { - //see https://stackoverflow.com/questions/43100718/typescript-enum-to-object-array - this._crud.getAlgebraNodes().subscribe(algs => { - this.$algModels.set(algs); - });//Object.keys(LogicalOperator).map(key => LogicalOperator[key]); - } - - - /** - * The topmost node and perform a bottomUp iteration to setup the autocomplete for all nodes - */ - setAutocomplete() { - if (this.autocomplete === undefined) { - return; - } - const tree = this.getTree(); - if (tree !== undefined) { - this.bottomUp(tree); - } - } - - /** - * Iterate the tree from bottom to top - */ - bottomUp(node: Node) { - for (const n of node.children) { - this.bottomUp(n); - } - return this.updateNodeAutocomplete(node); - } - - /** - * Set the autocomplete fields for each node - * default: get columns from all children - * Scan node: get columns from autocomplete object - * Project node: get columns from all children, but only save the ones that are being projected - */ - updateNodeAutocomplete(node: Node) { - const self = this; - - function getNode() { - return self.nodes.get(node.id); - } - - if (node.type === AlgType.Scan) { - if (node.entityName === undefined || !node.entityName.includes('\.')) { - getNode().acSchema.clear(); - getNode().acTable.clear(); - getNode().acSchema.add(node.entityName); - - } else { // node.tableName.includes('\.') - const tN = node.entityName.split('\.'); - getNode().acSchema.clear(); - getNode().acSchema.add(tN[0]); - getNode().acTable.clear(); - getNode().acTable.add(tN[1]); - const ac = []; - const cols = new Set(); - const tableCols = new Set(); - this.autocomplete.schemas - .filter(namespace => getNode().acSchema.has(namespace)) - .forEach((v1, i1) => { - this.autocomplete[v1].tables - .filter((v) => getNode().acTable.has(v[0])) - .forEach((v2, i2) => { - ac.push(v1 + '.' + v2[0]); - this.autocomplete[v1][v2[0]].columns.forEach((v3, i3) => { - cols.add(v3); - tableCols.add(v2[0] + '.' + v3); - }); - }); - }); - getNode().autocomplete = ac; - getNode().acColumns = cols; - getNode().acTableColumns = tableCols; - } - } else if (node.type === AlgType.Project) { - node = this.getFromChildren(node); - for (const col of getNode().acColumns) { - let contains = false; - for (const f of node.fields) { - if (f.split('\.')[1] === col) { - contains = true; - } - } - if (!contains) { - getNode().acColumns.delete(col); - } - } - const ac = []; - for (const tCol of getNode().acTableColumns) { - ac.push(tCol); - if (!node.fields.includes(tCol)) { - getNode().acTableColumns.delete(tCol); - } - } - getNode().autocomplete = ac; - } else { // all other nodes - node = this.getFromChildren(node); - } - return getNode(); - } - - /** - * iterate all children of a node and add all columns to its set - */ - getFromChildren(node: Node) { - const self = this; - - function getNode() { - return self.nodes.get(node.id); - } - - getNode().acSchema.clear(); - getNode().acTable.clear(); - getNode().acTableColumns.clear(); - getNode().acColumns.clear(); - for (const n of node.children) { - getNode().acSchema = new Set([...getNode().acSchema, ...n.acSchema]); - getNode().acTable = new Set([...getNode().acTable, ...n.acTable]); - getNode().acColumns = new Set([...getNode().acColumns, ...n.acColumns]); - getNode().acTableColumns = new Set([...getNode().acTableColumns, ...n.acTableColumns]); - } - getNode().autocomplete = [...getNode().acTableColumns, ...getNode().acColumns]; - return getNode(); - } - - // bottom node (start) - getX1(s: Node) { - if (s === undefined) { - return; - } - if (s.dragging && this.draggingNodeX !== undefined) { - return this.draggingNodeX + s.width / 2 + 50; - } - return s.$left() + s.width / 2; - } - - // bottom node (end) - getX2(t: Node) { - if (t === undefined) { - return; - } - if (t.dragging && this.draggingNodeX !== undefined) { - return this.draggingNodeX + t.width / 2; - } - return t.$left() + t.width / 2; - } - - getY1(s: Node) { - if (s === undefined) { - return; - } - if (s.dragging && this.draggingNodeY !== undefined) { - return this.draggingNodeY - 28; - } - return s.$top() - 16; - } - - getY2(t: Node) { - if (t === undefined) { - return; - } - if (t.dragging && this.draggingNodeY !== undefined) { - return this.draggingNodeY; - } - return t.$top() + t.height - 5; - } - - clickRun(event) { - this.cache = !(event.shiftKey && event.altKey); - this.runPlan(); - this.cache = true; - } - - /** - * Get the tree and perform a REST request to execute it - */ - runPlan() { - this.$loading.set(true); - $('#run i').removeClass().addClass('fa fa-hourglass-half'); - const tree = this.getTree(); - if (tree === undefined) { - $('#run i').removeClass().addClass('fa fa-play'); - this._toast.warn('Please provide a plan to be executed.', 'no plan'); - this.$loading.set(false); - return; - } - if (!this._crud.executeAlg(this.webSocket, tree, this.cache, this.analyzeQuery)) { - $('#run i').removeClass().addClass('fa fa-play'); - this.$result.set(new RelationalResult('Could not establish a connection with the server.')); - this.$loading.set(false); - } - } - - createView(info: ViewInformation) { - this._crud.executeAlg( - this.webSocket, - this.getTree(), - this.cache, - this.analyzeQuery, - true, - info.tableType, - info.newViewName, - info.stores, - info.freshness, - info.interval as unknown as string, - info.timeUnit); - } - - - /** - * Get the topmost node to be able to iterate the tree - * The topmost node is the one that has no outgoing connections - */ - getTopNode() { - let topNode: Node; - const haveOutgoingConnections = new Map(); - this.connections.forEach((v, k) => { - haveOutgoingConnections.set(v.source.id, v.source); - if (!haveOutgoingConnections.get(v.target.id)) { - topNode = v.target; - } - }); - return this.nodes.get(topNode.id); - } - - /** - * Get the whole tree by getting the topmost node and adding all children using the walkTree method - */ - getTree(): Node { - let tree; - if (this.connections.size === 0) { - if (this.nodes.size === 1) { - //get only node in Map - tree = this.walkTree(this.nodes.values().next().value.clone()); - } else { - //$('#run i').removeClass().addClass('fa fa-play'); - //this._toast.warn( 'Please provide a plan to be executed.', 'no plan' ); - return undefined; - } - } else { - tree = this.walkTree(this.getTopNode().clone()); - } - return tree; - } - - /** - * Add all children to a node recursively - */ - walkTree(node: Node): Node { - const children = []; - this.connections.forEach((v, k) => { - if (v.target.id === node.id) { - children.push(this.nodes.get(this.walkTree(v.source).id)); - // children.push( this.walkTree(v.source) ); - } - }); - node.children = children; - node.inputCount = children.length; - return node; - } - - - /** - * Set temporal values when dragging a node - */ - dragStart(e, node: Node) { - const $bg = $('#wrapper'); - this.scrollTop = $bg.scrollTop(); - this.scrollLeft = $bg.scrollLeft(); - this.nodes.get(node.id).dragging = true; - } - - /** - * Set temporal values when dragging a node - */ - draggingNode(e, node: Node) { - const $bg = $('#wrapper'); - - this.draggingNodeX = node.$left() + e.distance.x + $bg.scrollLeft() - this.scrollLeft; - this.draggingNodeY = node.$top() + e.distance.y + $bg.scrollTop() - this.scrollTop; - } - - /** - * Save the position of a node when it was moved - */ - savePos(e, node: Node) { - const $bg = $('#drop'); - this.nodes.get(node.id).dragging = false; - const nodeElement = $('#' + node.id); - const nodeWidth = nodeElement.width(); - const nodeHeight = nodeElement.height(); - const scrollTopDistance = $bg.scrollTop() - this.scrollTop; - const scrollLeftDistance = $bg.scrollLeft() - this.scrollLeft; - console.log(node.$left() + e.distance.x + scrollLeftDistance); - node.$left.set(Math.max(0, Math.min(node.$left() + e.distance.x + scrollLeftDistance, this.dropArea.nativeElement.offsetWidth - nodeWidth - 4))); - node.$top.set(Math.max(0, Math.min(node.$top() + e.distance.y + scrollTopDistance, this.dropArea.nativeElement.offsetHeight - nodeHeight - 4))); - this.draggingNodeX = null; - this.draggingNodeY = null; - } - - trackNode(index: number, e: KeyValue) { - if (e.value.id) { - return e.value.id; - } - return 1; - } - - /** - * Export the tree as JSON and save it to the clipboard - */ - exportTree() { - if (this.nodes.size === 0) { - return; - } - // see https://2ality.com/2015/08/es6-map-json.html - const out = {nodes: [...this.nodes], connections: [...this.connections]}; - this._util.clipboard(JSON.stringify(out)); - this._toast.success('The plan was exported to JSON and copied to your clipboard', null, 'exported'); - } - - /** - * Import a tree in the JSON format - */ - importTree() { - const input = prompt('Please paste your plan here.'); - if (input === null || input === '') { - return; - } - const inputObj = JSON.parse(input); - if (inputObj.nodes) { - const importedNodes = new Map(); - for (const [k, v] of Object.entries(inputObj.nodes)) { - importedNodes.set(v[0], Node.fromJson(v[1], this.dropArea.nativeElement.offsetWidth, this.dropArea.nativeElement.offsetHeight)); - } - this.nodes = importedNodes; - this.counter = importedNodes.size; - } - if (inputObj.connections) { - const importedConnections = new Map(); - for (const conn of Object.values(inputObj.connections)) { - importedConnections.set(conn[0], { - id: conn[1].id, - source: this.nodes.get(conn[1].source.id), - target: this.nodes.get(conn[1].target.id) - }); - } - this.connections = importedConnections; - } - this.setAutocomplete(); - } - - /** - * Calculates tree height of a balanced tree - */ - treeHeight() { - return Math.floor(Math.log2(this.counter)); - } - - /** - * Formats all nodes in the working field - */ - formatNodesTree() { - const height = this.treeHeight(); - let leftPadding = 0; - let upperPadding = 0; - let ind = 0; - for (let i = 0; i <= height; i++) { - upperPadding = i * 250; - for (let j = 0; j <= Math.pow(2, i) - 1; j++) { - leftPadding = 300 * j; - this.nodes.get('node' + ind).$left.set(leftPadding); - this.nodes.get('node' + ind).$top.set(upperPadding); - - ind++; - - } - - } - - } - - /** - * Formats all nodes as a square in the working field - */ - formatNodesSquare() { - const height = this.treeHeight(); - let leftPadding = 0; - let upperPadding = 0; - let ind = 0; - const edge = Math.ceil(Math.sqrt(this.counter)); - for (let i = 0; i < edge; i++) { - upperPadding = i * 250; - for (let j = 0; j < edge; j++) { - leftPadding = 300 * j; - this.nodes.get('node' + ind).$left.set(leftPadding); - this.nodes.get('node' + ind).$top.set(upperPadding); - - ind++; - - } - - } - } - - - /** - * Parses the whole JSON string received from the Query by Gesture Bridge and puts the parsed tree into the - * Pan Builder working field. - * @param data is JSON string received over the Web SOcket - */ - parseJson(data) { - const input = data; - if (input === null || input === '') { - return; - } - const inputObj = JSON.parse(data); - if (inputObj.nodes) { - const importedNodes = new Map(); - for (const [k, v] of Object.entries(inputObj.nodes)) { - importedNodes.set(v[0], Node.fromJson(v[1], this.dropArea.nativeElement.offsetWidth, this.dropArea.nativeElement.offsetHeight)); - } - this.nodes = importedNodes; - this.counter = importedNodes.size; - } - if (inputObj.connections) { - const importedConnections = new Map(); - for (const conn of Object.values(inputObj.connections)) { - importedConnections.set(conn[0], { - id: conn[1].id, - source: this.nodes.get(conn[1].source.id), - target: this.nodes.get(conn[1].target.id) - }); - } - this.connections = importedConnections; - } - this.setAutocomplete(); - } - - - toggleCache(b: boolean) { - this.cache = b; - } - -} diff --git a/src/app/views/querying/algebra/algebra.model.ts b/src/app/views/querying/algebra/algebra.model.ts deleted file mode 100644 index 7ff9970a..00000000 --- a/src/app/views/querying/algebra/algebra.model.ts +++ /dev/null @@ -1,182 +0,0 @@ -import {SortState} from '../../../components/data-view/models/sort-state.model'; -import {SidebarNode} from '../../../models/sidebar-node.model'; -import {signal, WritableSignal} from '@angular/core'; - -export enum LogicalOperator { - Scan = 'RelScan', - Join = 'Join', - Filter = 'RelFilter', - Project = 'RelProject', - Aggregate = 'Aggregate', - Minus = 'Minus', - Sort = 'Sort', - Union = 'Union', - Intersect = 'Intersect' -} - -export class LogicalOperatorUtil { - static operatorToSidebarNode(operator: LogicalOperator): SidebarNode { - let sidebarNode: SidebarNode; - switch (operator) { - case LogicalOperator.Scan: - sidebarNode = new SidebarNode('operator_' + operator, operator, 'fa fa-database', null, true); - break; - case LogicalOperator.Join: - sidebarNode = new SidebarNode('operator_' + operator, operator, null, null, true).setAlgSymbol('⋈'); - break; - case LogicalOperator.Filter: - sidebarNode = new SidebarNode('operator_' + operator, operator, null, null, true).setAlgSymbol('σ'); - break; - case LogicalOperator.Project: - sidebarNode = new SidebarNode('operator_' + operator, operator, null, null, true).setAlgSymbol('π'); - break; - case LogicalOperator.Aggregate: - sidebarNode = new SidebarNode('operator_' + operator, operator, 'fa fa-plus-circle', null, true); - break; - case LogicalOperator.Sort: - sidebarNode = new SidebarNode('operator_' + operator, operator, 'fa fa-arrows-v', null, true); - break; - case LogicalOperator.Union: - sidebarNode = new SidebarNode('operator_' + operator, operator, null, null, true).setAlgSymbol('∪'); - break; - case LogicalOperator.Minus: - sidebarNode = new SidebarNode('operator_' + operator, operator, 'fa fa-minus-circle', null, true); - break; - case LogicalOperator.Intersect: - sidebarNode = new SidebarNode('operator_' + operator, operator, null, null, true).setAlgSymbol('∩'); - break; - default: - sidebarNode = new SidebarNode('operator_' + operator, operator, 'fa fa-arrows', null, true); - } - return sidebarNode.setAutoActive(false); - } -} - -export interface Connection { - id: string; - source: Node; - target: Node; -} - -export type AlgNodeModel = { - name: string; - inputs: number; - icon: string; - symbol: string; - type: AlgType; -} - -export enum AlgType { - Join = 'Join', - Filter = 'Filter', - Project = 'Project', - Scan = 'Scan', - Aggregate = 'Aggregate', - Union = 'Union', - Sort = 'Sort', - Minus = 'Minus', - Modify = 'Modify', - ModifyCollect = 'ModifyCollect', - Intersect = 'Intersect', -} - -export class Node { - children: Node[] = []; - inputCount = 0; - dragging: boolean; - height: number; - width: number; - icon: string; - algSymbol: string; - class: string; - - //autocomplete - autocomplete: string[]; - acColumns = new Set(); - acTableColumns = new Set(); - acSchema = new Set(); - acTable = new Set(); - zIndex = 1; - - //parameters: - //Scan - entityName: string; - entityType: String; - - //Join - join = 'INNER'; - operator = '='; - col1: string; - col2: string; - - //filter - //(operator) - field: string; - filter: string; - - //project - fields: string[] = ['']; - - //aggregate - groupBy: string; - aggregation = 'SUM'; - alias: string; - //(field) - - //sort - sortColumns: SortState[] = [new SortState()]; - - //union, minus, intersect - all = false; - - //additional information needed for Views - tableTypes: String[]; - initialNames: String[]; - public $left: WritableSignal; - public $top: WritableSignal; - - constructor( - public id: string, - public type: AlgType, - left: number, - top: number - ) { - this.$left = signal(left); - this.$top = signal(top); - this.dragging = false; - } - - static fromJson(o: any, width: number, height: number) { - const n = new Node(o.id, o.type, o.left, o.top); - for (const [key, val] of Object.entries(o)) { - n[key] = o[key]; - } - //handle sets - const sets = ['acColumns', 'acTableColumns', 'acSchema', 'acTable']; - for (const s of sets) { - if (o[s].size !== undefined) { - n[s] = new Set([...o[s]]); - } else { - n[s] = new Set(); - } - } - //make sure, it is in the working area - if (n.$left() > width) { - n.$left.set(width - 260); - } - if (n.$top() > height) { - n.$top.set(height - 100); - } - - return n; - } - - clone() { - const n = new Node(this.id, this.type, this.$left(), this.$top()); - for (const [key, val] of Object.entries(this)) { - n[key] = val; - } - return n; - } - -} diff --git a/src/app/views/querying/algebra/node/node.component.html b/src/app/views/querying/algebra/node/node.component.html deleted file mode 100644 index 9489cf95..00000000 --- a/src/app/views/querying/algebra/node/node.component.html +++ /dev/null @@ -1,243 +0,0 @@ - -
    -
    - - - -
  • -
    - - - -
    -
  • - - -
  • -
    - - -
    -
  • - -
  • -
    - - -
    -
  • - -
  • -
    - - -
    -
  • -
  • -
    - - -
    -
  • -
    - - -
  • -
    - - -
    -
  • -
    - - -
    -
  • -
  • -
    - - -
    -
  • -
    - - -
  • - + add field -
  • -
  • -
    - - -
    -
  • -
    - - - -
  • -
    - - -
    -
  • -
  • -
    - - -
    -
  • -
  • -
    - - -
    -
  • -
  • -
    - - -
    -
  • -
    - - -
  • - + add column -
  • -
  • -
    - -
     
    - - - -
    -
  • -
    - -
  • -
    - - -
    -
  • - -
  • -
    - - -
    -
  • - -
  • -
    - - -
    -
  • - -
    - - - - - - - -
    -
    diff --git a/src/app/views/querying/algebra/node/node.component.scss b/src/app/views/querying/algebra/node/node.component.scss deleted file mode 100644 index 7b449d83..00000000 --- a/src/app/views/querying/algebra/node/node.component.scss +++ /dev/null @@ -1,43 +0,0 @@ -.param-wrapper label { - white-space: nowrap; -} - -.sort-wrapper:not(:last-child) { - margin-bottom: 5px; -} - -.sort-direction { - margin-left: 0.5em; - width: 7em; -} - -.sort-remove { - cursor: pointer; - line-height: 1.8; - margin-left: 0.5em; - color: #73818f; -} - -.sort-remove:hover { - color: initial; -} - -.sort-wrapper { - height: 30px; -} - -.sort-wrapper .icon-menu { - line-height: 1.8; - margin-right: 0.5em; - cursor: ns-resize; -} - -.sort-placeholder { - background: #f0f3f5; - margin: 5px 0; - border-radius: 0.25rem; -} - -.cdk-drag-preview { - display: flex; -} diff --git a/src/app/views/querying/algebra/node/node.component.ts b/src/app/views/querying/algebra/node/node.component.ts deleted file mode 100644 index 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 )}}
    + + +
    + + +
    + + + + + + + + + {{ 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:' }} +

    + + + +
    + + + + +
    + + + +
    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. +

    + + +
    + + + +
    + + + +
    Execute Plan
    + +
    + + Please specify the dynamic parameters: + + {{param[0]}}:{{param[1]}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    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 -}