From 8b5362c46a3ff40e57ea1b0721a19a049a962899 Mon Sep 17 00:00:00 2001 From: datomo Date: Wed, 20 Mar 2024 10:31:42 +0100 Subject: [PATCH] changed editor config to adjusted one and reformated --- protractor.conf.js | 28 - src/app/_nav.ts | 610 +++--- src/app/app-routing.module.ts | 88 +- src/app/app.component.spec.ts | 26 +- src/app/app.component.ts | 26 +- src/app/app.module.ts | 205 +- .../components/breadcrumb/breadcrumb-item.ts | 14 +- .../breadcrumb/breadcrumb.component.ts | 64 +- .../breadcrumb/breadcrumb.service.ts | 174 +- src/app/components/components.module.ts | 350 +-- .../data-card/data-card.component.ts | 44 +- .../data-graph/data-graph.component.ts | 1262 +++++------ .../data-table/data-table.component.ts | 252 +-- .../data-view/data-table/entity-config.ts | 14 +- .../data-template/data-template.component.ts | 997 ++++----- .../data-view/data-view.component.ts | 246 ++- .../components/data-view/data-view.model.ts | 183 +- .../expandable-text.component.ts | 20 +- .../data-view/input/input.component.ts | 402 ++-- .../json-text/json-text.component.ts | 38 +- .../data-view/media/media.component.ts | 56 +- .../models/pagination-element.model.ts | 46 +- .../data-view/models/result-set.model.ts | 524 ++--- .../data-view/models/sort-state.model.ts | 50 +- .../data-view/multiple-switch.pipe.ts | 8 +- src/app/components/data-view/shared-module.ts | 14 +- .../data-view/view/view.component.ts | 292 +-- .../delete-confirm.component.ts | 30 +- .../dockeredit/dockeredit.component.scss | 4 +- .../docker/dockeredit/dockeredit.component.ts | 368 ++-- .../dockerhandshake.component.ts | 34 +- .../docker/dockernew/dockernew.component.ts | 222 +- .../dockersettings.component.ts | 98 +- .../dynamic-forms.component.spec.ts | 32 +- .../dynamic-forms/dynamic-forms.component.ts | 102 +- src/app/components/editor/editor.component.ts | 320 +-- src/app/components/graph/graph.component.ts | 370 ++-- .../information-manager.component.ts | 132 +- .../render-item/render-item.component.ts | 272 +-- .../components/json/json-editor.component.ts | 434 ++-- .../json/json-elem/json-elem.component.ts | 409 ++-- .../left-sidebar/left-sidebar.component.scss | 8 +- .../left-sidebar.component.spec.ts | 32 +- .../left-sidebar/left-sidebar.component.ts | 282 +-- .../left-sidebar/left-sidebar.service.ts | 428 ++-- .../loading-screen.component.ts | 18 +- .../loading-screen/loading-screen.service.ts | 32 +- .../right-sidebar/right-sidebar.component.ts | 98 +- .../toast-exposer.component.scss | 1 - .../toast-exposer/toast-exposer.component.ts | 46 +- .../toast-exposer/toast/toast.component.ts | 76 +- .../components/toast-exposer/toaster.model.ts | 74 +- .../toast-exposer/toaster.service.ts | 182 +- .../default-layout.component.ts | 158 +- .../plan-node/plan-node.component.ts | 412 ++-- .../plan-view/plan-view.component.ts | 86 +- .../explain-visualizer.module.ts | 52 +- src/app/explain-visualizer/models/enums.ts | 20 +- src/app/explain-visualizer/models/iplan.ts | 28 +- src/app/explain-visualizer/pipes.ts | 54 +- .../services/color.service.ts | 108 +- .../services/help.service.ts | 36 +- .../services/plan.service.ts | 390 ++-- .../services/syntax-highlight.service.ts | 348 +-- src/app/models/catalog.model.ts | 210 +- src/app/models/docker.model.ts | 64 +- src/app/models/information-page.model.ts | 120 +- src/app/models/sidebar-button.model.ts | 16 +- src/app/models/sidebar-node.model.ts | 243 +-- src/app/models/ui-request.model.ts | 534 ++--- src/app/pipes/pipes.ts | 26 +- .../edit-notebook/edit-notebook.component.ts | 1432 ++++++------- .../nb-cell/nb-cell.component.ts | 488 ++--- .../nb-input-editor.component.ts | 110 +- .../nb-output-data.component.ts | 18 +- .../nb-poly-output.component.ts | 38 +- .../edit-notebook/notebook-wrapper.ts | 952 +++++---- .../manage-notebook.component.ts | 386 ++-- .../notebooks-dashboard.component.ts | 328 +-- .../components/notebooks.component.ts | 679 +++--- .../notebooks/models/kernel-response.model.ts | 118 +- .../notebooks/models/notebook.model.ts | 116 +- src/app/plugins/notebooks/notebooks.module.ts | 128 +- .../services/notebooks-content.service.ts | 820 +++---- .../services/notebooks-sidebar.service.ts | 334 +-- .../notebooks/services/notebooks-webSocket.ts | 154 +- .../notebooks/services/notebooks.service.ts | 381 ++-- .../notebooks/services/safe-html.pipe.ts | 12 +- .../services/unsaved-changes.guard.ts | 16 +- src/app/services/auth.service.ts | 96 +- src/app/services/catalog.service.ts | 705 +++--- src/app/services/config.service.ts | 148 +- src/app/services/crud.service.ts | 1376 ++++++------ src/app/services/dbms-types.service.ts | 392 ++-- src/app/services/information.service.ts | 180 +- src/app/services/plugin.service.ts | 68 +- ...ht-sidebar-to-relationalalgebra.service.ts | 14 +- src/app/services/util.service.ts | 88 +- src/app/services/webSocket.ts | 100 +- src/app/services/webui-settings.service.ts | 180 +- src/app/views/about/about.component.ts | 18 +- src/app/views/adapters/adapter.model.ts | 80 +- src/app/views/adapters/adapters.component.ts | 1089 +++++----- .../views/dashboard/dashboard.component.ts | 272 +-- .../dockerconfig/dockerconfig.component.ts | 320 +-- src/app/views/error/404.component.ts | 6 +- src/app/views/error/500.component.ts | 6 +- .../file-uploader/file-uploader.component.ts | 100 +- .../form-generator.component.spec.ts | 32 +- .../form-generator.component.ts | 706 +++--- .../form-generator/form-generator.model.ts | 6 +- src/app/views/login/login.component.ts | 100 +- .../views/monitoring/monitoring.component.ts | 224 +- .../query-interfaces.component.ts | 439 ++-- .../query-interfaces.model.ts | 38 +- .../querying/algebra/algebra.component.ts | 56 +- .../views/querying/algebra/algebra.model.ts | 2 +- .../querying/console/console.component.ts | 690 +++--- .../querying/console/query-history.model.ts | 98 +- .../graphical-querying.component.spec.ts | 32 +- .../graphical-querying.component.ts | 890 ++++---- .../refinement-options.component.ts | 454 ++-- src/app/views/querying/querying.component.ts | 32 +- .../document-edit-collection.component.ts | 196 +- .../document-edit-collections.component.ts | 502 ++--- .../edit-columns/edit-columns.component.ts | 1890 +++++++++-------- .../edit-entity/edit-entity.component.ts | 205 +- .../edit-source-columns.component.ts | 358 ++-- .../edit-tables/edit-tables.component.ts | 687 +++--- .../graph-edit-graph.component.ts | 172 +- .../schema-editing.component.ts | 409 ++-- .../statistics-column.component.ts | 74 +- .../views/table-view/table-view.component.ts | 92 +- src/app/views/uml/uml.component.spec.ts | 32 +- src/app/views/uml/uml.component.ts | 534 ++--- src/app/views/uml/uml.model.ts | 88 +- .../reload-button/reload-button.component.ts | 24 +- src/app/views/views-routing.module.ts | 336 +-- src/app/views/views.module.ts | 278 +-- src/test.ts | 4 +- 140 files changed, 17446 insertions(+), 17024 deletions(-) delete mode 100644 protractor.conf.js diff --git a/protractor.conf.js b/protractor.conf.js deleted file mode 100644 index 7ee3b5ee..00000000 --- a/protractor.conf.js +++ /dev/null @@ -1,28 +0,0 @@ -// Protractor configuration file, see link for more information -// https://github.com/angular/protractor/blob/master/lib/config.ts - -const { SpecReporter } = require('jasmine-spec-reporter'); - -exports.config = { - allScriptsTimeout: 11000, - specs: [ - './e2e/**/*.e2e-spec.ts' - ], - capabilities: { - 'browserName': 'chrome' - }, - directConnect: true, - baseUrl: 'http://localhost:4200/', - framework: 'jasmine', - jasmineNodeOpts: { - showColors: true, - defaultTimeoutInterval: 30000, - print: function() {} - }, - onPrepare() { - require('ts-node').register({ - project: 'e2e/tsconfig.e2e.json' - }); - jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); - } -}; diff --git a/src/app/_nav.ts b/src/app/_nav.ts index 8d2fa6dc..164fb009 100644 --- a/src/app/_nav.ts +++ b/src/app/_nav.ts @@ -1,338 +1,338 @@ interface NavAttributes { - [propName: string]: any; + [propName: string]: any; } interface NavWrapper { - attributes: NavAttributes; - element: string; + attributes: NavAttributes; + element: string; } interface NavBadge { - text: string; - variant: string; + text: string; + variant: string; } interface NavLabel { - class?: string; - variant: string; + class?: string; + variant: string; } export interface NavData { - name?: string; - url?: string; - icon?: string; - badge?: NavBadge; - title?: boolean; - children?: NavData[]; - variant?: string; - attributes?: NavAttributes; - divider?: boolean; - class?: string; - label?: NavLabel; - wrapper?: NavWrapper; + name?: string; + url?: string; + icon?: string; + badge?: NavBadge; + title?: boolean; + children?: NavData[]; + variant?: string; + attributes?: NavAttributes; + divider?: boolean; + class?: string; + label?: NavLabel; + wrapper?: NavWrapper; } export const navItems: NavData[] = [ - { - name: 'Dashboard', - url: '/dashboard', - icon: 'icon-speedometer', - /*badge: { - variant: 'info', - text: 'NEW' - }*/ - }, - { - name: 'Console', - url: '/console', - icon: 'icon-note' - }, - { - name: 'Data Table', - url: '/data-table', - icon: 'fa fa-table' - }, - { - name: 'Edit Columns', - url: '/edit-columns', - icon: 'icon-list' - }, - { - name: 'Graphical Querying', - url: '/graphical-querying', - icon: 'icon-layers' - }, - { - name: 'dynamic forms', - url: '/config', - icon: 'icon-drawer' - }, - { - name: 'Test', - icon: 'icon-puzzle', - attributes: {target: '_blank', rel: 'noopener'}, - children: [ - { - name: 'Test', - url: '/uml', - icon: 'icon-puzzle' - }, - { - name: 'Test', - url: '/uml', - icon: 'icon-puzzle' - } - ] - }, - { - name: 'test-nesting', - url: '#', - icon: 'icon-drawer', - class: 'test-class1 test-class2', - children: [ - { + { + name: 'Dashboard', + url: '/dashboard', + icon: 'icon-speedometer', + /*badge: { + variant: 'info', + text: 'NEW' + }*/ + }, + { + name: 'Console', + url: '/console', + icon: 'icon-note' + }, + { + name: 'Data Table', + url: '/data-table', + icon: 'fa fa-table' + }, + { + name: 'Edit Columns', + url: '/edit-columns', + icon: 'icon-list' + }, + { + name: 'Graphical Querying', + url: '/graphical-querying', + icon: 'icon-layers' + }, + { + name: 'dynamic forms', + url: '/config', + icon: 'icon-drawer' + }, + { name: 'Test', - url: '#', icon: 'icon-puzzle', + attributes: {target: '_blank', rel: 'noopener'}, children: [ - { - name: 'Test', - url: '#', - icon: 'icon-puzzle' - }, - { - name: 'Test', - url: '#', - icon: 'icon-puzzle', - children: [ - { + { name: 'Test', - url: '#', + url: '/uml', icon: 'icon-puzzle' - }, - { + }, + { name: 'Test', - url: '#', + url: '/uml', icon: 'icon-puzzle' - } - ] - } + } ] - }, - { - name: 'Test', + }, + { + name: 'test-nesting', url: '#', - icon: 'icon-puzzle' - } - ] - }, - { - title: true, - name: 'Theme' - }, - { - name: 'Colors', - url: '/theme/colors', - icon: 'icon-drop' - }, - { - name: 'Typography', - url: '/theme/typography', - icon: 'icon-pencil' - }, - { - title: true, - name: 'Components' - }, - { - name: 'Base', - url: '/base', - icon: 'icon-puzzle', - children: [ - { - name: 'Cards', - url: '/base/cards', - icon: 'icon-puzzle' - }, - { - name: 'Carousels', - url: '/base/carousels', - icon: 'icon-puzzle' - }, - { - name: 'Collapses', - url: '/base/collapses', - icon: 'icon-puzzle' - }, - { - name: 'Forms', - url: '/base/forms', - icon: 'icon-puzzle' - }, - { - name: 'Pagination', - url: '/base/paginations', - icon: 'icon-puzzle' - }, - { - name: 'Popovers', - url: '/base/popovers', - icon: 'icon-puzzle' - }, - { - name: 'Progress', - url: '/base/progress', - icon: 'icon-puzzle' - }, - { - name: 'Switches', - url: '/base/switches', - icon: 'icon-puzzle' - }, - { - name: 'Tables', - url: '/base/tables', - icon: 'icon-puzzle' - }, - { - name: 'Tabs', - url: '/base/tabs', - icon: 'icon-puzzle' - }, - { - name: 'Tooltips', - url: '/base/tooltips', - icon: 'icon-puzzle' - } - ] - }, - { - name: 'Buttons', - url: '/buttons', - icon: 'icon-cursor', - children: [ - { + icon: 'icon-drawer', + class: 'test-class1 test-class2', + children: [ + { + name: 'Test', + url: '#', + icon: 'icon-puzzle', + children: [ + { + name: 'Test', + url: '#', + icon: 'icon-puzzle' + }, + { + name: 'Test', + url: '#', + icon: 'icon-puzzle', + children: [ + { + name: 'Test', + url: '#', + icon: 'icon-puzzle' + }, + { + name: 'Test', + url: '#', + icon: 'icon-puzzle' + } + ] + } + ] + }, + { + name: 'Test', + url: '#', + icon: 'icon-puzzle' + } + ] + }, + { + title: true, + name: 'Theme' + }, + { + name: 'Colors', + url: '/theme/colors', + icon: 'icon-drop' + }, + { + name: 'Typography', + url: '/theme/typography', + icon: 'icon-pencil' + }, + { + title: true, + name: 'Components' + }, + { + name: 'Base', + url: '/base', + icon: 'icon-puzzle', + children: [ + { + name: 'Cards', + url: '/base/cards', + icon: 'icon-puzzle' + }, + { + name: 'Carousels', + url: '/base/carousels', + icon: 'icon-puzzle' + }, + { + name: 'Collapses', + url: '/base/collapses', + icon: 'icon-puzzle' + }, + { + name: 'Forms', + url: '/base/forms', + icon: 'icon-puzzle' + }, + { + name: 'Pagination', + url: '/base/paginations', + icon: 'icon-puzzle' + }, + { + name: 'Popovers', + url: '/base/popovers', + icon: 'icon-puzzle' + }, + { + name: 'Progress', + url: '/base/progress', + icon: 'icon-puzzle' + }, + { + name: 'Switches', + url: '/base/switches', + icon: 'icon-puzzle' + }, + { + name: 'Tables', + url: '/base/tables', + icon: 'icon-puzzle' + }, + { + name: 'Tabs', + url: '/base/tabs', + icon: 'icon-puzzle' + }, + { + name: 'Tooltips', + url: '/base/tooltips', + icon: 'icon-puzzle' + } + ] + }, + { name: 'Buttons', - url: '/buttons/buttons', - icon: 'icon-cursor' - }, - { - name: 'Dropdowns', - url: '/buttons/dropdowns', - icon: 'icon-cursor' - }, - { - name: 'Brand Buttons', - url: '/buttons/brand-buttons', - icon: 'icon-cursor' - } - ] - }, - { - name: 'Charts', - url: '/charts', - icon: 'icon-pie-chart' - }, - { - name: 'Icons', - url: '/icons', - icon: 'icon-star', - children: [ - { - name: 'CoreUI Icons', - url: '/icons/coreui-icons', + url: '/buttons', + icon: 'icon-cursor', + children: [ + { + name: 'Buttons', + url: '/buttons/buttons', + icon: 'icon-cursor' + }, + { + name: 'Dropdowns', + url: '/buttons/dropdowns', + icon: 'icon-cursor' + }, + { + name: 'Brand Buttons', + url: '/buttons/brand-buttons', + icon: 'icon-cursor' + } + ] + }, + { + name: 'Charts', + url: '/charts', + icon: 'icon-pie-chart' + }, + { + name: 'Icons', + url: '/icons', icon: 'icon-star', + children: [ + { + name: 'CoreUI Icons', + url: '/icons/coreui-icons', + icon: 'icon-star', + badge: { + variant: 'success', + text: 'NEW' + } + }, + { + name: 'Flags', + url: '/icons/flags', + icon: 'icon-star' + }, + { + name: 'Font Awesome', + url: '/icons/font-awesome', + icon: 'icon-star', + badge: { + variant: 'secondary', + text: '4.7' + } + }, + { + name: 'Simple Line Icons', + url: '/icons/simple-line-icons', + icon: 'icon-star' + } + ] + }, + { + name: 'Notifications', + url: '/notifications', + icon: 'icon-bell', + children: [ + { + name: 'Alerts', + url: '/notifications/alerts', + icon: 'icon-bell' + }, + { + name: 'Badges', + url: '/notifications/badges', + icon: 'icon-bell' + }, + { + name: 'Modals', + url: '/notifications/modals', + icon: 'icon-bell' + } + ] + }, + { + name: 'Widgets', + url: '/widgets', + icon: 'icon-calculator', badge: { - variant: 'success', - text: 'NEW' + variant: 'info', + text: 'NEW' } - }, - { - name: 'Flags', - url: '/icons/flags', - icon: 'icon-star' - }, - { - name: 'Font Awesome', - url: '/icons/font-awesome', + }, + { + divider: true + }, + { + title: true, + name: 'Extras', + }, + { + name: 'Pages', + url: '/pages', icon: 'icon-star', - badge: { - variant: 'secondary', - text: '4.7' - } - }, - { - name: 'Simple Line Icons', - url: '/icons/simple-line-icons', - icon: 'icon-star' - } - ] - }, - { - name: 'Notifications', - url: '/notifications', - icon: 'icon-bell', - children: [ - { - name: 'Alerts', - url: '/notifications/alerts', - icon: 'icon-bell' - }, - { - name: 'Badges', - url: '/notifications/badges', - icon: 'icon-bell' - }, - { - name: 'Modals', - url: '/notifications/modals', - icon: 'icon-bell' - } - ] - }, - { - name: 'Widgets', - url: '/widgets', - icon: 'icon-calculator', - badge: { - variant: 'info', - text: 'NEW' - } - }, - { - divider: true - }, - { - title: true, - name: 'Extras', - }, - { - name: 'Pages', - url: '/pages', - icon: 'icon-star', - children: [ - { - name: 'Login', - url: '/login', - icon: 'icon-star' - }, - { - name: 'Register', - url: '/register', - icon: 'icon-star' - }, - { - name: 'Error 404', - url: '/404', - icon: 'icon-star' - }, - { - name: 'Error 500', - url: '/500', - icon: 'icon-star' - } - ] - }/*, + children: [ + { + name: 'Login', + url: '/login', + icon: 'icon-star' + }, + { + name: 'Register', + url: '/register', + icon: 'icon-star' + }, + { + name: 'Error 404', + url: '/404', + icon: 'icon-star' + }, + { + name: 'Error 500', + url: '/500', + icon: 'icon-star' + } + ] + }/*, { name: 'Disabled', url: '/dashboard', diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 2400a53b..9d471aad 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -7,54 +7,54 @@ import {LoginComponent} from './views/login/login.component'; export const routes: Routes = [ - { - path: '404', - component: P404Component, - data: { - title: 'Page 404' - } - }, - { - path: '500', - component: P500Component, - data: { - title: 'Page 500' - } - }, - { - path: 'login', - component: LoginComponent, - data: { - title: 'Login Page' - } - }, - { - path: '', - pathMatch: 'full', - redirectTo: 'views/monitoring' - }, - { - path: '', - component: DefaultLayoutComponent, - data: { - title: 'Home' + { + path: '404', + component: P404Component, + data: { + title: 'Page 404' + } }, - children: [ - { - path: 'views', - loadChildren: () => import('./views/views.module').then(m => m.ViewsModule) - } - ] - }, - {path: '**', component: P404Component} + { + path: '500', + component: P500Component, + data: { + title: 'Page 500' + } + }, + { + path: 'login', + component: LoginComponent, + data: { + title: 'Login Page' + } + }, + { + path: '', + pathMatch: 'full', + redirectTo: 'views/monitoring' + }, + { + path: '', + component: DefaultLayoutComponent, + data: { + title: 'Home' + }, + children: [ + { + path: 'views', + loadChildren: () => import('./views/views.module').then(m => m.ViewsModule) + } + ] + }, + {path: '**', component: P404Component} ]; @NgModule({ - imports: [ - RouterModule, - RouterModule.forRoot(routes) - ], - exports: [RouterModule] + imports: [ + RouterModule, + RouterModule.forRoot(routes) + ], + exports: [RouterModule] }) export class AppRoutingModule { } diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 74f10a66..d5202eca 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -3,17 +3,17 @@ import {TestBed, waitForAsync} from '@angular/core/testing'; import {AppComponent} from './app.component'; describe('AppComponent', () => { - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [ - AppComponent - ], - imports: [RouterTestingModule] - }).compileComponents(); - })); - it('should create the app', waitForAsync(() => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.debugElement.componentInstance; - expect(app).toBeTruthy(); - })); + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + AppComponent + ], + imports: [RouterTestingModule] + }).compileComponents(); + })); + it('should create the app', waitForAsync(() => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app).toBeTruthy(); + })); }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 2c62fc87..82a50070 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -2,20 +2,20 @@ import {Component, OnInit} from '@angular/core'; import {NavigationEnd, Router} from '@angular/router'; @Component({ - // tslint:disable-next-line - selector: 'body', - templateUrl: './app.component.html' + // tslint:disable-next-line + selector: 'body', + templateUrl: './app.component.html' }) export class AppComponent implements OnInit { - constructor(private router: Router) { - } + constructor(private router: Router) { + } - ngOnInit() { - this.router.events.subscribe((evt) => { - if (!(evt instanceof NavigationEnd)) { - return; - } - window.scrollTo(0, 0); - }); - } + ngOnInit() { + this.router.events.subscribe((evt) => { + if (!(evt instanceof NavigationEnd)) { + return; + } + window.scrollTo(0, 0); + }); + } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 19a8e0b0..228982cb 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -19,7 +19,48 @@ import {TabsModule} from 'ngx-bootstrap/tabs'; import {NgChartsModule} from 'ng2-charts'; import {HttpClientModule} from '@angular/common/http'; import {ComponentsModule} from './components/components.module'; -import {AvatarComponent, BreadcrumbComponent, ButtonCloseDirective, ButtonDirective, CardBodyComponent, CardComponent, ColComponent, ContainerComponent, DropdownComponent, DropdownItemDirective, DropdownMenuDirective, DropdownToggleDirective, FooterComponent, GutterDirective, HeaderBrandComponent, HeaderComponent, HeaderDividerComponent, HeaderNavComponent, HeaderTextComponent, HeaderTogglerDirective, ModalBodyComponent, ModalComponent, ModalFooterComponent, ModalHeaderComponent, ModalTitleDirective, NavItemComponent, NavLinkDirective, ProgressBarComponent, ProgressComponent, RowComponent, SidebarComponent, SidebarNavComponent, SidebarToggleDirective, SidebarTogglerComponent, ToastBodyComponent, ToastCloseDirective, ToastComponent, ToasterComponent, ToastHeaderComponent, TooltipDirective} from '@coreui/angular'; +import { + AvatarComponent, + BreadcrumbComponent, + ButtonCloseDirective, + ButtonDirective, + CardBodyComponent, + CardComponent, + ColComponent, + ContainerComponent, + DropdownComponent, + DropdownItemDirective, + DropdownMenuDirective, + DropdownToggleDirective, + FooterComponent, + GutterDirective, + HeaderBrandComponent, + HeaderComponent, + HeaderDividerComponent, + HeaderNavComponent, + HeaderTextComponent, + HeaderTogglerDirective, + ModalBodyComponent, + ModalComponent, + ModalFooterComponent, + ModalHeaderComponent, + ModalTitleDirective, + NavItemComponent, + NavLinkDirective, + ProgressBarComponent, + ProgressComponent, + RowComponent, + SidebarComponent, + SidebarNavComponent, + SidebarToggleDirective, + SidebarTogglerComponent, + ToastBodyComponent, + ToastCloseDirective, + ToastComponent, + ToasterComponent, + ToastHeaderComponent, + TooltipDirective +} from '@coreui/angular'; import {DefaultLayoutComponent} from './containers/default-layout'; import {P404Component} from './views/error/404.component'; import {P500Component} from './views/error/500.component'; @@ -34,87 +75,87 @@ import {IconDirective} from '@coreui/icons-angular'; @NgModule({ - imports: [ - ComponentsModule, - AppRoutingModule, - BrowserAnimationsModule, - BrowserModule, - BsDropdownModule.forRoot(), - TabsModule.forRoot(), - NgChartsModule, - // plugins - NotebooksModule, - ToastComponent, - NgChartsModule, - ToasterComponent, - // coreui / bootstrap - TooltipModule.forRoot(), - // forms - FormsModule, - ReactiveFormsModule, - BsDropdownModule, - TypeaheadModule.forRoot(), - HttpClientModule, - ViewsModule, - PopoverModule.forRoot(), - ModalModule.forRoot(), - NgxJsonViewerModule, - FooterComponent, - HeaderComponent, - ContainerComponent, - HeaderDividerComponent, - HeaderBrandComponent, - HeaderNavComponent, - NavItemComponent, - DropdownToggleDirective, - DropdownComponent, - NavLinkDirective, - HeaderTextComponent, - SidebarComponent, - SidebarToggleDirective, - NgOptimizedImage, - DropdownItemDirective, - DropdownMenuDirective, - HeaderTogglerDirective, - SidebarTogglerComponent, - SidebarNavComponent, - IconDirective, - RowComponent, - ColComponent, - GutterDirective, - ToastHeaderComponent, - BreadcrumbComponent, - ProgressComponent, - ToastBodyComponent, - ToastHeaderComponent, - ProgressBarComponent, - ToastCloseDirective, - ToasterComponent, - AvatarComponent, - ModalComponent, - ModalHeaderComponent, - ModalBodyComponent, - ModalFooterComponent, - ModalTitleDirective, - ButtonCloseDirective, - ButtonDirective, - CardComponent, - CardBodyComponent, - TooltipDirective - ], - declarations: [ - AppComponent, - DefaultLayoutComponent, - P404Component, - P500Component, - LoginComponent - ], - providers: [{ - provide: LocationStrategy, - useClass: HashLocationStrategy - }], - bootstrap: [AppComponent], - exports: [] + imports: [ + ComponentsModule, + AppRoutingModule, + BrowserAnimationsModule, + BrowserModule, + BsDropdownModule.forRoot(), + TabsModule.forRoot(), + NgChartsModule, + // plugins + NotebooksModule, + ToastComponent, + NgChartsModule, + ToasterComponent, + // coreui / bootstrap + TooltipModule.forRoot(), + // forms + FormsModule, + ReactiveFormsModule, + BsDropdownModule, + TypeaheadModule.forRoot(), + HttpClientModule, + ViewsModule, + PopoverModule.forRoot(), + ModalModule.forRoot(), + NgxJsonViewerModule, + FooterComponent, + HeaderComponent, + ContainerComponent, + HeaderDividerComponent, + HeaderBrandComponent, + HeaderNavComponent, + NavItemComponent, + DropdownToggleDirective, + DropdownComponent, + NavLinkDirective, + HeaderTextComponent, + SidebarComponent, + SidebarToggleDirective, + NgOptimizedImage, + DropdownItemDirective, + DropdownMenuDirective, + HeaderTogglerDirective, + SidebarTogglerComponent, + SidebarNavComponent, + IconDirective, + RowComponent, + ColComponent, + GutterDirective, + ToastHeaderComponent, + BreadcrumbComponent, + ProgressComponent, + ToastBodyComponent, + ToastHeaderComponent, + ProgressBarComponent, + ToastCloseDirective, + ToasterComponent, + AvatarComponent, + ModalComponent, + ModalHeaderComponent, + ModalBodyComponent, + ModalFooterComponent, + ModalTitleDirective, + ButtonCloseDirective, + ButtonDirective, + CardComponent, + CardBodyComponent, + TooltipDirective + ], + declarations: [ + AppComponent, + DefaultLayoutComponent, + P404Component, + P500Component, + LoginComponent + ], + providers: [{ + provide: LocationStrategy, + useClass: HashLocationStrategy + }], + bootstrap: [AppComponent], + exports: [] }) export class AppModule { } diff --git a/src/app/components/breadcrumb/breadcrumb-item.ts b/src/app/components/breadcrumb/breadcrumb-item.ts index cd4a4b7e..4ebcd31c 100644 --- a/src/app/components/breadcrumb/breadcrumb-item.ts +++ b/src/app/components/breadcrumb/breadcrumb-item.ts @@ -1,11 +1,11 @@ export class BreadcrumbItem { - name: string; - routerLink?: any; + name: string; + routerLink?: any; - constructor(name: string, routerLink?: any) { - this.name = name; - if (routerLink) { - this.routerLink = routerLink; + constructor(name: string, routerLink?: any) { + this.name = name; + if (routerLink) { + this.routerLink = routerLink; + } } - } } diff --git a/src/app/components/breadcrumb/breadcrumb.component.ts b/src/app/components/breadcrumb/breadcrumb.component.ts index afbf5fac..9efe18ae 100644 --- a/src/app/components/breadcrumb/breadcrumb.component.ts +++ b/src/app/components/breadcrumb/breadcrumb.component.ts @@ -2,52 +2,52 @@ import {Component, inject, OnInit, signal} from '@angular/core'; import {BreadcrumbService} from './breadcrumb.service'; @Component({ - selector: 'app-breadcrumb-main', - templateUrl: './breadcrumb.component.html', - styleUrls: ['./breadcrumb.component.scss'] + selector: 'app-breadcrumb-main', + templateUrl: './breadcrumb.component.html', + styleUrls: ['./breadcrumb.component.scss'] }) export class BreadcrumbComponent implements OnInit { - breadcrumbs: BreadcrumbItem[] = []; - zoom; - hidden = signal(true); + breadcrumbs: BreadcrumbItem[] = []; + zoom; + hidden = signal(true); - routerId; + routerId; - private readonly _breadcrumb = inject(BreadcrumbService); + private readonly _breadcrumb = inject(BreadcrumbService); - constructor() { - } + constructor() { + } - ngOnInit() { + ngOnInit() { - this.routerId = this._breadcrumb.routerId; - this.zoom = this._breadcrumb.getZoom(); + this.routerId = this._breadcrumb.routerId; + this.zoom = this._breadcrumb.getZoom(); - this._breadcrumb.getBreadcrumbs().subscribe(breadcrumbs => { - this.breadcrumbs = breadcrumbs; - this.hidden.set(breadcrumbs.length <= 0); - }); - } + this._breadcrumb.getBreadcrumbs().subscribe(breadcrumbs => { + this.breadcrumbs = breadcrumbs; + this.hidden.set(breadcrumbs.length <= 0); + }); + } - zoomIn() { - this.zoom = this._breadcrumb.zoomIn(); - } + zoomIn() { + this.zoom = this._breadcrumb.zoomIn(); + } - zoomOut() { - this.zoom = this._breadcrumb.zoomOut(); - } + zoomOut() { + this.zoom = this._breadcrumb.zoomOut(); + } } class BreadcrumbItem { - name: string; - routerLink?: any; - - constructor(name: string, routerLink?: any) { - this.name = name; - if (routerLink) { - this.routerLink = routerLink; + name: string; + routerLink?: any; + + constructor(name: string, routerLink?: any) { + this.name = name; + if (routerLink) { + this.routerLink = routerLink; + } } - } } diff --git a/src/app/components/breadcrumb/breadcrumb.service.ts b/src/app/components/breadcrumb/breadcrumb.service.ts index 7fa74f0e..080e5520 100644 --- a/src/app/components/breadcrumb/breadcrumb.service.ts +++ b/src/app/components/breadcrumb/breadcrumb.service.ts @@ -3,118 +3,118 @@ import {BehaviorSubject} from 'rxjs'; import {BreadcrumbItem} from './breadcrumb-item'; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class BreadcrumbService implements OnInit, OnDestroy { - //BehaviorSubjects: https://pillar-soft.com/2018/07/02/behavior-subjects-in-angular-6/ - breadcrumbs: BehaviorSubject = new BehaviorSubject([]); - MAXCOLS = 10; - zoom: number; - _showZoom = true; - tableName = null; - dataModel = null; - - routerId; + //BehaviorSubjects: https://pillar-soft.com/2018/07/02/behavior-subjects-in-angular-6/ + breadcrumbs: BehaviorSubject = new BehaviorSubject([]); + MAXCOLS = 10; + zoom: number; + _showZoom = true; + tableName = null; + dataModel = null; + + routerId; + + constructor() { + if (localStorage.getItem('breadcrumb.zoom') === null) { + localStorage.setItem('breadcrumb.zoom', String(this.MAXCOLS - 1)); + } + this.zoom = +localStorage.getItem('breadcrumb.zoom'); + } - constructor() { - if (localStorage.getItem('breadcrumb.zoom') === null) { - localStorage.setItem('breadcrumb.zoom', String(this.MAXCOLS - 1)); + ngOnInit() { } - this.zoom = +localStorage.getItem('breadcrumb.zoom'); - } - ngOnInit() { - } + ngOnDestroy() { + this.breadcrumbs.next([]); + } - ngOnDestroy() { - this.breadcrumbs.next([]); - } + private setZoom(zoom: number) { + this.zoom = zoom; + localStorage.setItem('breadcrumb.zoom', String(zoom)); + } - private setZoom(zoom: number) { - this.zoom = zoom; - localStorage.setItem('breadcrumb.zoom', String(zoom)); - } + zoomIn() { + if (this.zoom < this.MAXCOLS) { + this.setZoom(this.zoom + 1); + } + return this.zoom; + } - zoomIn() { - if (this.zoom < this.MAXCOLS) { - this.setZoom(this.zoom + 1); + zoomOut() { + if (this.zoom > 1) { + this.setZoom(this.zoom - 1); + } + return this.zoom; } - return this.zoom; - } - zoomOut() { - if (this.zoom > 1) { - this.setZoom(this.zoom - 1); + getZoom() { + return this.zoom; } - return this.zoom; - } - getZoom() { - return this.zoom; - } + getTableId() { + return this.tableName; + } - getTableId() { - return this.tableName; - } + getMasonryZoom() { + return this.MAXCOLS - (this.zoom + 1); + } - getMasonryZoom() { - return this.MAXCOLS - (this.zoom + 1); - } + getCardClass() { + //todo color.. + return ''; + } - getCardClass() { - //todo color.. - return ''; - } + getBreadcrumbs() { + return this.breadcrumbs.asObservable(); + } - getBreadcrumbs() { - return this.breadcrumbs.asObservable(); - } + public setBreadcrumbs(breadcrumbs: BreadcrumbItem[]) { + this.breadcrumbs.next(breadcrumbs); + this.showZoom(); + } - public setBreadcrumbs(breadcrumbs: BreadcrumbItem[]) { - this.breadcrumbs.next(breadcrumbs); - this.showZoom(); - } + public setDashboardBreadcrumbs(breadcrumbs: BreadcrumbItem[]) { + this.breadcrumbs.next(breadcrumbs); + this.hideZoom(); + } - public setDashboardBreadcrumbs(breadcrumbs: BreadcrumbItem[]) { - this.breadcrumbs.next(breadcrumbs); - this.hideZoom(); - } + public setBreadcrumbsSchema(breadcrumbs: BreadcrumbItem[], tableId: string) { + this.breadcrumbs.next(breadcrumbs); + this.hideZoom(); + this.tableName = tableId; + } - public setBreadcrumbsSchema(breadcrumbs: BreadcrumbItem[], tableId: string) { - this.breadcrumbs.next(breadcrumbs); - this.hideZoom(); - this.tableName = tableId; - } + hide() { + this.breadcrumbs.next([]); + } - hide() { - this.breadcrumbs.next([]); - } + hideZoom() { + this._showZoom = false; + } - hideZoom() { - this._showZoom = false; - } + showZoom() { + this._showZoom = true; + } - showZoom() { - this._showZoom = true; - } + getNamespaceName() { + if (this.breadcrumbs.getValue().length < 2) { + return null; + } + return this.breadcrumbs.getValue()[1].name; + } - getNamespaceName() { - if (this.breadcrumbs.getValue().length < 2) { - return null; + isRelational() { + return this.dataModel != null && this.dataModel === 'relational'; } - return this.breadcrumbs.getValue()[1].name; - } - isRelational() { - return this.dataModel != null && this.dataModel === 'relational'; - } - - setNamespaceType(dataModel: string) { - if (!dataModel) { - this.dataModel = null; - return; + setNamespaceType(dataModel: string) { + if (!dataModel) { + this.dataModel = null; + return; + } + this.dataModel = dataModel.toLowerCase(); } - this.dataModel = dataModel.toLowerCase(); - } } diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts index f8e76cd9..d486f662 100644 --- a/src/app/components/components.module.ts +++ b/src/app/components/components.module.ts @@ -4,60 +4,60 @@ import {GraphComponent} from './graph/graph.component'; import {NgChartsModule} from 'ng2-charts'; import { - BgColorDirective, - BreadcrumbComponent as BreadCrumb, - ButtonCloseDirective, - ButtonDirective, - ButtonGroupComponent, - CardBodyComponent, - CardComponent, - CardFooterComponent, - CardHeaderComponent, - ColComponent, - ColDirective, - ContainerComponent, - DropdownComponent, - DropdownDividerDirective, - DropdownItemDirective, - DropdownMenuDirective, - DropdownToggleDirective, - FormControlDirective, - FormDirective, - FormFeedbackComponent, - FormSelectDirective, - GutterDirective, - InputGroupComponent, - InputGroupTextDirective, - ListGroupDirective, - ListGroupItemDirective, - ModalBodyComponent, - ModalComponent, - ModalContentComponent, - ModalFooterComponent, - ModalHeaderComponent, - ModalTitleDirective, - NavComponent, - NavItemComponent, - NavLinkDirective, - PageItemDirective, - PageLinkDirective, - PaginationComponent, - ProgressBarComponent, - ProgressComponent, - RowComponent, - RowDirective, - SpinnerComponent, - TabContentComponent, - TabContentRefDirective, - TableDirective, - TabPaneComponent, - TextColorDirective, - ToastBodyComponent, - ToastCloseDirective, - ToastComponent, - ToasterComponent, - ToastHeaderComponent, - TooltipDirective + BgColorDirective, + BreadcrumbComponent as BreadCrumb, + ButtonCloseDirective, + ButtonDirective, + ButtonGroupComponent, + CardBodyComponent, + CardComponent, + CardFooterComponent, + CardHeaderComponent, + ColComponent, + ColDirective, + ContainerComponent, + DropdownComponent, + DropdownDividerDirective, + DropdownItemDirective, + DropdownMenuDirective, + DropdownToggleDirective, + FormControlDirective, + FormDirective, + FormFeedbackComponent, + FormSelectDirective, + GutterDirective, + InputGroupComponent, + InputGroupTextDirective, + ListGroupDirective, + ListGroupItemDirective, + ModalBodyComponent, + ModalComponent, + ModalContentComponent, + ModalFooterComponent, + ModalHeaderComponent, + ModalTitleDirective, + NavComponent, + NavItemComponent, + NavLinkDirective, + PageItemDirective, + PageLinkDirective, + PaginationComponent, + ProgressBarComponent, + ProgressComponent, + RowComponent, + RowDirective, + SpinnerComponent, + TabContentComponent, + TabContentRefDirective, + TableDirective, + TabPaneComponent, + TextColorDirective, + ToastBodyComponent, + ToastCloseDirective, + ToastComponent, + ToasterComponent, + ToastHeaderComponent, + TooltipDirective } from '@coreui/angular'; import {BreadcrumbComponent} from './breadcrumb/breadcrumb.component'; @@ -104,127 +104,127 @@ import {ViewComponent} from './data-view/view/view.component'; //import 'hammerjs'; @NgModule({ - imports: [ - //AppRoutingModule, - RouterModule, - CommonModule, - NgChartsModule, - TypeaheadModule.forRoot(), - TabsModule.forRoot(), - ToasterComponent, - ToastBodyComponent, - ToastHeaderComponent, - ProgressbarModule, - // forms - FormsModule, ReactiveFormsModule, - CollapseModule, - TooltipModule, - ProgressbarModule.forRoot(), - ExplainVisualizerModule, - ModalModule.forRoot(), - CarouselModule, - NgxJsonViewerModule, - DatesPipeModule, - ToastComponent, - BgColorDirective, - TreeModule, - RowComponent, - ColComponent, - InputGroupComponent, - FormControlDirective, - InputGroupTextDirective, - ProgressBarComponent, - ProgressComponent, - ToastCloseDirective, - BreadCrumb, - SpinnerComponent, - ButtonDirective, - TableDirective, - CardComponent, - CardHeaderComponent, - CardBodyComponent, - CardFooterComponent, - FormFeedbackComponent, - ListGroupDirective, - ListGroupItemDirective, - PaginationComponent, - PageItemDirective, - PageLinkDirective, - TextColorDirective, - ModalComponent, - ModalContentComponent, - ModalHeaderComponent, - ModalBodyComponent, - ButtonCloseDirective, - ButtonGroupComponent, - NavComponent, - NavItemComponent, - NavLinkDirective, - TabContentComponent, - TabPaneComponent, - TabContentRefDirective, - ModalFooterComponent, - GutterDirective, - ColDirective, - DropdownMenuDirective, - DropdownItemDirective, - DropdownDividerDirective, - DropdownToggleDirective, ModalTitleDirective, FormDirective, RowDirective, DropdownComponent, FormSelectDirective, TooltipDirective, ContainerComponent - ], - declarations: [ - BreadcrumbComponent, - DynamicFormsComponent, - GraphComponent, - LeftSidebarComponent, - RightSidebarComponent, - InformationManagerComponent, - RenderItemComponent, - InputComponent, - EditorComponent, - JsonEditorComponent, - DataViewComponent, - DataCardComponent, - DataTableComponent, - DataGraphComponent, - MediaComponent, - DeleteConfirmComponent, - ExpandableTextComponent, - JsonTextComponent, - JsonElemComponent, - LoadingScreenComponent, - DockereditComponent, - DockerhandshakeComponent, - DockernewComponent, - DockersettingsComponent, - ToastExposerComponent, - Toast, - ReloadButtonComponent, - ViewComponent - ], - exports: [ - BreadcrumbComponent, - DataViewComponent, - DataTableComponent, - DataCardComponent, - DynamicFormsComponent, - GraphComponent, - LeftSidebarComponent, - RightSidebarComponent, - ToastComponent, - InformationManagerComponent, - InputComponent, - JsonEditorComponent, - EditorComponent, - DeleteConfirmComponent, - LoadingScreenComponent, - DockereditComponent, - DockerhandshakeComponent, - DockernewComponent, - DockersettingsComponent, - ToastExposerComponent, - Toast, - ReloadButtonComponent - ] + imports: [ + //AppRoutingModule, + RouterModule, + CommonModule, + NgChartsModule, + TypeaheadModule.forRoot(), + TabsModule.forRoot(), + ToasterComponent, + ToastBodyComponent, + ToastHeaderComponent, + ProgressbarModule, + // forms + FormsModule, ReactiveFormsModule, + CollapseModule, + TooltipModule, + ProgressbarModule.forRoot(), + ExplainVisualizerModule, + ModalModule.forRoot(), + CarouselModule, + NgxJsonViewerModule, + DatesPipeModule, + ToastComponent, + BgColorDirective, + TreeModule, + RowComponent, + ColComponent, + InputGroupComponent, + FormControlDirective, + InputGroupTextDirective, + ProgressBarComponent, + ProgressComponent, + ToastCloseDirective, + BreadCrumb, + SpinnerComponent, + ButtonDirective, + TableDirective, + CardComponent, + CardHeaderComponent, + CardBodyComponent, + CardFooterComponent, + FormFeedbackComponent, + ListGroupDirective, + ListGroupItemDirective, + PaginationComponent, + PageItemDirective, + PageLinkDirective, + TextColorDirective, + ModalComponent, + ModalContentComponent, + ModalHeaderComponent, + ModalBodyComponent, + ButtonCloseDirective, + ButtonGroupComponent, + NavComponent, + NavItemComponent, + NavLinkDirective, + TabContentComponent, + TabPaneComponent, + TabContentRefDirective, + ModalFooterComponent, + GutterDirective, + ColDirective, + DropdownMenuDirective, + DropdownItemDirective, + DropdownDividerDirective, + DropdownToggleDirective, ModalTitleDirective, FormDirective, RowDirective, DropdownComponent, FormSelectDirective, TooltipDirective, ContainerComponent + ], + declarations: [ + BreadcrumbComponent, + DynamicFormsComponent, + GraphComponent, + LeftSidebarComponent, + RightSidebarComponent, + InformationManagerComponent, + RenderItemComponent, + InputComponent, + EditorComponent, + JsonEditorComponent, + DataViewComponent, + DataCardComponent, + DataTableComponent, + DataGraphComponent, + MediaComponent, + DeleteConfirmComponent, + ExpandableTextComponent, + JsonTextComponent, + JsonElemComponent, + LoadingScreenComponent, + DockereditComponent, + DockerhandshakeComponent, + DockernewComponent, + DockersettingsComponent, + ToastExposerComponent, + Toast, + ReloadButtonComponent, + ViewComponent + ], + exports: [ + BreadcrumbComponent, + DataViewComponent, + DataTableComponent, + DataCardComponent, + DynamicFormsComponent, + GraphComponent, + LeftSidebarComponent, + RightSidebarComponent, + ToastComponent, + InformationManagerComponent, + InputComponent, + JsonEditorComponent, + EditorComponent, + DeleteConfirmComponent, + LoadingScreenComponent, + DockereditComponent, + DockerhandshakeComponent, + DockernewComponent, + DockersettingsComponent, + ToastExposerComponent, + Toast, + ReloadButtonComponent + ] }) export class ComponentsModule { } diff --git a/src/app/components/data-view/data-card/data-card.component.ts b/src/app/components/data-view/data-card/data-card.component.ts index 984c3410..5022bb03 100644 --- a/src/app/components/data-view/data-card/data-card.component.ts +++ b/src/app/components/data-view/data-card/data-card.component.ts @@ -3,35 +3,35 @@ import {DataTemplateComponent} from '../data-template/data-template.component'; import {DataModel} from '../../../models/ui-request.model'; @Component({ - selector: 'app-data-card', - templateUrl: './data-card.component.html', - styleUrls: ['./data-card.component.scss'] + selector: 'app-data-card', + templateUrl: './data-card.component.html', + styleUrls: ['./data-card.component.scss'] }) export class DataCardComponent extends DataTemplateComponent implements OnInit { - constructor() { - super(); - } + constructor() { + super(); + } - showInsertCard = false; - jsonValid = false; + showInsertCard = false; + jsonValid = false; - protected readonly DataModel = DataModel; + protected readonly DataModel = DataModel; - ngOnInit(): void { - super.ngOnInit(); - if (this.entityConfig && this.entityConfig().create) { - this.buildInsertObject(); + ngOnInit(): void { + super.ngOnInit(); + if (this.entityConfig && this.entityConfig().create) { + this.buildInsertObject(); + } + this.setPagination(); } - this.setPagination(); - } - setJsonValid($event: any) { - this.jsonValid = $event; - } + setJsonValid($event: any) { + this.jsonValid = $event; + } - showInsert() { - this.editing = null; - this.showInsertCard = true; - } + showInsert() { + this.editing = null; + this.showInsertCard = true; + } } 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 29e68549..5b8be7cd 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 @@ -6,700 +6,700 @@ import {DataTemplateComponent} from '../data-template/data-template.component'; const d3 = await import('d3'); @Component({ - selector: 'app-data-graph', - templateUrl: './data-graph.component.html', - styleUrls: ['./data-graph.component.scss'] + selector: 'app-data-graph', + templateUrl: './data-graph.component.html', + styleUrls: ['./data-graph.component.scss'] }) export class DataGraphComponent extends DataTemplateComponent { - constructor() { - super(); - this.initWebsocket(); - - effect(() => { - const result = this.$result(); - if (!result) { - return; - } - - this.graphLoading = true; - d3.select('.svg-responsive').remove(); - this.getGraph(result); - }); - } - - private hidden: string[]; - private update: () => void; - private graph: Graph; - public isLimited: boolean; - - - showInsertCard = false; - jsonValid = false; - public graphLoading = false; - private initialIds: Set; - showProperties = false; - detail: Detail; - private height: number; - private width: number; - private zoom: any; - private subElement: any; - private labels: string[]; - private ratio: number; - private color: any; - private isPath: boolean; - private initialEdgeIds: string[]; - private afterInit = false; - - 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']; - - if (source === target) { - return true; + constructor() { + super(); + this.initWebsocket(); + + effect(() => { + const result = this.$result(); + if (!result) { + return; + } + + this.graphLoading = true; + d3.select('.svg-responsive').remove(); + this.getGraph(result); + }); } - const connectionsIncluded = !hidden.includes(source) && !hidden.includes(target); - if (connectionsIncluded) { - if (p.isPath) { - if (p.initialEdgeIds.includes(d.id)) { - return true; + private hidden: string[]; + private update: () => void; + private graph: Graph; + public isLimited: boolean; + + + showInsertCard = false; + jsonValid = false; + public graphLoading = false; + private initialIds: Set; + showProperties = false; + detail: Detail; + private height: number; + private width: number; + private zoom: any; + private subElement: any; + private labels: string[]; + private ratio: number; + private color: any; + private isPath: boolean; + private initialEdgeIds: string[]; + private afterInit = false; + + 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']; + + if (source === target) { + return true; } - } else { - return true; - } + + const connectionsIncluded = !hidden.includes(source) && !hidden.includes(target); + if (connectionsIncluded) { + if (p.isPath) { + if (p.initialEdgeIds.includes(d.id)) { + return true; + } + } else { + return true; + } + } + return false; } - return false; - } - private renderGraph(graph: Graph) { - if (!this.initialIds) { - this.initialIds = new Set(graph.nodes.map(n => n.id)); - } + private renderGraph(graph: Graph) { + if (!this.initialIds) { + this.initialIds = new Set(graph.nodes.map(n => n.id)); + } - if (!this.initialEdgeIds) { - this.initialEdgeIds = graph.edges.map(e => e.id); - } + if (!this.initialEdgeIds) { + this.initialEdgeIds = graph.edges.map(e => e.id); + } - const size = 20; - const overlaySize = 30; - const overlayStroke = 3; - const textSize = 13; - const linkSize = 9; + const size = 20; + const overlaySize = 30; + const overlayStroke = 3; + const textSize = 13; + const linkSize = 9; - this.graph = graph; + this.graph = graph; - this.hidden = []; + this.hidden = []; - for (const n of graph.nodes) { - if (!this.initialIds.has(n.id)) { - this.hidden.push(n.id); - } - } + for (const n of graph.nodes) { + if (!this.initialIds.has(n.id)) { + this.hidden.push(n.id); + } + } - const width = 600; - this.width = width; - const height = 325; - this.height = height; - - d3 - .select('#chart-area > *').remove(); - - const svg = d3 - .select('#chart-area') - .append('div') - .attr('class', 'svg-responsive') - .append('svg') - .attr('preserveAspectRatio', 'xMinYMin meet') - .attr('viewBox', `0 0 ${width} ${height}`) - .attr('class', 'svg-content-responsive'); + const width = 600; + this.width = width; + const height = 325; + this.height = height; - svg.exit().remove(); - - const g = svg.append('g'); + d3 + .select('#chart-area > *').remove(); - const zoom_actions = () => { - g.attr('transform', d3.event.transform); - }; - - this.zoom = d3.zoom() - .on('zoom', zoom_actions) - .filter(() => !d3.event.button); // fix for windows trackpad zooming - - this.zoom(svg); - - - // Add "forces" to the simulation here - const simulation = d3.forceSimulation() - .force('center', d3.forceCenter(width / 2, height / 2)) - .force('charge', d3.forceManyBody().strength(-this.initialIds.size)) - .force('collide', d3.forceCollide(100).strength(0.9).radius(40)) - .force('link', d3.forceLink().id(d => d.id).distance(160)); - - - // disable charge after initial setup - setInterval(() => simulation.force('charge', null), 1500); - - const action = (d) => { - this.detail = new Detail(d); - this.showProperties = true; - }; - - // Change the value of alpha, so things move around when we drag a node - const onDragStart = d => { - action(d); - if (!d3.event.active) { - simulation.alphaTarget(0.8).restart(); - } - d.fx = d.x; - d.fy = d.y; - }; - - // Fix the position of the node that we are looking at - const onDrag = d => { - d.fx = d3.event.x; - d.fy = d3.event.y; - }; - - // Let the node do what it wants again once we've looked at it - const onDragEnd = d => { - if (!d3.event.active) { - simulation.alphaTarget(0); - } - d.fx = null; - d.fy = null; - }; - - - let left, t, right, link, links, newLinks, newNode, node, overlay, newOverlay, newSelectionHelp, selectionHelp, - newText, text, els, linktext, newLinktext, preNode; - - - const restart = (p: any) => { - const hidden = p.hidden; - - g.exit().remove(); - - // build the arrow. - g - .append('svg:defs') - .selectAll('marker') - .data(['end']) // Different link/path types can be defined here - .enter() - .append('svg:marker') // This section adds in the arrows - .attr('id', String) - .attr('viewBox', '0 -5 10 10') - .attr('refX', 30) - .attr('refY', 0) - .attr('markerWidth', 10) - .attr('markerHeight', 10) - .attr('orient', 'auto') - .attr('fill', '#999') - .append('svg:path') - .attr('d', 'M0,-5L10,0L0,5'); - - - // Add lines for every link in the dataset - newLinks = g - //.style('border', '1px solid black') - .append('g') - .attr('class', 'links') - .selectAll('path') - // add edges to hidden nodes - .data(graph.edges.filter((d) => { - return DataGraphComponent.filterEdges(hidden, d, p); - })); - - - newLinks.remove().exit(); - - - newLinktext = g - .selectAll('g.linklabelholder') - .data(graph.edges.filter((d) => DataGraphComponent.filterEdges(hidden, d, p))); - - newLinktext - .enter() - .append('svg:g') - .attr('class', 'linklabelholder') - .append('text') - .attr('class', 'linklabel') - .style('font-size', linkSize + 'px') - .attr('x', '50') - .attr('y', '0') - .attr('dy', '-5') - .attr('text-anchor', 'start') - .style('fill', '#000') - .append('textPath') - .on('click', action) - .attr('xlink:href', function (d, i) { - return '#linkId_' + i; - }) - .attr('cursor', 'pointer') - .on('mouseover', function (d) { - d3.select(this).attr('fill', 'grey'); - }) - .on('mouseout', function (d) { - d3.select(this).attr('fill', '#000'); - }) - .text(function (d) { - if (d.labels.length === 0) { - return ''; - } else { - return d.labels[0].toUpperCase(); - } + const svg = d3 + .select('#chart-area') + .append('div') + .attr('class', 'svg-responsive') + .append('svg') + .attr('preserveAspectRatio', 'xMinYMin meet') + .attr('viewBox', `0 0 ${width} ${height}`) + .attr('class', 'svg-content-responsive'); - }); - - - link = newLinks - .enter() - .filter(d => { - return DataGraphComponent.filterEdges(hidden, d, p); - }) - .append('g') - .attr('class', 'link') - .append('path') - .style('stroke', 'grey') - //.attr("stroke-width", d => Math.sqrt(d.value)) - .attr('class', function (d) { - return 'link ' + d.type; - }) - .attr('id', function (d, i) { - return 'linkId_' + i; - }) - .attr('marker-end', 'url(#end)'); - - link.exit().remove(); - newLinktext.exit().remove(); - - - preNode = g - .append('g') - .attr('class', 'nodes') - .selectAll('circle') - .data(graph.nodes.filter((d) => !hidden.includes(d.id))); - - preNode.exit().remove(); - - els = preNode - .enter() - .append('g') - .attr('class', 'node'); - - els.exit().remove(); - - const arc = d3.arc() - .innerRadius(size + overlayStroke) - .outerRadius(size + overlayStroke + overlaySize); - - newText = g.selectAll('.name') - .append('g') - .attr('class', 'node-label') - .data(graph.nodes.filter(d => !hidden.includes(d.id))) - .enter() - .append('text') - .attr('pointer-events', 'none'); - - newText.exit().remove(); - - newText.style('fill', 'black') - .attr('width', '10') - .attr('height', '10') - .attr('dy', 5) - .attr('text-anchor', 'middle') - .text(d => { - for (const key of Object.keys(d.properties)) { - if (key !== '_id') { - const prop = d.properties[key]; - return prop.toString().substring(0, 6); - } - } - return ''; - }); - - newText.exit().remove(); - - if (newOverlay !== undefined) { - newOverlay.exit().remove(); - } - - - newSelectionHelp = els.append('g').attr('class', 'aid').append('circle').attr('r', size + overlayStroke + overlaySize).attr('fill', 'transparent').attr('display', 'none'); - - newOverlay = els.append('g').attr('class', 'overlay').attr('display', 'none'); - - t = newOverlay.append('path').attr('fill', 'transparent') - .attr('stroke-width', overlayStroke) - .attr('stroke', 'transparent') - .attr('d', arc({startAngle: -(Math.PI / 3), endAngle: (Math.PI / 3)})); - - right = newOverlay.append('path').attr('fill', 'grey') - .attr('stroke-width', overlayStroke) - .attr('stroke', 'white') - .style('cursor', 'pointer') - .attr('d', arc({startAngle: 0, endAngle: Math.PI})) - .on('mouseover', function (d) { - d3.select(this).attr('fill', 'darkgray'); - }) - .on('mouseout', function (d) { - d3.select(this).attr('fill', 'grey'); - }) - .on('click', (d) => { - graph.edges.filter(function (e) { - return d.id === e.source['id'] || d.id === e.target['id']; //connected nodes - }).forEach((e) => { - if (this.hidden.indexOf(e.id) !== -1) { - this.hidden = this.hidden.filter((i) => i !== e.id); - } - const id = e.source['id'] !== d.id ? e.source['id'] : e.target['id']; - if (this.hidden.indexOf(id) !== -1) { - this.hidden = this.hidden.filter((i) => i !== id); - } - }); + svg.exit().remove(); - this.update(); - }); - - left = newOverlay.append('path').attr('fill', 'grey') - .attr('stroke-width', overlayStroke) - .attr('stroke', 'white') - .style('cursor', 'pointer') - .attr('d', arc({startAngle: -Math.PI, endAngle: 0})) - .on('mouseover', function (d) { - d3.select(this).attr('fill', 'darkgray'); - }) - .on('mouseout', function (d) { - d3.select(this).attr('fill', 'grey'); - }) - .on('click', (d) => { - hidden.push(d.id); - this.update(); - }); - - newOverlay.append('text').attr('text-anchor', 'middle') - .attr('dominant-baseline', 'central') - .attr('font-family', 'FontAwesome') - .attr('font-size', textSize + 'px') - .attr('fill', 'white') - .attr('class', 'el-select el-back fa') - .style('transform', 'translateX(-35px)') - .style('pointer-events', 'none') - .text(function (d) { - return '\uf070'; - }); - - newOverlay.append('text').attr('text-anchor', 'middle') - .attr('dominant-baseline', 'central') - .attr('font-family', 'CoreUI-Icons-Free') - .attr('font-size', textSize + 'px') - .attr('fill', 'white') - .attr('class', 'el-select el-cross') - .style('transform', 'translateX(35px)') - .style('pointer-events', 'none') - .text(function (d) { - return '\uebd8'; - }); - - newOverlay.selectAll(); - - p.labels = new Set(); - - for (const e of graph.edges) { - e.labels.forEach(l => p.labels.add(l)); - } - - for (const n of graph.nodes) { - n.labels.forEach(l => p.labels.add(l)); - } - - p.labels = Array.from(p.labels); - p.color = d3.interpolateSinebow; - p.ratio = 1 / p.labels.length; - - - // Add circles for every node in the dataset - newNode = els - .append('circle') - .attr('r', size) - .attr('fill', d => { - const i = p.labels.indexOf(d.labels[0]); - return p.color(p.ratio * i); - }) - .on('click', action) - .attr('cursor', 'pointer') - .call( - d3 - .drag() - .on('start', onDragStart) - .on('drag', onDrag) - .on('end', onDragEnd) - ); - - newNode.exit().remove(); - - els.on('mouseover', function (d) { - d3.select(this).select('.overlay').transition().attr('display', 'inherit'); - d3.select(this).select('.aid').attr('display', 'inherit').select('circle').attr('display', 'inherit'); - }) - .on('mouseout', function (d) { - d3.select(this).select('.overlay').transition().duration(200).attr('display', 'none'); - d3.select(this).select('.aid').attr('display', 'none'); - }); - - - g.exit().remove(); - - }; - - // Dynamically update the position of the nodes/links as time passes - const onTick = () => { - - link.attr('d', function (d) { - const dx = d.target.x - d.source.x, - dy = d.target.y - d.source.y, - dr = 0; //linknum is defined above - return 'M' + d.source.x + ',' + d.source.y + 'A' + dr + ',' + dr + ' 0 0,1 ' + d.target.x + ',' + d.target.y; - }); - els - .attr('cx', d => d.x) - .attr('cy', d => d.y); - node - .attr('cx', d => d.x) - .attr('cy', d => d.y); - overlay.attr('transform', function (d) { - return 'translate(' + d.x + ',' + d.y + ')'; - }); - - selectionHelp - .attr('cx', d => d.x) - .attr('cy', d => d.y); - - text - .attr('x', function (d) { - return d.x; - }) - .attr('y', function (d) { - return d.y; - }); - }; - - restart(this); - - - node = newNode; - text = newText; - links = newLinks; - overlay = newOverlay; - selectionHelp = newSelectionHelp; - activate(); + const g = svg.append('g'); + + const zoom_actions = () => { + g.attr('transform', d3.event.transform); + }; + + this.zoom = d3.zoom() + .on('zoom', zoom_actions) + .filter(() => !d3.event.button); // fix for windows trackpad zooming + + this.zoom(svg); + + + // Add "forces" to the simulation here + const simulation = d3.forceSimulation() + .force('center', d3.forceCenter(width / 2, height / 2)) + .force('charge', d3.forceManyBody().strength(-this.initialIds.size)) + .force('collide', d3.forceCollide(100).strength(0.9).radius(40)) + .force('link', d3.forceLink().id(d => d.id).distance(160)); + + + // disable charge after initial setup + setInterval(() => simulation.force('charge', null), 1500); + + const action = (d) => { + this.detail = new Detail(d); + this.showProperties = true; + }; + + // Change the value of alpha, so things move around when we drag a node + const onDragStart = d => { + action(d); + if (!d3.event.active) { + simulation.alphaTarget(0.8).restart(); + } + d.fx = d.x; + d.fy = d.y; + }; + + // Fix the position of the node that we are looking at + const onDrag = d => { + d.fx = d3.event.x; + d.fy = d3.event.y; + }; + + // Let the node do what it wants again once we've looked at it + const onDragEnd = d => { + if (!d3.event.active) { + simulation.alphaTarget(0); + } + d.fx = null; + d.fy = null; + }; + + + let left, t, right, link, links, newLinks, newNode, node, overlay, newOverlay, newSelectionHelp, selectionHelp, + newText, text, els, linktext, newLinktext, preNode; + + + const restart = (p: any) => { + const hidden = p.hidden; + + g.exit().remove(); + + // build the arrow. + g + .append('svg:defs') + .selectAll('marker') + .data(['end']) // Different link/path types can be defined here + .enter() + .append('svg:marker') // This section adds in the arrows + .attr('id', String) + .attr('viewBox', '0 -5 10 10') + .attr('refX', 30) + .attr('refY', 0) + .attr('markerWidth', 10) + .attr('markerHeight', 10) + .attr('orient', 'auto') + .attr('fill', '#999') + .append('svg:path') + .attr('d', 'M0,-5L10,0L0,5'); + + + // Add lines for every link in the dataset + newLinks = g + //.style('border', '1px solid black') + .append('g') + .attr('class', 'links') + .selectAll('path') + // add edges to hidden nodes + .data(graph.edges.filter((d) => { + return DataGraphComponent.filterEdges(hidden, d, p); + })); + + + newLinks.remove().exit(); + + + newLinktext = g + .selectAll('g.linklabelholder') + .data(graph.edges.filter((d) => DataGraphComponent.filterEdges(hidden, d, p))); + + newLinktext + .enter() + .append('svg:g') + .attr('class', 'linklabelholder') + .append('text') + .attr('class', 'linklabel') + .style('font-size', linkSize + 'px') + .attr('x', '50') + .attr('y', '0') + .attr('dy', '-5') + .attr('text-anchor', 'start') + .style('fill', '#000') + .append('textPath') + .on('click', action) + .attr('xlink:href', function (d, i) { + return '#linkId_' + i; + }) + .attr('cursor', 'pointer') + .on('mouseover', function (d) { + d3.select(this).attr('fill', 'grey'); + }) + .on('mouseout', function (d) { + d3.select(this).attr('fill', '#000'); + }) + .text(function (d) { + if (d.labels.length === 0) { + return ''; + } else { + return d.labels[0].toUpperCase(); + } + + }); + + + link = newLinks + .enter() + .filter(d => { + return DataGraphComponent.filterEdges(hidden, d, p); + }) + .append('g') + .attr('class', 'link') + .append('path') + .style('stroke', 'grey') + //.attr("stroke-width", d => Math.sqrt(d.value)) + .attr('class', function (d) { + return 'link ' + d.type; + }) + .attr('id', function (d, i) { + return 'linkId_' + i; + }) + .attr('marker-end', 'url(#end)'); + + link.exit().remove(); + newLinktext.exit().remove(); + + + preNode = g + .append('g') + .attr('class', 'nodes') + .selectAll('circle') + .data(graph.nodes.filter((d) => !hidden.includes(d.id))); + + preNode.exit().remove(); + + els = preNode + .enter() + .append('g') + .attr('class', 'node'); + + els.exit().remove(); + + const arc = d3.arc() + .innerRadius(size + overlayStroke) + .outerRadius(size + overlayStroke + overlaySize); + + newText = g.selectAll('.name') + .append('g') + .attr('class', 'node-label') + .data(graph.nodes.filter(d => !hidden.includes(d.id))) + .enter() + .append('text') + .attr('pointer-events', 'none'); + + newText.exit().remove(); + + newText.style('fill', 'black') + .attr('width', '10') + .attr('height', '10') + .attr('dy', 5) + .attr('text-anchor', 'middle') + .text(d => { + for (const key of Object.keys(d.properties)) { + if (key !== '_id') { + const prop = d.properties[key]; + return prop.toString().substring(0, 6); + } + } + return ''; + }); + + newText.exit().remove(); + + if (newOverlay !== undefined) { + newOverlay.exit().remove(); + } + + + newSelectionHelp = els.append('g').attr('class', 'aid').append('circle').attr('r', size + overlayStroke + overlaySize).attr('fill', 'transparent').attr('display', 'none'); + + newOverlay = els.append('g').attr('class', 'overlay').attr('display', 'none'); + + t = newOverlay.append('path').attr('fill', 'transparent') + .attr('stroke-width', overlayStroke) + .attr('stroke', 'transparent') + .attr('d', arc({startAngle: -(Math.PI / 3), endAngle: (Math.PI / 3)})); + + right = newOverlay.append('path').attr('fill', 'grey') + .attr('stroke-width', overlayStroke) + .attr('stroke', 'white') + .style('cursor', 'pointer') + .attr('d', arc({startAngle: 0, endAngle: Math.PI})) + .on('mouseover', function (d) { + d3.select(this).attr('fill', 'darkgray'); + }) + .on('mouseout', function (d) { + d3.select(this).attr('fill', 'grey'); + }) + .on('click', (d) => { + graph.edges.filter(function (e) { + return d.id === e.source['id'] || d.id === e.target['id']; //connected nodes + }).forEach((e) => { + if (this.hidden.indexOf(e.id) !== -1) { + this.hidden = this.hidden.filter((i) => i !== e.id); + } + const id = e.source['id'] !== d.id ? e.source['id'] : e.target['id']; + if (this.hidden.indexOf(id) !== -1) { + this.hidden = this.hidden.filter((i) => i !== id); + } + }); + + this.update(); + }); + + left = newOverlay.append('path').attr('fill', 'grey') + .attr('stroke-width', overlayStroke) + .attr('stroke', 'white') + .style('cursor', 'pointer') + .attr('d', arc({startAngle: -Math.PI, endAngle: 0})) + .on('mouseover', function (d) { + d3.select(this).attr('fill', 'darkgray'); + }) + .on('mouseout', function (d) { + d3.select(this).attr('fill', 'grey'); + }) + .on('click', (d) => { + hidden.push(d.id); + this.update(); + }); + + newOverlay.append('text').attr('text-anchor', 'middle') + .attr('dominant-baseline', 'central') + .attr('font-family', 'FontAwesome') + .attr('font-size', textSize + 'px') + .attr('fill', 'white') + .attr('class', 'el-select el-back fa') + .style('transform', 'translateX(-35px)') + .style('pointer-events', 'none') + .text(function (d) { + return '\uf070'; + }); + + newOverlay.append('text').attr('text-anchor', 'middle') + .attr('dominant-baseline', 'central') + .attr('font-family', 'CoreUI-Icons-Free') + .attr('font-size', textSize + 'px') + .attr('fill', 'white') + .attr('class', 'el-select el-cross') + .style('transform', 'translateX(35px)') + .style('pointer-events', 'none') + .text(function (d) { + return '\uebd8'; + }); + + newOverlay.selectAll(); + + p.labels = new Set(); + + for (const e of graph.edges) { + e.labels.forEach(l => p.labels.add(l)); + } + + for (const n of graph.nodes) { + n.labels.forEach(l => p.labels.add(l)); + } + + p.labels = Array.from(p.labels); + p.color = d3.interpolateSinebow; + p.ratio = 1 / p.labels.length; + + + // Add circles for every node in the dataset + newNode = els + .append('circle') + .attr('r', size) + .attr('fill', d => { + const i = p.labels.indexOf(d.labels[0]); + return p.color(p.ratio * i); + }) + .on('click', action) + .attr('cursor', 'pointer') + .call( + d3 + .drag() + .on('start', onDragStart) + .on('drag', onDrag) + .on('end', onDragEnd) + ); + + newNode.exit().remove(); + + els.on('mouseover', function (d) { + d3.select(this).select('.overlay').transition().attr('display', 'inherit'); + d3.select(this).select('.aid').attr('display', 'inherit').select('circle').attr('display', 'inherit'); + }) + .on('mouseout', function (d) { + d3.select(this).select('.overlay').transition().duration(200).attr('display', 'none'); + d3.select(this).select('.aid').attr('display', 'none'); + }); + + + g.exit().remove(); + + }; + + // Dynamically update the position of the nodes/links as time passes + const onTick = () => { + + link.attr('d', function (d) { + const dx = d.target.x - d.source.x, + dy = d.target.y - d.source.y, + dr = 0; //linknum is defined above + return 'M' + d.source.x + ',' + d.source.y + 'A' + dr + ',' + dr + ' 0 0,1 ' + d.target.x + ',' + d.target.y; + }); + els + .attr('cx', d => d.x) + .attr('cy', d => d.y); + node + .attr('cx', d => d.x) + .attr('cy', d => d.y); + overlay.attr('transform', function (d) { + return 'translate(' + d.x + ',' + d.y + ')'; + }); + + selectionHelp + .attr('cx', d => d.x) + .attr('cy', d => d.y); + + text + .attr('x', function (d) { + return d.x; + }) + .attr('y', function (d) { + return d.y; + }); + }; + + restart(this); + + + node = newNode; + text = newText; + links = newLinks; + overlay = newOverlay; + selectionHelp = newSelectionHelp; + activate(); // general update pattern for updating the graph - this.update = () => { - g.selectAll('*').remove(); - restart(this); - node = newNode; - links = newLinks; - overlay = newOverlay; - text = newText; - linktext = newLinktext; - selectionHelp = newSelectionHelp; - activate(); - - }; - - - function activate() { - // Attach nodes to the simulation, add listener on the "tick" event - simulation - .nodes(graph.nodes) - .on('tick', onTick); - - // Associate the lines with the "link" force - simulation - .force('link') - .links(graph.edges); - - simulation.alpha(1).alphaTarget(0).restart(); + this.update = () => { + g.selectAll('*').remove(); + restart(this); + node = newNode; + links = newLinks; + overlay = newOverlay; + text = newText; + linktext = newLinktext; + selectionHelp = newSelectionHelp; + activate(); + + }; + + + function activate() { + // Attach nodes to the simulation, add listener on the "tick" event + simulation + .nodes(graph.nodes) + .on('tick', onTick); + + // Associate the lines with the "link" force + simulation + .force('link') + .links(graph.edges); + + simulation.alpha(1).alphaTarget(0).restart(); + } + + const reset = () => { + this.hidden = []; + }; + this.afterInit = true; } - const reset = () => { - this.hidden = []; - }; - this.afterInit = true; - } - - center() { - d3.select('svg') - .transition().duration(500) - .call(this.zoom.transform, d3.zoomIdentity); - } - - initWebsocket() { - const sub = this.webSocket.onMessage().subscribe({ - next: res => { - const unparsedGraph: string = res; - this.graphLoading = false; - this.renderGraph(Graph.from(unparsedGraph['nodes'], unparsedGraph['edges'])); - - }, - error: err => { - this._toast.error('Could not load the data.'); - console.log(err); - } - }); - this.subscriptions.add(sub); - } - - getGraph(graphResult: GraphResult) { - const nodeIds: Set = new Set(); - const edgeIds: Set = new Set(); - let i = -1; - - for (const dbColumn of graphResult.header) { - console.log(dbColumn); - i++; - if (!dbColumn.dataType.toLowerCase().includes('node') && !dbColumn.dataType.toLowerCase().includes('edge')) { - continue; - } - - if (dbColumn.dataType.toLowerCase().includes('node')) { - graphResult.data.forEach(d => { - console.log(d); - nodeIds.add(JSON.parse(d[i])['id']); - }); - } + center() { + d3.select('svg') + .transition().duration(500) + .call(this.zoom.transform, d3.zoomIdentity); + } - if (dbColumn.dataType.toLowerCase().includes('edge')) { - graphResult.data.forEach(d => { - edgeIds.add(JSON.parse(d[i])['id']); + initWebsocket() { + const sub = this.webSocket.onMessage().subscribe({ + next: res => { + const unparsedGraph: string = res; + this.graphLoading = false; + this.renderGraph(Graph.from(unparsedGraph['nodes'], unparsedGraph['edges'])); + + }, + error: err => { + this._toast.error('Could not load the data.'); + console.log(err); + } }); - } - - if (dbColumn.dataType.toLowerCase().includes('path')) { - this.isPath = true; - graphResult.data.forEach(d => { - for (const el of JSON.parse(d[i]).path) { - if (el.type.includes('NODE')) { - nodeIds.add(el.id); + this.subscriptions.add(sub); + } + + getGraph(graphResult: GraphResult) { + const nodeIds: Set = new Set(); + const edgeIds: Set = new Set(); + let i = -1; + + for (const dbColumn of graphResult.header) { + console.log(dbColumn); + i++; + if (!dbColumn.dataType.toLowerCase().includes('node') && !dbColumn.dataType.toLowerCase().includes('edge')) { + continue; } - if (el.type.includes('EDGE')) { - edgeIds.add(el.id); + + if (dbColumn.dataType.toLowerCase().includes('node')) { + graphResult.data.forEach(d => { + console.log(d); + nodeIds.add(JSON.parse(d[i])['id']); + }); } - } - }); - } - this.initialIds = nodeIds; - this.initialEdgeIds = Array.from(edgeIds); - } + if (dbColumn.dataType.toLowerCase().includes('edge')) { + graphResult.data.forEach(d => { + edgeIds.add(JSON.parse(d[i])['id']); + }); + } + + if (dbColumn.dataType.toLowerCase().includes('path')) { + this.isPath = true; + graphResult.data.forEach(d => { + for (const el of JSON.parse(d[i]).path) { + if (el.type.includes('NODE')) { + nodeIds.add(el.id); + } + if (el.type.includes('EDGE')) { + edgeIds.add(el.id); + } + } + }); + } + this.initialIds = nodeIds; + this.initialEdgeIds = Array.from(edgeIds); + } - console.log(this.initialIds); - if (!graphResult.header.map(h => h.dataType.toLowerCase()).includes('graph')) { - // is native - if (!this._crud.getGraph(this.webSocket, new GraphRequest(graphResult.namespace, nodeIds, edgeIds))) { - // is printed every time console.log('Could not retrieve the graphical representation of the graph.'); - } else { - this.graphLoading = true; - } - } else { + console.log(this.initialIds); - this.graphLoading = false; - const graph = Graph.from(graphResult.data.map(r => r.map(n => JSON.parse(n)).reduce((a, v) => ({ - ...a['id'], - [v]: v - }))), []); + if (!graphResult.header.map(h => h.dataType.toLowerCase()).includes('graph')) { + // is native + if (!this._crud.getGraph(this.webSocket, new GraphRequest(graphResult.namespace, nodeIds, edgeIds))) { + // is printed every time console.log('Could not retrieve the graphical representation of the graph.'); + } else { + this.graphLoading = true; + } + } else { - this.renderGraph(graph); - } + this.graphLoading = false; + const graph = Graph.from(graphResult.data.map(r => r.map(n => JSON.parse(n)).reduce((a, v) => ({ + ...a['id'], + [v]: v + }))), []); + this.renderGraph(graph); + } - } - setJsonValid($event: any) { - this.jsonValid = $event; - } + } - showInsert() { - this.editing = null; - this.showInsertCard = true; - } + setJsonValid($event: any) { + this.jsonValid = $event; + } - getLabelColor(label: string): string { - const i = this.labels.indexOf(label); - return this.color(this.ratio * i); - } + showInsert() { + this.editing = null; + this.showInsertCard = true; + } - reset() { - this.hidden = []; + getLabelColor(label: string): string { + const i = this.labels.indexOf(label); + return this.color(this.ratio * i); + } - for (const n of this.graph.nodes) { - if (!this.initialIds.has(n.id)) { - this.hidden.push(n.id); - } + reset() { + this.hidden = []; + + for (const n of this.graph.nodes) { + if (!this.initialIds.has(n.id)) { + this.hidden.push(n.id); + } + } + this.update(); } - this.update(); - } } class Edge { - id: string; - labels: any[]; - properties: any[]; - direction: string; - source: string; - target: string; + id: string; + labels: any[]; + properties: any[]; + direction: string; + source: string; + target: string; } class Node { - id: string; - labels: any[]; - properties: Map; + id: string; + labels: any[]; + properties: Map; } class PolyList { - size: number; - instance: any[]; + size: number; + instance: any[]; } class Graph { - nodes: Node[]; - edges: Edge[]; - selfEdges: Edge[]; + nodes: Node[]; + edges: Edge[]; + selfEdges: Edge[]; - public static from(n: any, e: any): Graph { - const nodes = Object.values(n); - const edges = Object.values(e).filter(d => d['source'] !== d['target']); + public static from(n: any, e: any): Graph { + const nodes = Object.values(n); + const edges = Object.values(e).filter(d => d['source'] !== d['target']); - return new Graph(nodes, edges); - } + return new Graph(nodes, edges); + } - constructor(nodes: Node[], edges: Edge[]) { + constructor(nodes: Node[], edges: Edge[]) { - this.nodes = nodes; - this.edges = edges; - this.selfEdges = edges.filter(d => d['source'] === d['target']); - } + this.nodes = nodes; + this.edges = edges; + this.selfEdges = edges.filter(d => d['source'] === d['target']); + } } class Detail { - constructor(d) { - this.id = d.id; - this.properties = d.properties; - this.labels = d.labels; - } - - id: string; - properties: {}; - labels: string[]; + constructor(d) { + this.id = d.id; + this.properties = d.properties; + this.labels = d.labels; + } + + id: string; + properties: {}; + labels: string[]; } diff --git a/src/app/components/data-view/data-table/data-table.component.ts b/src/app/components/data-view/data-table/data-table.component.ts index a66b95ac..68bea619 100644 --- a/src/app/components/data-view/data-table/data-table.component.ts +++ b/src/app/components/data-view/data-table/data-table.component.ts @@ -13,152 +13,152 @@ import {WebuiSettingsService} from '../../../services/webui-settings.service'; @Component({ - selector: 'app-data-table', - templateUrl: './data-table.component.html', - styleUrls: ['./data-table.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'app-data-table', + templateUrl: './data-table.component.html', + styleUrls: ['./data-table.component.scss'], + encapsulation: ViewEncapsulation.None }) export class DataTableComponent extends DataTemplateComponent implements OnInit { - public readonly _crud = inject(CrudService); - public readonly _types = inject(DbmsTypesService); - public readonly _settings = inject(WebuiSettingsService); - public readonly _sidebar = inject(LeftSidebarService); - public readonly _catalog = inject(CatalogService); + public readonly _crud = inject(CrudService); + public readonly _types = inject(DbmsTypesService); + public readonly _settings = inject(WebuiSettingsService); + public readonly _sidebar = inject(LeftSidebarService); + public readonly _catalog = inject(CatalogService); - constructor() { - super(); + constructor() { + super(); - } - - @ViewChild('decisionTree', {static: false}) public decisionTree: TemplateRef; - @ViewChild('sql', {static: false}) public sql: TemplateRef; - @ViewChild('editorGenerated', {static: false}) editorGenerated; - @ViewChild('tutorial', {static: false}) public tutorial: TemplateRef; - - - @Input() tutorialMode: boolean; + } - columns = []; - userInput = {}; + @ViewChild('decisionTree', {static: false}) public decisionTree: TemplateRef; + @ViewChild('sql', {static: false}) public sql: TemplateRef; + @ViewChild('editorGenerated', {static: false}) editorGenerated; + @ViewChild('tutorial', {static: false}) public tutorial: TemplateRef; - protected readonly NamespaceType = DataModel; - trackByFn(index: any, item: any) { - return index; - } + @Input() tutorialMode: boolean; - ngOnInit() { - super.ngOnInit(); - } + columns = []; + userInput = {}; + protected readonly NamespaceType = DataModel; - filterTable(e, filterVal, col: UiColumnDefinition) { - this.$result().currentPage = 1; - if (e.keyCode === 27) { //esc - $('.table-filter').val(''); - this.filter.clear(); - this.getEntityData(); - return; - } - if (col.collectionsType || col.dataType.includes('ARRAY')) { - if (this.isValidArray(filterVal) || !filterVal) { - this.filter.set(col.name, filterVal); - } - } else { - this.filter.set(col.name, filterVal); + trackByFn(index: any, item: any) { + return index; } - this.focusId = 'search-' + col.name; - this.getEntityData(); - } - - paginate(p: PaginationElement) { - this.$result().currentPage = p.page; - this.currentPage.set(p.page); - this.getEntityData(); - } - - sortTable(s: SortState) { - //todo primary ordering, secondary ordering - if (s.sorting === false) { - s.sorting = true; - s.direction = SortDirection.ASC; - } else { - if (s.direction === SortDirection.ASC) { - s.direction = SortDirection.DESC; - } else { - s.direction = SortDirection.ASC; - s.sorting = false; - } - } - this.getEntityData(); - } - - isValidArray(val: string): boolean { - if (val.startsWith('[') && val.endsWith(']')) { - try { - JSON.parse(val); - return true; - } catch (e) { - return false; - } - } - return false; - } - isValidFilter(val, col: UiColumnDefinition) { - if (!val) { - return; - } - if (col.collectionsType || col.dataType.includes('ARRAY')) { - if (!this.isValidArray(val)) { - return 'is-invalid'; - } + ngOnInit() { + super.ngOnInit(); } - } - getTooltip(col: UiColumnDefinition): string { - if (!col) { - return ''; - } - let out = 'name: ' + col.name; - out += '\ntype: ' + col.dataType; - if (col.collectionsType) { - out += '\ncollection: ' + col.collectionsType; - } - if (col.primary) { - out += '\nprimary'; - } - if (col.unique) { - out += '\nunique: ' + col.unique; - } - if (col.nullable) { - out += '\nnullable: ' + col.nullable; - } - if (col.defaultValue) { - out += '\ndefaultValue: ' + col.defaultValue; + filterTable(e, filterVal, col: UiColumnDefinition) { + this.$result().currentPage = 1; + if (e.keyCode === 27) { //esc + $('.table-filter').val(''); + this.filter.clear(); + this.getEntityData(); + return; + } + if (col.collectionsType || col.dataType.includes('ARRAY')) { + if (this.isValidArray(filterVal) || !filterVal) { + this.filter.set(col.name, filterVal); + } + } else { + this.filter.set(col.name, filterVal); + } + this.focusId = 'search-' + col.name; + this.getEntityData(); } - if (col.precision) { - out += '\nprecision: ' + col.precision; + paginate(p: PaginationElement) { + this.$result().currentPage = p.page; + this.currentPage.set(p.page); + this.getEntityData(); } - if (col.scale) { - out += '\nscale: ' + col.scale; + + sortTable(s: SortState) { + //todo primary ordering, secondary ordering + if (s.sorting === false) { + s.sorting = true; + s.direction = SortDirection.ASC; + } else { + if (s.direction === SortDirection.ASC) { + s.direction = SortDirection.DESC; + } else { + s.direction = SortDirection.ASC; + s.sorting = false; + } + } + this.getEntityData(); } - if (col.dimension) { - out += '\ndimension: ' + col.dimension; + + isValidArray(val: string): boolean { + if (val.startsWith('[') && val.endsWith(']')) { + try { + JSON.parse(val); + return true; + } catch (e) { + return false; + } + } + return false; } - if (col.cardinality) { - out += '\ncardinality: ' + col.cardinality; + + isValidFilter(val, col: UiColumnDefinition) { + if (!val) { + return; + } + if (col.collectionsType || col.dataType.includes('ARRAY')) { + if (!this.isValidArray(val)) { + return 'is-invalid'; + } + } + } + + + getTooltip(col: UiColumnDefinition): string { + if (!col) { + return ''; + } + let out = 'name: ' + col.name; + out += '\ntype: ' + col.dataType; + if (col.collectionsType) { + out += '\ncollection: ' + col.collectionsType; + } + if (col.primary) { + out += '\nprimary'; + } + if (col.unique) { + out += '\nunique: ' + col.unique; + } + if (col.nullable) { + out += '\nnullable: ' + col.nullable; + } + if (col.defaultValue) { + out += '\ndefaultValue: ' + col.defaultValue; + } + + if (col.precision) { + out += '\nprecision: ' + col.precision; + } + if (col.scale) { + out += '\nscale: ' + col.scale; + } + if (col.dimension) { + out += '\ndimension: ' + col.dimension; + } + if (col.cardinality) { + out += '\ncardinality: ' + col.cardinality; + } + return out; + } + + /** + * returns true if a columns can be ordered + */ + canOrder(col: UiColumnDefinition) { + return !this._types.isMultimedia(col.dataType) && !col.collectionsType; } - return out; - } - - /** - * returns true if a columns can be ordered - */ - canOrder(col: UiColumnDefinition) { - return !this._types.isMultimedia(col.dataType) && !col.collectionsType; - } } diff --git a/src/app/components/data-view/data-table/entity-config.ts b/src/app/components/data-view/data-table/entity-config.ts index 9ae9c5b3..9eab288a 100644 --- a/src/app/components/data-view/data-table/entity-config.ts +++ b/src/app/components/data-view/data-table/entity-config.ts @@ -1,9 +1,9 @@ export interface EntityConfig { - create: boolean; - update: boolean; - delete: boolean; - sort: boolean; - search: boolean; - exploring: boolean; - hideCreateView?: boolean; + create: boolean; + update: boolean; + delete: boolean; + sort: boolean; + search: boolean; + exploring: boolean; + hideCreateView?: boolean; } diff --git a/src/app/components/data-view/data-template/data-template.component.ts b/src/app/components/data-view/data-template/data-template.component.ts index da7988d0..2b2c4707 100644 --- a/src/app/components/data-view/data-template/data-template.component.ts +++ b/src/app/components/data-view/data-template/data-template.component.ts @@ -1,4 +1,17 @@ -import {Component, computed, effect, EventEmitter, inject, Input, OnDestroy, OnInit, Signal, signal, untracked, WritableSignal} from '@angular/core'; +import { + Component, + computed, + effect, + EventEmitter, + inject, + Input, + OnDestroy, + OnInit, + Signal, + signal, + untracked, + WritableSignal +} from '@angular/core'; import {RelationalResult, Result, UiColumnDefinition} from '../models/result-set.model'; import {WebuiSettingsService} from '../../../services/webui-settings.service'; import {CatalogService} from '../../../services/catalog.service'; @@ -22,567 +35,567 @@ import {CombinedResult} from '../data-view.model'; const INITIAL_TYPE = 'BIGINT'; @Component({ - selector: 'data-template', - templateUrl: './data-template.component.html', - styleUrls: ['./data-template.component.scss'] + selector: 'data-template', + templateUrl: './data-template.component.html', + styleUrls: ['./data-template.component.scss'] }) export abstract class DataTemplateComponent implements OnInit, OnDestroy { - protected readonly _settings: WebuiSettingsService = inject(WebuiSettingsService); - protected readonly _router: Router = inject(Router); - protected readonly _route: ActivatedRoute = inject(ActivatedRoute); - protected readonly _sidebar: LeftSidebarService = inject(LeftSidebarService); - protected readonly _catalog: CatalogService = inject(CatalogService); - protected readonly _crud: CrudService = inject(CrudService); - protected readonly _toast: ToasterService = inject(ToasterService); - protected readonly _types: DbmsTypesService = inject(DbmsTypesService); + protected readonly _settings: WebuiSettingsService = inject(WebuiSettingsService); + protected readonly _router: Router = inject(Router); + protected readonly _route: ActivatedRoute = inject(ActivatedRoute); + protected readonly _sidebar: LeftSidebarService = inject(LeftSidebarService); + protected readonly _catalog: CatalogService = inject(CatalogService); + protected readonly _crud: CrudService = inject(CrudService); + protected readonly _toast: ToasterService = inject(ToasterService); + protected readonly _types: DbmsTypesService = inject(DbmsTypesService); - protected readonly webSocket: WebSocket; + protected readonly webSocket: WebSocket; - @Input() set inputConfig(config: EntityConfig) { - if (!config) { - return; - } - this.entityConfig.set(config); - } - - protected readonly entityConfig: WritableSignal = signal({ - create: true, - search: true, - sort: true, - update: true, - delete: true, - exploring: false - }); - protected readonly currentRoute: WritableSignal = signal(this._route.snapshot.paramMap.get('id')); - protected readonly routeParams = toSignal(this._route.params); - protected readonly entity: Signal; - - protected readonly $result: WritableSignal = signal(null); - protected readonly loading: WritableSignal = signal(false); - protected readonly subscriptions = new Subscription(); - - @Input() set result(result: Result) { - if (!result) { - return; + @Input() set inputConfig(config: EntityConfig) { + if (!config) { + return; + } + this.entityConfig.set(config); } - this.$result.set(CombinedResult.from(result)); - } - - pagination: PaginationElement[] = []; - currentPage: WritableSignal = signal(1); - editing = -1;//-1 if not editing any row, else the index of that row - sortStates = new Map(); - filter = new Map(); - protected focusId: string; - - insertValues = new Map(); - insertDirty = new Map();//check if field has been edited (if yes, it is "dirty") - updateValues = new Map(); - - /** -1 if not uploading, 0 or 100: striped, else: showing progress */ - uploadProgress = -1; - downloadProgress = -1; - downloadingIthRow = -1; - confirm = -1; - - - protected constructor() { - this.webSocket = new WebSocket(); - this._route.params.subscribe(route => { - this.currentRoute.set(route['id']); - }); - this.entity = computed(() => { - const catalog = this._catalog.listener(); - if (!this.currentRoute || !this.currentRoute()) { - return null; - } - const route = this.currentRoute(); - if (!route) { - return null; - } - const splits = route.split('.'); - - return catalog.getEntityFromName(splits[0], splits[1]); + protected readonly entityConfig: WritableSignal = signal({ + create: true, + search: true, + sort: true, + update: true, + delete: true, + exploring: false }); + protected readonly currentRoute: WritableSignal = signal(this._route.snapshot.paramMap.get('id')); + protected readonly routeParams = toSignal(this._route.params); + protected readonly entity: Signal; - // to get the correct insert defaults - effect(() => { - if (!this.$result || !this.$result() || this.$result().error) { - return; - } - untracked(() => { - this.buildInsertObject(); - this.setPagination(); - }); - }); - } + protected readonly $result: WritableSignal = signal(null); + protected readonly loading: WritableSignal = signal(false); + protected readonly subscriptions = new Subscription(); + @Input() set result(result: Result) { + if (!result) { + return; + } + this.$result.set(CombinedResult.from(result)); + } - ngOnInit() { - this._sidebar.open(); - //listen to results - this.initWebsocket(); + pagination: PaginationElement[] = []; + currentPage: WritableSignal = signal(1); + editing = -1;//-1 if not editing any row, else the index of that row + sortStates = new Map(); + filter = new Map(); + protected focusId: string; - this.currentPage.set(+this._route.snapshot.paramMap.get('page') || 1); + insertValues = new Map(); + insertDirty = new Map();//check if field has been edited (if yes, it is "dirty") + updateValues = new Map(); - if (this.$result && this.$result()) { - this.$result().currentPage = this.currentPage(); - } + /** -1 if not uploading, 0 or 100: striped, else: showing progress */ + uploadProgress = -1; + downloadProgress = -1; + downloadingIthRow = -1; + confirm = -1; - //listen to parameter changes - this._route.params.subscribe((params) => { - this.currentPage.set(+this._route.snapshot.paramMap.get('page') || 1); - this.$result?.update(res => { - if (!res) { - return res; - } - res.currentPage = this.currentPage(); - return res; - }); + protected constructor() { + this.webSocket = new WebSocket(); + this._route.params.subscribe(route => { + this.currentRoute.set(route['id']); + }); - }); - } + this.entity = computed(() => { + const catalog = this._catalog.listener(); + if (!this.currentRoute || !this.currentRoute()) { + return null; + } + const route = this.currentRoute(); + if (!route) { + return null; + } + const splits = route.split('.'); - ngOnDestroy() { - this.subscriptions.unsubscribe(); - this.webSocket.close(); - } + return catalog.getEntityFromName(splits[0], splits[1]); + }); + // to get the correct insert defaults + effect(() => { + if (!this.$result || !this.$result() || this.$result().error) { + return; + } + untracked(() => { + this.buildInsertObject(); + this.setPagination(); + }); + }); + } - protected initWebsocket() { - const sub = this.webSocket.onMessage().subscribe({ - next: (result: Result) => { - if (!result) { - return; - } - //go to the highest page if you are "lost" (if you are on a page that is higher than the highest possible page) - if (this.$result && +this._route.snapshot.paramMap.get('page') > this.$result()?.highestPage) { - this._router.navigate(['/views/data-table/' + this.entity()?.name + '/' + this.$result().highestPage]).then(null); + ngOnInit() { + this._sidebar.open(); + //listen to results + this.initWebsocket(); + + this.currentPage.set(+this._route.snapshot.paramMap.get('page') || 1); + + if (this.$result && this.$result()) { + this.$result().currentPage = this.currentPage(); } - this.editing = -1; - this.buildInsertObject(); - - this.entityConfig.update(conf => { - if (this.entity().entityType === EntityType.ENTITY) { - conf.create = true; - conf.update = true; - conf.delete = true; - } else { - conf.create = false; - conf.update = false; - conf.delete = false; - } - return conf; - }); - this.$result.set(CombinedResult.from(result)); - this.loading.set(false); - }, error: err => { - console.log(err); - this.loading.set(false); - this.$result.set(CombinedResult.fromRelational(new RelationalResult('Server is not available'))); - } - }); - this.subscriptions.add(sub); - } + //listen to parameter changes + this._route.params.subscribe((params) => { + this.currentPage.set(+this._route.snapshot.paramMap.get('page') || 1); - removeNull(dataType: string) { - return dataType.replace(' NOT NULL', ''); - } + this.$result?.update(res => { + if (!res) { + return res; + } + res.currentPage = this.currentPage(); + return res; + }); - setPagination() { - if (!this.$result()) { - return; - } - const activePage = this.$result().currentPage; - const highestPage = this.$result().highestPage; - this.pagination = []; - if (highestPage < 2) { - return; + }); } - if (!this.entity || !this.entity()) { - return; + + ngOnDestroy() { + this.subscriptions.unsubscribe(); + this.webSocket.close(); } - const entity = this.entity(); - const entityId = entity.id; - const neighbors = 1;//from active page, show n neighbors to the left and n neighbors to the right. - const prev = new PaginationElement().withPage(entityId, Math.max(1, activePage - 1)).withLabel('<'); - if (activePage === 1) { - prev.setDisabled(); + protected initWebsocket() { + const sub = this.webSocket.onMessage().subscribe({ + next: (result: Result) => { + if (!result) { + return; + } + + //go to the highest page if you are "lost" (if you are on a page that is higher than the highest possible page) + if (this.$result && +this._route.snapshot.paramMap.get('page') > this.$result()?.highestPage) { + this._router.navigate(['/views/data-table/' + this.entity()?.name + '/' + this.$result().highestPage]).then(null); + } + this.editing = -1; + this.buildInsertObject(); + + this.entityConfig.update(conf => { + if (this.entity().entityType === EntityType.ENTITY) { + conf.create = true; + conf.update = true; + conf.delete = true; + } else { + conf.create = false; + conf.update = false; + conf.delete = false; + } + return conf; + }); + + this.$result.set(CombinedResult.from(result)); + this.loading.set(false); + }, error: err => { + console.log(err); + this.loading.set(false); + this.$result.set(CombinedResult.fromRelational(new RelationalResult('Server is not available'))); + } + }); + this.subscriptions.add(sub); } - this.pagination.push(prev); - if (activePage === 1) { - this.pagination.push(new PaginationElement().withPage(entityId, 1).setActive()); - } else { - this.pagination.push(new PaginationElement().withPage(entityId, 1)); + removeNull(dataType: string) { + return dataType.replace(' NOT NULL', ''); } - if (activePage - neighbors > 2) { - this.pagination.push(new PaginationElement().withLabel('..').setDisabled()); - } - let counter = Math.max(2, activePage - neighbors); - while (counter <= activePage + neighbors && counter <= highestPage) { - if (counter === activePage) { - this.pagination.push(new PaginationElement().withPage(entityId, counter).setActive()); - } else { - this.pagination.push(new PaginationElement().withPage(entityId, counter)); - } - counter++; - } - counter--; - if (counter < highestPage) { - if (counter + neighbors < highestPage) { - this.pagination.push(new PaginationElement().withLabel('..').setDisabled()); - } - this.pagination.push(new PaginationElement().withPage(entityId, highestPage)); - } - const next = new PaginationElement().withPage(entityId, Math.min(highestPage, activePage + 1)).withLabel('>'); - if (activePage === highestPage) { - next.setDisabled(); - } + setPagination() { + if (!this.$result()) { + return; + } + const activePage = this.$result().currentPage; + const highestPage = this.$result().highestPage; + this.pagination = []; + if (highestPage < 2) { + return; + } + if (!this.entity || !this.entity()) { + return; + } + const entity = this.entity(); + const entityId = entity.id; - this.pagination.push(next); + const neighbors = 1;//from active page, show n neighbors to the left and n neighbors to the right. + const prev = new PaginationElement().withPage(entityId, Math.max(1, activePage - 1)).withLabel('<'); - return this.pagination; - } + if (activePage === 1) { + prev.setDisabled(); + } - paginate(p: PaginationElement) { - this.$result().currentPage = p.page; - this.getEntityData(); - } + this.pagination.push(prev); + if (activePage === 1) { + this.pagination.push(new PaginationElement().withPage(entityId, 1).setActive()); + } else { + this.pagination.push(new PaginationElement().withPage(entityId, 1)); + } + if (activePage - neighbors > 2) { + this.pagination.push(new PaginationElement().withLabel('..').setDisabled()); - public getEntityData() { - const filterObj = this.mapToObject(this.filter); - const sortState = {}; - this.$result()?.header?.forEach((h: UiColumnDefinition) => { - this.sortStates.set(h.name, h.sort); - sortState[h.name] = h.sort; - }); - const request = new EntityRequest(this.entity()?.id, this._catalog.getNamespaceFromId(this.entity()?.namespaceId).name, this.currentPage(), filterObj, sortState); - if (!this._crud.getEntityData(this.webSocket, request)) { - this.$result.set(CombinedResult.fromRelational(new RelationalResult('Could not establish a connection with the server.'))); - } - } + } + let counter = Math.max(2, activePage - neighbors); + while (counter <= activePage + neighbors && counter <= highestPage) { + if (counter === activePage) { + this.pagination.push(new PaginationElement().withPage(entityId, counter).setActive()); + } else { + this.pagination.push(new PaginationElement().withPage(entityId, counter)); + } + counter++; + } + counter--; + if (counter < highestPage) { + if (counter + neighbors < highestPage) { + this.pagination.push(new PaginationElement().withLabel('..').setDisabled()); + } + this.pagination.push(new PaginationElement().withPage(entityId, highestPage)); + } + const next = new PaginationElement().withPage(entityId, Math.min(highestPage, activePage + 1)).withLabel('>'); + if (activePage === highestPage) { + next.setDisabled(); + } - mapToObject(map: Map) { - const obj = {}; - map.forEach((v, k) => { - obj[k] = v; - }); - return obj; - } + this.pagination.push(next); - deleteRow(values: string[], i) { - if (this.confirm !== i) { - this.confirm = i; - return; + return this.pagination; } - if (this.$result().dataModel === DataModel.DOCUMENT) { - this.adjustDocument(Method.DROP, values[0]); - return; + + paginate(p: PaginationElement) { + this.$result().currentPage = p.page; + this.getEntityData(); } - const rowMap = new Map(); - values.forEach((val, key) => { - rowMap.set(this.$result().header[key].name, val); - }); - const row = this.mapToObject(rowMap); - const request = new DeleteRequest(this.entity()?.id, row); - const emitResult = new EventEmitter(); - this._crud.deleteTuple(request).subscribe({ - next: (result: RelationalResult) => { - emitResult.emit(result); - if (result.error) { - this._toast.exception(result, 'Could not delete this tuple:'); - } else { - this.getEntityData(); + public getEntityData() { + const filterObj = this.mapToObject(this.filter); + const sortState = {}; + this.$result()?.header?.forEach((h: UiColumnDefinition) => { + this.sortStates.set(h.name, h.sort); + sortState[h.name] = h.sort; + }); + const request = new EntityRequest(this.entity()?.id, this._catalog.getNamespaceFromId(this.entity()?.namespaceId).name, this.currentPage(), filterObj, sortState); + if (!this._crud.getEntityData(this.webSocket, request)) { + this.$result.set(CombinedResult.fromRelational(new RelationalResult('Could not establish a connection with the server.'))); } - }, error: err => { - this._toast.error('Could not delete this tuple.'); - console.log(err); - emitResult.emit(new RelationalResult('Could not delete this tuple.')); - } - }); - return emitResult; - } - - - /** - * In the card and carousel view, show mm data first (only image, video and audio columns) - */ - showFirst(dataType: string) { - switch (dataType) { - case 'IMAGE': - case 'VIDEO': - case 'AUDIO': - return true; } - return false; - } - - getFileLink(data: string) { - return this._crud.getFileUrl(data); - } - - getFile(data: string, index: number) { - this.downloadingIthRow = index; - this.downloadProgress = 0; - this._crud.getFile(data).subscribe({ - next: res => { - if (res.type && res.type === HttpEventType.DownloadProgress) { - this.downloadProgress = Math.round(100 * res.loaded / res.total); - } else if (res.type === HttpEventType.Response) { - //see https://stackoverflow.com/questions/51960172/ - const url = window.URL.createObjectURL(res.body); - window.open(url); - } - }, - error: err => { - console.log(err); - } - }).add(() => { - this.downloadingIthRow = -1; - this.downloadProgress = -1; - }); - - } - triggerEditing(i) { - if (this.confirm !== -1) { - //when double-clicking the delete btn - return; + mapToObject(map: Map) { + const obj = {}; + map.forEach((v, k) => { + obj[k] = v; + }); + return obj; } - if (this.entityConfig.update) { - this.updateValues.clear(); - this.$result().data[i].forEach((v, k) => { - if (this.$result().header[k].dataType === 'bool') { - this.updateValues.set(this.$result().header[k].name, this.getBoolean(v)); + + deleteRow(values: string[], i) { + if (this.confirm !== i) { + this.confirm = i; + return; } - //assign multimedia types: null if the item is NULL, else undefined - //null items will be submitted and updated, undefined items will not be part of the UPDATE statement - else if (this._types.isMultimedia(this.$result().header[k].dataType)) { - if (v === null) { - this.updateValues.set(this.$result().header[k].name, null); - } else { - this.updateValues.set(this.$result().header[k].name, undefined); - } - } else { - this.updateValues.set(this.$result().header[k].name, v); + if (this.$result().dataModel === DataModel.DOCUMENT) { + this.adjustDocument(Method.DROP, values[0]); + return; } - }); - this.editing = i; + + const rowMap = new Map(); + values.forEach((val, key) => { + rowMap.set(this.$result().header[key].name, val); + }); + const row = this.mapToObject(rowMap); + const request = new DeleteRequest(this.entity()?.id, row); + const emitResult = new EventEmitter(); + this._crud.deleteTuple(request).subscribe({ + next: (result: RelationalResult) => { + emitResult.emit(result); + if (result.error) { + this._toast.exception(result, 'Could not delete this tuple:'); + } else { + this.getEntityData(); + } + }, error: err => { + this._toast.error('Could not delete this tuple.'); + console.log(err); + emitResult.emit(new RelationalResult('Could not delete this tuple.')); + } + }); + return emitResult; } - } - - getBoolean(value: any): Boolean { - switch (value) { - case true: - case 'true': - case 't': - case 1: - case '1': - case 'on': - case 'yes': - return true; - case 'null': - case 'NULL': - case null: - return null; - default: + + + /** + * In the card and carousel view, show mm data first (only image, video and audio columns) + */ + showFirst(dataType: string) { + switch (dataType) { + case 'IMAGE': + case 'VIDEO': + case 'AUDIO': + return true; + } return false; } - } - inputChange(name: string, e) { - this.insertValues.set(name, e); - this.insertDirty.set(name, true); - } + getFileLink(data: string) { + return this._crud.getFileUrl(data); + } + + getFile(data: string, index: number) { + this.downloadingIthRow = index; + this.downloadProgress = 0; + this._crud.getFile(data).subscribe({ + next: res => { + if (res.type && res.type === HttpEventType.DownloadProgress) { + this.downloadProgress = Math.round(100 * res.loaded / res.total); + } else if (res.type === HttpEventType.Response) { + //see https://stackoverflow.com/questions/51960172/ + const url = window.URL.createObjectURL(res.body); + window.open(url); + } + }, + error: err => { + console.log(err); + } + }).add(() => { + this.downloadingIthRow = -1; + this.downloadProgress = -1; + }); - insertTuple() { - if (this.$result().dataModel === DataModel.DOCUMENT) { - this.adjustDocument(Method.ADD); - return; } - const formData = new FormData(); - this.insertValues.forEach((v, k) => { - //only values with dirty state will be submitted. Columns that are not nullable are already set dirty - if (this.insertDirty.get(k) === true) { - let value; - if (isNaN(v)) { - value = v; - } else { - value = String(v); - } - formData.append(k, value); - } - }); - formData.append('entityId', String(this.entity().id)); - this.uploadProgress = 100;//show striped progressbar - const emitResult = new EventEmitter(); - - this._crud.insertTuple(formData).subscribe({ - next: res => { - if (res.type && res.type === HttpEventType.UploadProgress) { - this.uploadProgress = Math.round(100 * res.loaded / res.total); - } else if (res.type === HttpEventType.Response) { - this.uploadProgress = -1; - const result = res.body; - emitResult.emit(result); - if (result.error) { - this._toast.exception(result, 'Could not insert the data', 'insert error'); - } else if (result.affectedTuples === 1) { - $('.insert-input').val(''); - this.insertValues.clear(); - this.buildInsertObject(); - this.getEntityData(); - } - } - }, - error: err => { - this._toast.error('Could not insert the data.'); - console.log(err); - emitResult.emit(new RelationalResult('Could not insert the data.')); - } - }).add(() => this.uploadProgress = -1); - return emitResult; - } - - private adjustDocument(method: Method, initialData: string = '') { - const entity = this.entity(); - switch (method) { - case Method.ADD: - const data = this.insertValues.get('_id'); - const add = `db.${entity.name}.insert(${data})`; - - this._crud.anyQuery(this.webSocket, new QueryRequest(add, false, true, 'mql', this.$result().namespace)); - this.insertValues.clear(); - this.getEntityData(); - break; - case Method.MODIFY: - const values = new Map();//previous values - for (let i = 0; i < this.$result().header.length; i++) { - values.set(this.$result().header[i].name, this.$result().data[this.editing][i]); - i++; + + triggerEditing(i) { + if (this.confirm !== -1) { + //when double-clicking the delete btn + return; } - const updated = this.updateValues.get('_id'); - const parsed = JSON.parse(updated); - if (parsed.hasOwnProperty('_id')) { - const modify = `db.${entity.name}.updateMany({"_id": "${parsed['_id']}"}, {"$set": ${updated}})`; - this._crud.anyQuery(this.webSocket, new QueryRequest(modify, false, true, 'mql', this.$result().namespace)); - this.insertValues.clear(); - this.getEntityData(); + if (this.entityConfig.update) { + this.updateValues.clear(); + this.$result().data[i].forEach((v, k) => { + if (this.$result().header[k].dataType === 'bool') { + this.updateValues.set(this.$result().header[k].name, this.getBoolean(v)); + } + //assign multimedia types: null if the item is NULL, else undefined + //null items will be submitted and updated, undefined items will not be part of the UPDATE statement + else if (this._types.isMultimedia(this.$result().header[k].dataType)) { + if (v === null) { + this.updateValues.set(this.$result().header[k].name, null); + } else { + this.updateValues.set(this.$result().header[k].name, undefined); + } + } else { + this.updateValues.set(this.$result().header[k].name, v); + } + }); + this.editing = i; } - break; - case Method.DROP: - const parsedDelete = JSON.parse(initialData); - if (parsedDelete.hasOwnProperty('_id')) { - const modify = `db.${entity.name}.deleteMany({"_id": "${parsedDelete['_id']}" })`; - this._crud.anyQuery(this.webSocket, new QueryRequest(modify, false, true, 'mql', this.$result().namespace)); - this.insertValues.clear(); - this.getEntityData(); + } + + getBoolean(value: any): Boolean { + switch (value) { + case true: + case 'true': + case 't': + case 1: + case '1': + case 'on': + case 'yes': + return true; + case 'null': + case 'NULL': + case null: + return null; + default: + return false; } - break; } - } - buildInsertObject() { - if (this.entityConfig && !this.entityConfig().create || !this.$result()) { - return; + inputChange(name: string, e) { + this.insertValues.set(name, e); + this.insertDirty.set(name, true); } - this.insertValues.clear(); - this.insertDirty.clear(); - - if (this.$result().header) { - for (const g of this.$result().header) { - //set insertDirty - if (!g.nullable && g.dataType !== 'serial' && g.defaultValue === undefined) { - //set dirty if not nullable, so it will be submitted, except if it has autoincrement (dataType 'serial') or a default value - this.insertDirty.set(g.name, true); - } else { - this.insertDirty.set(g.name, false); - } - //set insertValues - if (g.nullable) { - this.insertValues.set(g.name, null); - continue; + + insertTuple() { + if (this.$result().dataModel === DataModel.DOCUMENT) { + this.adjustDocument(Method.ADD); + return; } + const formData = new FormData(); + this.insertValues.forEach((v, k) => { + //only values with dirty state will be submitted. Columns that are not nullable are already set dirty + if (this.insertDirty.get(k) === true) { + let value; + if (isNaN(v)) { + value = v; + } else { + value = String(v); + } + formData.append(k, value); + } + }); + formData.append('entityId', String(this.entity().id)); + this.uploadProgress = 100;//show striped progressbar + const emitResult = new EventEmitter(); + + this._crud.insertTuple(formData).subscribe({ + next: res => { + if (res.type && res.type === HttpEventType.UploadProgress) { + this.uploadProgress = Math.round(100 * res.loaded / res.total); + } else if (res.type === HttpEventType.Response) { + this.uploadProgress = -1; + const result = res.body; + emitResult.emit(result); + if (result.error) { + this._toast.exception(result, 'Could not insert the data', 'insert error'); + } else if (result.affectedTuples === 1) { + $('.insert-input').val(''); + this.insertValues.clear(); + this.buildInsertObject(); + this.getEntityData(); + } + } + }, + error: err => { + this._toast.error('Could not insert the data.'); + console.log(err); + emitResult.emit(new RelationalResult('Could not insert the data.')); + } + }).add(() => this.uploadProgress = -1); + return emitResult; + } - if (this._types.isNumeric((g.dataType))) { - this.insertValues.set(g.name, 0); - } else if (this._types.isBoolean(g.dataType)) { - this.insertValues.set(g.name, false); - } else { - this.insertValues.set(g.name, ''); + private adjustDocument(method: Method, initialData: string = '') { + const entity = this.entity(); + switch (method) { + case Method.ADD: + const data = this.insertValues.get('_id'); + const add = `db.${entity.name}.insert(${data})`; + + this._crud.anyQuery(this.webSocket, new QueryRequest(add, false, true, 'mql', this.$result().namespace)); + this.insertValues.clear(); + this.getEntityData(); + break; + case Method.MODIFY: + const values = new Map();//previous values + for (let i = 0; i < this.$result().header.length; i++) { + values.set(this.$result().header[i].name, this.$result().data[this.editing][i]); + i++; + } + const updated = this.updateValues.get('_id'); + const parsed = JSON.parse(updated); + if (parsed.hasOwnProperty('_id')) { + const modify = `db.${entity.name}.updateMany({"_id": "${parsed['_id']}"}, {"$set": ${updated}})`; + this._crud.anyQuery(this.webSocket, new QueryRequest(modify, false, true, 'mql', this.$result().namespace)); + this.insertValues.clear(); + this.getEntityData(); + } + break; + case Method.DROP: + const parsedDelete = JSON.parse(initialData); + if (parsedDelete.hasOwnProperty('_id')) { + const modify = `db.${entity.name}.deleteMany({"_id": "${parsedDelete['_id']}" })`; + this._crud.anyQuery(this.webSocket, new QueryRequest(modify, false, true, 'mql', this.$result().namespace)); + this.insertValues.clear(); + this.getEntityData(); + } + break; } - } } - } + buildInsertObject() { + if (this.entityConfig && !this.entityConfig().create || !this.$result()) { + return; + } + this.insertValues.clear(); + this.insertDirty.clear(); + + if (this.$result().header) { + for (const g of this.$result().header) { + //set insertDirty + if (!g.nullable && g.dataType !== 'serial' && g.defaultValue === undefined) { + //set dirty if not nullable, so it will be submitted, except if it has autoincrement (dataType 'serial') or a default value + this.insertDirty.set(g.name, true); + } else { + this.insertDirty.set(g.name, false); + } + //set insertValues + if (g.nullable) { + this.insertValues.set(g.name, null); + continue; + } + + if (this._types.isNumeric((g.dataType))) { + this.insertValues.set(g.name, 0); + } else if (this._types.isBoolean(g.dataType)) { + this.insertValues.set(g.name, false); + } else { + this.insertValues.set(g.name, ''); + } + } + } + } - newUpdateValue(key, val) { - this.updateValues.set(key, val); - } - updateTuple() { - if (this.$result().dataModel === DataModel.DOCUMENT) { - this.adjustDocument(Method.MODIFY); - return; + newUpdateValue(key, val) { + this.updateValues.set(key, val); } - const oldValues = new Map();//previous values - for (let i = 0; i < this.$result().header.length; i++) { - oldValues.set(this.$result().header[i].name, this.$result().data[this.editing][i]); - i++; - } - const formData = new FormData(); - formData.append('entityId', String(this.entity()?.id)); - formData.append('oldValues', JSON.stringify(this.mapToObject(oldValues))); - for (const [k, v] of this.updateValues) { - if (v === undefined) { - //don't add undefined file inputs, but if they are null, they need to be added - continue; - } - if (!(v instanceof File)) { - //stringify to distinguish between null and 'null' - formData.append(k, JSON.stringify(v)); - } else { - formData.append(k, v); - } - } - this.uploadProgress = 100;//show striped progressbar - //const req = new UpdateRequest(this.resultSet.table, this.mapToObject(this.updateValues), this.mapToObject(oldValues)); - this._crud.updateTuple(formData).subscribe({ - next: res => { - if (res.type && res.type === HttpEventType.UploadProgress) { - this.uploadProgress = Math.round(100 * res.loaded / res.total); - } else if (res.type === HttpEventType.Response) { - this.uploadProgress = -1; - const result = res.body; - if (result.affectedTuples) { - this.getEntityData(); - let rows = ' tuples'; - if (result.affectedTuples === 1) { - rows = ' tuple'; + updateTuple() { + if (this.$result().dataModel === DataModel.DOCUMENT) { + this.adjustDocument(Method.MODIFY); + return; + } + + const oldValues = new Map();//previous values + for (let i = 0; i < this.$result().header.length; i++) { + oldValues.set(this.$result().header[i].name, this.$result().data[this.editing][i]); + i++; + } + const formData = new FormData(); + formData.append('entityId', String(this.entity()?.id)); + formData.append('oldValues', JSON.stringify(this.mapToObject(oldValues))); + for (const [k, v] of this.updateValues) { + if (v === undefined) { + //don't add undefined file inputs, but if they are null, they need to be added + continue; + } + if (!(v instanceof File)) { + //stringify to distinguish between null and 'null' + formData.append(k, JSON.stringify(v)); + } else { + formData.append(k, v); } - this._toast.success('Updated ' + result.affectedTuples + rows, result.query, 'update', ToastDuration.SHORT); - } else if (result.error) { - this._toast.exception(result, 'Could not update this tuple'); - } } - }, - error: err => { - this._toast.error('Could not update the data.'); - console.log(err); - } - }).add(() => this.uploadProgress = -1); - } + this.uploadProgress = 100;//show striped progressbar + //const req = new UpdateRequest(this.resultSet.table, this.mapToObject(this.updateValues), this.mapToObject(oldValues)); + this._crud.updateTuple(formData).subscribe({ + next: res => { + if (res.type && res.type === HttpEventType.UploadProgress) { + this.uploadProgress = Math.round(100 * res.loaded / res.total); + } else if (res.type === HttpEventType.Response) { + this.uploadProgress = -1; + const result = res.body; + if (result.affectedTuples) { + this.getEntityData(); + let rows = ' tuples'; + if (result.affectedTuples === 1) { + rows = ' tuple'; + } + this._toast.success('Updated ' + result.affectedTuples + rows, result.query, 'update', ToastDuration.SHORT); + } else if (result.error) { + this._toast.exception(result, 'Could not update this tuple'); + } + } + }, + error: err => { + this._toast.error('Could not update the data.'); + console.log(err); + } + }).add(() => this.uploadProgress = -1); + } } diff --git a/src/app/components/data-view/data-view.component.ts b/src/app/components/data-view/data-view.component.ts index 96ce78b0..48e24356 100644 --- a/src/app/components/data-view/data-view.component.ts +++ b/src/app/components/data-view/data-view.component.ts @@ -1,4 +1,18 @@ -import {Component, computed, effect, EventEmitter, inject, Input, OnDestroy, Output, Signal, signal, untracked, ViewChild, WritableSignal} from '@angular/core'; +import { + Component, + computed, + effect, + EventEmitter, + inject, + Input, + OnDestroy, + Output, + Signal, + signal, + untracked, + ViewChild, + WritableSignal +} from '@angular/core'; import {DataPresentationType, QueryLanguage, Result} from './models/result-set.model'; import {EntityConfig} from './data-table/entity-config'; import {CrudService} from '../../services/crud.service'; @@ -19,150 +33,150 @@ import {CombinedResult} from './data-view.model'; import {ViewComponent} from './view/view.component'; export class ViewInformation { - freshness: string; - fullQuery: string; - tableType: string; - newViewName: string; - initialQuery: string; - stores: string; - interval: number; - timeUnit: string; - - - constructor(tableType: string, newViewName: string) { - this.tableType = tableType; - this.newViewName = newViewName; - } + freshness: string; + fullQuery: string; + tableType: string; + newViewName: string; + initialQuery: string; + stores: string; + interval: number; + timeUnit: string; + + + constructor(tableType: string, newViewName: string) { + this.tableType = tableType; + this.newViewName = newViewName; + } } @Component({ - selector: 'app-data-view', - templateUrl: './data-view.component.html', - styleUrls: ['./data-view.component.scss'] + selector: 'app-data-view', + templateUrl: './data-view.component.html', + styleUrls: ['./data-view.component.scss'] }) export class DataViewComponent implements OnDestroy { - public readonly _crud = inject(CrudService); - public readonly _toast = inject(ToasterService); - public readonly _route = inject(ActivatedRoute); - public readonly _router = inject(Router); - public readonly _types = inject(DbmsTypesService); - public readonly _settings = inject(WebuiSettingsService); - public readonly _sidebar = inject(LeftSidebarService); - public readonly _catalog = inject(CatalogService); - - - constructor() { - this.webSocket = new WebSocket(); - - this.$tables = computed(() => { - const catalog = this._catalog.listener(); - const entities = this._catalog.getEntities(null); - return entities.filter(e => e.dataModel === DataModel.RELATIONAL) - .map(n => Table.fromModel(n)) - .sort((a, b) => a.name.localeCompare(b.name)); - }); - - effect(() => { - if (!this.$result || !this.$result()) { - return; - } - - untracked(() => { - switch (this.$result().dataModel) { - case DataModel.DOCUMENT: - this.$presentationType.set(DataPresentationType.CARD); - break; - case DataModel.RELATIONAL: - this.$presentationType.set(DataPresentationType.TABLE); - break; - case DataModel.GRAPH: - this.$presentationType.set(DataPresentationType.GRAPH); - break; - default: - this.$presentationType.set(DataPresentationType.TABLE); - } - }); - - }); - } + public readonly _crud = inject(CrudService); + public readonly _toast = inject(ToasterService); + public readonly _route = inject(ActivatedRoute); + public readonly _router = inject(Router); + public readonly _types = inject(DbmsTypesService); + public readonly _settings = inject(WebuiSettingsService); + public readonly _sidebar = inject(LeftSidebarService); + public readonly _catalog = inject(CatalogService); + + + constructor() { + this.webSocket = new WebSocket(); + + this.$tables = computed(() => { + const catalog = this._catalog.listener(); + const entities = this._catalog.getEntities(null); + return entities.filter(e => e.dataModel === DataModel.RELATIONAL) + .map(n => Table.fromModel(n)) + .sort((a, b) => a.name.localeCompare(b.name)); + }); + + effect(() => { + if (!this.$result || !this.$result()) { + return; + } + + untracked(() => { + switch (this.$result().dataModel) { + case DataModel.DOCUMENT: + this.$presentationType.set(DataPresentationType.CARD); + break; + case DataModel.RELATIONAL: + this.$presentationType.set(DataPresentationType.TABLE); + break; + case DataModel.GRAPH: + this.$presentationType.set(DataPresentationType.GRAPH); + break; + default: + this.$presentationType.set(DataPresentationType.TABLE); + } + }); + + }); + } - @ViewChild(ViewComponent, {static: false}) - public readonly view: ViewComponent; - public readonly $result: WritableSignal = signal(null); + @ViewChild(ViewComponent, {static: false}) + public readonly view: ViewComponent; + public readonly $result: WritableSignal = signal(null); - @Input() - set result(result: Result) { - if (!result) { - return; + @Input() + set result(result: Result) { + if (!result) { + return; + } + this.$result.set(CombinedResult.from(result)); } - this.$result.set(CombinedResult.from(result)); - } - @Input() - $entity?: Signal; + @Input() + $entity?: Signal; - @Input() - config: EntityConfig; + @Input() + config: EntityConfig; - @Output() - readonly viewQueryConsumer = new EventEmitter(); + @Output() + readonly viewQueryConsumer = new EventEmitter(); - readonly $loading: WritableSignal = signal(false); + readonly $loading: WritableSignal = signal(false); - @Input() set loading(loading: boolean) { - this.$loading.set(loading); - } + @Input() set loading(loading: boolean) { + this.$loading.set(loading); + } - $modalVisible: WritableSignal = signal(false); + $modalVisible: WritableSignal = signal(false); - $presentationType: WritableSignal = signal(DataPresentationType.TABLE); - presentationTypes: typeof DataPresentationType = DataPresentationType; + $presentationType: WritableSignal = signal(DataPresentationType.TABLE); + presentationTypes: typeof DataPresentationType = DataPresentationType; - player: Plyr; - webSocket: WebSocket; - subscriptions = new Subscription(); + player: Plyr; + webSocket: WebSocket; + subscriptions = new Subscription(); - query: string; + query: string; - readonly $tables: Signal; - $dataModel: Signal = computed(() => this.$result()?.dataModel); + readonly $tables: Signal; + $dataModel: Signal = computed(() => this.$result()?.dataModel); - protected readonly NamespaceType = DataModel; + protected readonly NamespaceType = DataModel; - ngOnDestroy() { - this.subscriptions.unsubscribe(); - this.webSocket.close(); - } + ngOnDestroy() { + this.subscriptions.unsubscribe(); + this.webSocket.close(); + } - isDMLResult() { - return this.$result() - && this.$result().affectedTuples === 1 - && this.$result().header[0].dataType === 'BIGINT' - && this.$result().header[0].name === 'ROWCOUNT'; - } + isDMLResult() { + return this.$result() + && this.$result().affectedTuples === 1 + && this.$result().header[0].dataType === 'BIGINT' + && this.$result().header[0].name === 'ROWCOUNT'; + } - checkModelAndLanguage() { - return (this.$result().dataModel === DataModel.DOCUMENT && this.$result().language === QueryLanguage.MQL) || - (this.$result().dataModel === DataModel.RELATIONAL && this.$result().language === QueryLanguage.SQL); - } + checkModelAndLanguage() { + return (this.$result().dataModel === DataModel.DOCUMENT && this.$result().language === QueryLanguage.MQL) || + (this.$result().dataModel === DataModel.RELATIONAL && this.$result().language === QueryLanguage.SQL); + } - showCreateView() { - return !this.config.hideCreateView - && this.$result().data - && !(this._router.url.startsWith('/views/data-table/')) - && !this.isDMLResult() - && this.$result().language !== QueryLanguage.CQL - && this.checkModelAndLanguage(); - } + showCreateView() { + return !this.config.hideCreateView + && this.$result().data + && !(this._router.url.startsWith('/views/data-table/')) + && !this.isDMLResult() + && this.$result().language !== QueryLanguage.CQL + && this.checkModelAndLanguage(); + } - showAny(): boolean { - return !(this.$dataModel() === DataModel.RELATIONAL || this.$dataModel() === DataModel.DOCUMENT); + showAny(): boolean { + return !(this.$dataModel() === DataModel.RELATIONAL || this.$dataModel() === DataModel.DOCUMENT); - } + } } diff --git a/src/app/components/data-view/data-view.model.ts b/src/app/components/data-view/data-view.model.ts index f44a15a2..004a6771 100644 --- a/src/app/components/data-view/data-view.model.ts +++ b/src/app/components/data-view/data-view.model.ts @@ -1,108 +1,117 @@ import {DataModel} from '../../models/ui-request.model'; -import {DocumentResult, FieldDefinition, GraphResult, QueryLanguage, RelationalResult, Result, ResultException, UiColumnDefinition} from './models/result-set.model'; +import { + DocumentResult, + FieldDefinition, + GraphResult, + QueryLanguage, + RelationalResult, + Result, + ResultException, + UiColumnDefinition +} from './models/result-set.model'; import {EntityType} from '../../models/catalog.model'; export enum Freshness { - UPDATE = 'UPDATE', - INTERVAL = 'INTERVAL', - MANUAL = 'MANUAL' + UPDATE = 'UPDATE', + INTERVAL = 'INTERVAL', + MANUAL = 'MANUAL' } export enum TimeUnits { - MILLISECONDS = 'milliseconds', - SECONDS = 'seconds', - MINUTES = 'minutes', - HOURS = 'hours', - DAYS = 'days' + MILLISECONDS = 'milliseconds', + SECONDS = 'seconds', + MINUTES = 'minutes', + HOURS = 'hours', + DAYS = 'days' } export enum ViewType { - VIEW = 'VIEW', - MATERIALIZED = 'MATERIALIZED' + VIEW = 'VIEW', + MATERIALIZED = 'MATERIALIZED' } export class CombinedResult { - dataModel: DataModel; - namespace: string; - query: string; - data: string[][]; - header: FieldDefinition[] | UiColumnDefinition[]; - exception: ResultException; - error: string; - language: QueryLanguage; - hasMore: boolean; - currentPage: number; - highestPage: number; - entityName: string; - entityId: number; - entites: string[]; - affectedTuples: number; - type: EntityType;//"table" or "view" + dataModel: DataModel; + namespace: string; + query: string; + data: string[][]; + header: FieldDefinition[] | UiColumnDefinition[]; + exception: ResultException; + error: string; + language: QueryLanguage; + hasMore: boolean; + currentPage: number; + highestPage: number; + entityName: string; + entityId: number; + entites: string[]; + affectedTuples: number; + type: EntityType;//"table" or "view" - static fromRelational(relational: RelationalResult): CombinedResult { - const res = new CombinedResult(); - res.header = relational.header; - res.data = relational.data; - res.namespace = relational.namespace; - res.dataModel = relational.dataModel; - res.currentPage = relational.currentPage; - res.highestPage = relational.highestPage; - res.type = relational.type; - res.error = relational.error; - res.hasMore = relational.hasMore; - res.entityId = relational.tableId; - res.entityName = relational.table; - res.affectedTuples = relational.affectedTuples; - res.language = relational.language; - res.query = relational.query + static fromRelational(relational: RelationalResult): CombinedResult { + const res = new CombinedResult(); + res.header = relational.header; + res.data = relational.data; + res.namespace = relational.namespace; + res.dataModel = relational.dataModel; + res.currentPage = relational.currentPage; + res.highestPage = relational.highestPage; + res.type = relational.type; + res.error = relational.error; + res.hasMore = relational.hasMore; + res.entityId = relational.tableId; + res.entityName = relational.table; + res.affectedTuples = relational.affectedTuples; + res.language = relational.language; + res.query = relational.query; - return res; - } - - static fromDocument(doc: DocumentResult): CombinedResult { - const res = new CombinedResult(); - res.header = doc.header; - res.data = doc.data.map(t => new Array(t)); - res.namespace = doc.namespace; - res.dataModel = doc.dataModel; - res.currentPage = doc.currentPage; - res.highestPage = doc.highestPage; - res.error = doc.error; - res.hasMore = doc.hasMore; - res.language = doc.language; - res.query = doc.query; - return res; - } + return res; + } - static fromGraph(graph: GraphResult): CombinedResult { - const res = new CombinedResult(); - res.header = graph.header; - res.data = graph.data; - res.namespace = graph.namespace; - res.dataModel = graph.dataModel; - res.currentPage = graph.currentPage; - res.highestPage = graph.highestPage; - res.error = graph.error; - res.hasMore = graph.hasMore; - res.language = graph.language; - res.query = graph.query; - return res; - } + static fromDocument(doc: DocumentResult): CombinedResult { + const res = new CombinedResult(); + res.header = doc.header; + res.data = doc.data.map(t => new Array(t)); + res.namespace = doc.namespace; + res.dataModel = doc.dataModel; + res.currentPage = doc.currentPage; + res.highestPage = doc.highestPage; + res.error = doc.error; + res.hasMore = doc.hasMore; + res.language = doc.language; + res.query = doc.query; + return res; + } - static from(result: Result) { - if (result instanceof CombinedResult) { - return result; + static fromGraph(graph: GraphResult): CombinedResult { + const res = new CombinedResult(); + res.header = graph.header; + res.data = graph.data; + res.namespace = graph.namespace; + res.dataModel = graph.dataModel; + res.currentPage = graph.currentPage; + res.highestPage = graph.highestPage; + res.error = graph.error; + res.hasMore = graph.hasMore; + res.language = graph.language; + res.query = graph.query; + return res; } - switch (result.dataModel) { - case DataModel.DOCUMENT: - return CombinedResult.fromDocument(result); - case DataModel.RELATIONAL: - return CombinedResult.fromRelational(result as RelationalResult); - case DataModel.GRAPH: - return CombinedResult.fromGraph(result); - default: - return CombinedResult.fromRelational(result as RelationalResult); + static from(result: Result) { + if (result instanceof CombinedResult) { + return result; + } + + switch (result.dataModel) { + case DataModel.DOCUMENT: + return CombinedResult.fromDocument(result); + case DataModel.RELATIONAL: + return CombinedResult.fromRelational(result as RelationalResult); + case DataModel.GRAPH: + return CombinedResult.fromGraph(result); + default: + return CombinedResult.fromRelational(result as RelationalResult); + } } - } } diff --git a/src/app/components/data-view/expandable-text/expandable-text.component.ts b/src/app/components/data-view/expandable-text/expandable-text.component.ts index 269d316e..07ce0a2b 100644 --- a/src/app/components/data-view/expandable-text/expandable-text.component.ts +++ b/src/app/components/data-view/expandable-text/expandable-text.component.ts @@ -1,20 +1,20 @@ import {Component, Input, OnInit} from '@angular/core'; @Component({ - selector: 'app-expandable-text', - templateUrl: './expandable-text.component.html', - styleUrls: ['./expandable-text.component.scss'] + selector: 'app-expandable-text', + templateUrl: './expandable-text.component.html', + styleUrls: ['./expandable-text.component.scss'] }) export class ExpandableTextComponent implements OnInit { - @Input() expand? = false; - @Input() text?: string; - @Input() sliceLength? = 1000; + @Input() expand? = false; + @Input() text?: string; + @Input() sliceLength? = 1000; - constructor() { - } + constructor() { + } - ngOnInit(): void { - } + ngOnInit(): void { + } } diff --git a/src/app/components/data-view/input/input.component.ts b/src/app/components/data-view/input/input.component.ts index 10fd8968..9671c237 100644 --- a/src/app/components/data-view/input/input.component.ts +++ b/src/app/components/data-view/input/input.component.ts @@ -1,4 +1,16 @@ -import {AfterViewInit, Component, ElementRef, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild} from '@angular/core'; +import { + AfterViewInit, + Component, + ElementRef, + EventEmitter, + inject, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, + ViewChild +} from '@angular/core'; import {FieldDefinition, UiColumnDefinition} from '../models/result-set.model'; import {DbmsTypesService} from '../../../services/dbms-types.service'; import * as $ from 'jquery'; @@ -6,230 +18,230 @@ import flatpickr from 'flatpickr'; import {InputValidation} from '../models/sort-state.model'; function getObjectId() { - // https://stackoverflow.com/questions/1349404/generate-random-string-characters-in-javascript - const s = 'abcdef0123456789'; - return 'ObjectId(' + Array.apply(null, Array(24)).map(() => s.charAt(Math.floor(Math.random() * s.length))).join('') + ')'; + // https://stackoverflow.com/questions/1349404/generate-random-string-characters-in-javascript + const s = 'abcdef0123456789'; + return 'ObjectId(' + Array.apply(null, Array(24)).map(() => s.charAt(Math.floor(Math.random() * s.length))).join('') + ')'; } @Component({ - selector: 'app-input', - templateUrl: './input.component.html', - styleUrls: ['./input.component.scss'] + selector: 'app-input', + templateUrl: './input.component.html', + styleUrls: ['./input.component.scss'] }) export class InputComponent implements OnInit, OnChanges, AfterViewInit { - public readonly _types = inject(DbmsTypesService); - - constructor() { - this.randomId = Math.floor((Math.random() * 10e8)); - } - - @Input() header: UiColumnDefinition | FieldDefinition; - @Input() value: any; - @Input() showLabel? = false; - @Output() valueChange = new EventEmitter(); - @Output() enter = new EventEmitter(); - @ViewChild('inputElement', {static: false}) inputElement: ElementRef; - @ViewChild('flatpickr', {static: false}) flatpickrElement: ElementRef; - @ViewChild('fileInput', {static: false}) fileInput: ElementRef; - flatpickrObj; - inputFileName = 'Choose File'; - randomId; - - private static validateJSON(val) { - let doc = val.replace(/NumberDecimal\("[0-9.]*"\)/g, '0'); - doc = doc.replace(/[0-9a-zA-Z.]+[*\/+-]+[0-9a-zA-Z.]+/g, '0'); - if (doc === '') { - return new InputValidation(false, 'Non-valid document'); - } - try { - doc = JSON.parse(doc); - } catch (Exception) { - return new InputValidation(false, 'Non-valid document'); + public readonly _types = inject(DbmsTypesService); + + constructor() { + this.randomId = Math.floor((Math.random() * 10e8)); + } + + @Input() header: UiColumnDefinition | FieldDefinition; + @Input() value: any; + @Input() showLabel? = false; + @Output() valueChange = new EventEmitter(); + @Output() enter = new EventEmitter(); + @ViewChild('inputElement', {static: false}) inputElement: ElementRef; + @ViewChild('flatpickr', {static: false}) flatpickrElement: ElementRef; + @ViewChild('fileInput', {static: false}) fileInput: ElementRef; + flatpickrObj; + inputFileName = 'Choose File'; + randomId; + + private static validateJSON(val) { + let doc = val.replace(/NumberDecimal\("[0-9.]*"\)/g, '0'); + doc = doc.replace(/[0-9a-zA-Z.]+[*\/+-]+[0-9a-zA-Z.]+/g, '0'); + if (doc === '') { + return new InputValidation(false, 'Non-valid document'); + } + try { + doc = JSON.parse(doc); + } catch (Exception) { + return new InputValidation(false, 'Non-valid document'); + } + return new InputValidation(true); } - return new InputValidation(true); - } - - ngOnInit() { - - } - ngAfterViewInit() { - this.initFlatpickr(); - } + ngOnInit() { - ngOnChanges(changes: SimpleChanges) { - if (this.inputElement !== undefined && changes.value && changes.value.currentValue === null) { - $(this.inputElement.nativeElement).removeClass('is-valid').removeClass('is-invalid'); - } else if (this._types.isDateTime(this.header.dataType) && (changes.value.currentValue == null || changes.value.currentValue === '') && this.flatpickrObj) { - this.flatpickrObj.setDate(null); } - if (!changes.value.currentValue) { - this.inputFileName = 'Choose file'; - if (this.fileInput) { - //see https://stackoverflow.com/questions/49976714/how-to-upload-the-same-file-in-angular4 - this.fileInput.nativeElement.value = ''; - } - } - } - triggerNull(value) { - if (value !== null) { - return null; + ngAfterViewInit() { + this.initFlatpickr(); } - if ((this.header instanceof UiColumnDefinition) && this.header.collectionsType) { - return ''; - } else if (this._types.isNumeric(this.header.dataType)) { - return 0; - } else if (this._types.isBoolean(this.header.dataType)) { - return false; - } else if (this._types.isMultimedia(this.header.dataType)) { - return null; - } else { - return ''; + ngOnChanges(changes: SimpleChanges) { + if (this.inputElement !== undefined && changes.value && changes.value.currentValue === null) { + $(this.inputElement.nativeElement).removeClass('is-valid').removeClass('is-invalid'); + } else if (this._types.isDateTime(this.header.dataType) && (changes.value.currentValue == null || changes.value.currentValue === '') && this.flatpickrObj) { + this.flatpickrObj.setDate(null); + } + if (!changes.value.currentValue) { + this.inputFileName = 'Choose file'; + if (this.fileInput) { + //see https://stackoverflow.com/questions/49976714/how-to-upload-the-same-file-in-angular4 + this.fileInput.nativeElement.value = ''; + } + } } - } - onValueChange(newVal: any, event = null) { - this.valueChange.emit(newVal); - if (event !== null && event.keyCode === 13) { - this.enter.emit(true); - } - } + triggerNull(value) { + if (value !== null) { + return null; + } - validate(inputElement): InputValidation { - const val = inputElement.value; - if (!val) { - return; + if ((this.header instanceof UiColumnDefinition) && this.header.collectionsType) { + return ''; + } else if (this._types.isNumeric(this.header.dataType)) { + return 0; + } else if (this._types.isBoolean(this.header.dataType)) { + return false; + } else if (this._types.isMultimedia(this.header.dataType)) { + return null; + } else { + return ''; + } } - if (!(this.header instanceof UiColumnDefinition)) { - return null; + + onValueChange(newVal: any, event = null) { + this.valueChange.emit(newVal); + if (event !== null && event.keyCode === 13) { + this.enter.emit(true); + } } - if (this.header.collectionsType) { - return this.validateArray(val); - } else if (this._types.isNumeric(this.header.dataType)) { - if (isNaN(val)) { - return new InputValidation(false, 'Non-numeric input'); - } - } else if (this.header.dataType.toLowerCase() === 'json') { - return InputComponent.validateJSON(val); + validate(inputElement): InputValidation { + const val = inputElement.value; + if (!val) { + return; + } + if (!(this.header instanceof UiColumnDefinition)) { + return null; + } + if (this.header.collectionsType) { + return this.validateArray(val); + } else if (this._types.isNumeric(this.header.dataType)) { + if (isNaN(val)) { + return new InputValidation(false, 'Non-numeric input'); + } + } else if (this.header.dataType.toLowerCase() === 'json') { + return InputComponent.validateJSON(val); + + } } - } - - private validateArray(val) { - if (val.startsWith('[') && val.endsWith(']')) { - if (!this._types.isNumeric(this.header.dataType)) { - //don't make further checks on non-numeric arrays - return; - } - if (!(this.header instanceof UiColumnDefinition)) { - return null; - } - - try { - const parsed = JSON.parse(val); - if (!Array.isArray(parsed)) { - return new InputValidation(false, 'Non-valid array'); + + private validateArray(val) { + if (val.startsWith('[') && val.endsWith(']')) { + if (!this._types.isNumeric(this.header.dataType)) { + //don't make further checks on non-numeric arrays + return; + } + if (!(this.header instanceof UiColumnDefinition)) { + return null; + } + + try { + const parsed = JSON.parse(val); + if (!Array.isArray(parsed)) { + return new InputValidation(false, 'Non-valid array'); + } else { + if (this.header.cardinality && this.getMaxCardinality(parsed) > this.header.cardinality) { + return new InputValidation(false, 'Exceeded max cardinality of ' + this.header.cardinality); + } else if (this.header.dimension && this.getMaxDimension(parsed) > this.header.dimension) { + return new InputValidation(false, 'Exceeded max dimension of ' + this.header.dimension); + } + return new InputValidation(true); + } + } catch (e) { + return new InputValidation(false, 'Non-valid array'); + } } else { - if (this.header.cardinality && this.getMaxCardinality(parsed) > this.header.cardinality) { - return new InputValidation(false, 'Exceeded max cardinality of ' + this.header.cardinality); - } else if (this.header.dimension && this.getMaxDimension(parsed) > this.header.dimension) { - return new InputValidation(false, 'Exceeded max dimension of ' + this.header.dimension); - } - return new InputValidation(true); - } - } catch (e) { - return new InputValidation(false, 'Non-valid array'); - } - } else { - return new InputValidation(false, 'Non-valid array'); + return new InputValidation(false, 'Non-valid array'); + } } - } - - getMaxDimension(arr: number[]): number { - let maxDim = 1; - for (const ele of arr) { - if (Array.isArray(ele)) { - maxDim = Math.max(maxDim, this.getMaxDimension(ele) + 1); - } + + getMaxDimension(arr: number[]): number { + let maxDim = 1; + for (const ele of arr) { + if (Array.isArray(ele)) { + maxDim = Math.max(maxDim, this.getMaxDimension(ele) + 1); + } + } + return maxDim; } - return maxDim; - } - - getMaxCardinality(arr: number[]): number { - let maxCard = arr.length; - for (const ele of arr) { - if (Array.isArray(ele)) { - maxCard = Math.max(maxCard, this.getMaxCardinality(ele)); - } + + getMaxCardinality(arr: number[]): number { + let maxCard = arr.length; + for (const ele of arr) { + if (Array.isArray(ele)) { + maxCard = Math.max(maxCard, this.getMaxCardinality(ele)); + } + } + return maxCard; } - return maxCard; - } - initFlatpickr() { - // https://flatpickr.js.org/options/ - const self = this; + initFlatpickr() { + // https://flatpickr.js.org/options/ + const self = this; - function onChange(selectedDates, dateStr, instance) { - self.onValueChange(dateStr); - } + function onChange(selectedDates, dateStr, instance) { + self.onValueChange(dateStr); + } - switch (this.header.dataType.toLowerCase()) { - case 'date': - this.flatpickrObj = flatpickr(this.flatpickrElement.nativeElement, { - dateFormat: 'Y-m-d', - onChange: onChange - }); - break; - case 'time': - this.flatpickrObj = flatpickr(this.flatpickrElement.nativeElement, { - enableTime: true, - noCalendar: true, - dateFormat: 'H:i:S', - time_24hr: true, - onChange: onChange - }); - break; - case 'timestamp': - this.flatpickrObj = flatpickr(this.flatpickrElement.nativeElement, { - enableTime: true, - dateFormat: 'Y-m-d H:i:S', - time_24hr: true, - onChange: onChange - }); - break; - } - } - - clearFlatpickr() { - this.valueChange.emit(null); - } - - onFileChange(files, event = null) { - if (files === null) { - this.valueChange.emit(null); - if (event) { - event.stopPropagation(); - } - return; + switch (this.header.dataType.toLowerCase()) { + case 'date': + this.flatpickrObj = flatpickr(this.flatpickrElement.nativeElement, { + dateFormat: 'Y-m-d', + onChange: onChange + }); + break; + case 'time': + this.flatpickrObj = flatpickr(this.flatpickrElement.nativeElement, { + enableTime: true, + noCalendar: true, + dateFormat: 'H:i:S', + time_24hr: true, + onChange: onChange + }); + break; + case 'timestamp': + this.flatpickrObj = flatpickr(this.flatpickrElement.nativeElement, { + enableTime: true, + dateFormat: 'Y-m-d H:i:S', + time_24hr: true, + onChange: onChange + }); + break; + } } - let file; - if (files.length > 0) { - file = files[0]; - } else { - file = undefined; + + clearFlatpickr() { + this.valueChange.emit(null); } - if (file) { - this.inputFileName = file.name; - this.value = file; - } else { - this.inputFileName = 'Choose file'; - this.value = undefined; + + onFileChange(files, event = null) { + if (files === null) { + this.valueChange.emit(null); + if (event) { + event.stopPropagation(); + } + return; + } + let file; + if (files.length > 0) { + file = files[0]; + } else { + file = undefined; + } + if (file) { + this.inputFileName = file.name; + this.value = file; + } else { + this.inputFileName = 'Choose file'; + this.value = undefined; + } + this.valueChange.emit(file); } - this.valueChange.emit(file); - } } diff --git a/src/app/components/data-view/json-text/json-text.component.ts b/src/app/components/data-view/json-text/json-text.component.ts index 159c0b77..6c918437 100644 --- a/src/app/components/data-view/json-text/json-text.component.ts +++ b/src/app/components/data-view/json-text/json-text.component.ts @@ -1,31 +1,31 @@ import {Component, Input, OnInit} from '@angular/core'; @Component({ - selector: 'app-json-text', - templateUrl: './json-text.component.html', - styleUrls: ['./json-text.component.scss'] + selector: 'app-json-text', + templateUrl: './json-text.component.html', + styleUrls: ['./json-text.component.scss'] }) export class JsonTextComponent implements OnInit { - @Input() text?: string; - json: {}; + @Input() text?: string; + json: {}; - constructor() { - } + constructor() { + } - ngOnInit(): void { - const regex = new RegExp('/ObjectId(\d{1,24})/g'); - if (regex.test(this.text)) { - return; + ngOnInit(): void { + const regex = new RegExp('/ObjectId(\d{1,24})/g'); + if (regex.test(this.text)) { + return; + } + this.json = this.parse(this.text); } - this.json = this.parse(this.text); - } - parse(text: string): {} { - try { - return JSON.parse(text); - } catch (e) { - return text; + parse(text: string): {} { + try { + return JSON.parse(text); + } catch (e) { + return text; + } } - } } diff --git a/src/app/components/data-view/media/media.component.ts b/src/app/components/data-view/media/media.component.ts index 84c6f548..cec4f026 100644 --- a/src/app/components/data-view/media/media.component.ts +++ b/src/app/components/data-view/media/media.component.ts @@ -2,43 +2,43 @@ import {AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild} from '@a import * as Plyr from 'plyr'; @Component({ - selector: 'app-media', - templateUrl: './media.component.html', - styleUrls: ['./media.component.scss'] + selector: 'app-media', + templateUrl: './media.component.html', + styleUrls: ['./media.component.scss'] }) export class MediaComponent implements OnInit, AfterViewInit { - @Input() src: string; - @Input() type: string; - @Input() style?: any; - @ViewChild('video') video: ElementRef; - @ViewChild('audio') audio: ElementRef; + @Input() src: string; + @Input() type: string; + @Input() style?: any; + @ViewChild('video') video: ElementRef; + @ViewChild('audio') audio: ElementRef; - plyr: Plyr; + plyr: Plyr; - constructor() { - } - - ngOnInit(): void { - } + constructor() { + } - ngAfterViewInit() { - const options = { - controls: ['play-large', 'play', 'progress', 'current-time', 'pip', 'fullscreen'] - }; - if (this.video) { - this.plyr = new Plyr(this.video.nativeElement, options); + ngOnInit(): void { } - if (this.audio) { - this.plyr = new Plyr(this.audio.nativeElement, options); + + ngAfterViewInit() { + const options = { + controls: ['play-large', 'play', 'progress', 'current-time', 'pip', 'fullscreen'] + }; + if (this.video) { + this.plyr = new Plyr(this.video.nativeElement, options); + } + if (this.audio) { + this.plyr = new Plyr(this.audio.nativeElement, options); + } } - } - getStyle() { - if (this.plyr && this.plyr.fullscreen.active) { - return; + getStyle() { + if (this.plyr && this.plyr.fullscreen.active) { + return; + } + return this.style ? this.style : {}; } - return this.style ? this.style : {}; - } } diff --git a/src/app/components/data-view/models/pagination-element.model.ts b/src/app/components/data-view/models/pagination-element.model.ts index f206558c..b304db5b 100644 --- a/src/app/components/data-view/models/pagination-element.model.ts +++ b/src/app/components/data-view/models/pagination-element.model.ts @@ -2,31 +2,31 @@ * model for the pagination in the data-table component */ export class PaginationElement { - page: number; - label: string; - active = false; - disabled = false; - routerLink: string; + page: number; + label: string; + active = false; + disabled = false; + routerLink: string; - withPage(tableId: number, page: number) { - this.page = page; - this.label = page.toString(); - this.routerLink = '/views/data-table/' + tableId + '/' + page; - return this; - } + withPage(tableId: number, page: number) { + this.page = page; + this.label = page.toString(); + this.routerLink = '/views/data-table/' + tableId + '/' + page; + return this; + } - withLabel(label: string) { - this.label = label; - return this; - } + withLabel(label: string) { + this.label = label; + return this; + } - setActive() { - this.active = true; - return this; - } + setActive() { + this.active = true; + return this; + } - setDisabled() { - this.disabled = true; - return this; - } + setDisabled() { + this.disabled = true; + return this; + } } diff --git a/src/app/components/data-view/models/result-set.model.ts b/src/app/components/data-view/models/result-set.model.ts index 5fd5f004..ca1bbe0f 100644 --- a/src/app/components/data-view/models/result-set.model.ts +++ b/src/app/components/data-view/models/result-set.model.ts @@ -8,48 +8,48 @@ import {Pair} from '../../json/json-editor.component'; */ export interface FieldDefinition { - name: string; - dataType: string; - nullable: boolean; - defaultValue: string; + name: string; + dataType: string; + nullable: boolean; + defaultValue: string; } export class Result { - dataModel: DataModel; - namespace: string; - query: string; - data: D[]; - header: H[]; - exception: ResultException; - error: string; - language: QueryLanguage; - hasMore: boolean; - currentPage: number; - highestPage: number; - affectedTuples: number; + dataModel: DataModel; + namespace: string; + query: string; + data: D[]; + header: H[]; + exception: ResultException; + error: string; + language: QueryLanguage; + hasMore: boolean; + currentPage: number; + highestPage: number; + affectedTuples: number; } export class RelationalResult extends Result { - table: string; - tableId: number; - tables: string[]; - error: string; - type: EntityType;//"table" or "view" + table: string; + tableId: number; + tables: string[]; + error: string; + type: EntityType;//"table" or "view" - constructor(error: string, affectedRows = 0) { - super(); - this.error = error; - this.affectedTuples = affectedRows; - } + constructor(error: string, affectedRows = 0) { + super(); + this.error = error; + this.affectedTuples = affectedRows; + } } export class RelationalExploreResult extends RelationalResult { - explorerId: number; - classificationInfo: string; - includesClassificationInfo: boolean; - classifiedData: string[][]; - isConvertedToSql: boolean; + explorerId: number; + classificationInfo: string; + includesClassificationInfo: boolean; + classifiedData: string[][]; + isConvertedToSql: boolean; } export class GraphResult extends Result { @@ -61,51 +61,51 @@ export class DocumentResult extends Result { } export enum QueryLanguage { - MQL = 'mql', - MONGO = 'mongo', - SQL = 'sql', - CYPHER = 'cypher', - CQL = 'cql' + MQL = 'mql', + MONGO = 'mongo', + SQL = 'sql', + CYPHER = 'cypher', + CQL = 'cql' } export class InfoSet extends RelationalResult { - constructor(error: string, generatedQuery: any, affectedRows: number) { - super(error, affectedRows); - } + constructor(error: string, generatedQuery: any, affectedRows: number) { + super(error, affectedRows); + } } /** * model with classified data coming form server */ export class ExploreSet { - header: UiColumnDefinition[]; - dataAfterClassification: string[]; - exploreManagerId: number; - graph: string; + header: UiColumnDefinition[]; + dataAfterClassification: string[]; + exploreManagerId: number; + graph: string; } export class ExplorColSet { - [column: string]: {} + [column: string]: {} } export class SelectedColSet { - [column: string]: { - selected: string - } + [column: string]: { + selected: string + } } export class DashboardSet { - availableAdapter: { string: Pair }; - availableNamespaces: {}; - catalogPersistent: boolean; - numberOfCommits: number; - numberOfRollbacks: number; - numberOfQueries: number; - numberOfWorkloads: number; + availableAdapter: { string: Pair }; + availableNamespaces: {}; + catalogPersistent: boolean; + numberOfCommits: number; + numberOfRollbacks: number; + numberOfQueries: number; + numberOfWorkloads: number; } @@ -113,313 +113,313 @@ export class DashboardSet { * model for statistics coming from the server */ export class StatisticSet { - // error: string; - [column: string]: { - type: string[], - min: null, - max: null, - check: string[], - sort: string - } + // error: string; + [column: string]: { + type: string[], + min: null, + max: null, + check: string[], + sort: string + } - constructor() { - // this.error = error; - } + constructor() { + // this.error = error; + } } export class StatisticTableSet { - table: null; - calls: TableCallSet; - numberOfRows: null; - alphabeticColumn: StatisticColumnSet; - numericalColumn: StatisticColumnSet; - temporalColumn: StatisticColumnSet; - tableType: string; + table: null; + calls: TableCallSet; + numberOfRows: null; + alphabeticColumn: StatisticColumnSet; + numericalColumn: StatisticColumnSet; + temporalColumn: StatisticColumnSet; + tableType: string; - constructor() { - } + constructor() { + } } export class TableCallSet { - numberOfSelects: number; - numberOfInserts: number; - numberOfDeletes: number; - numberOfUpdates: number; + numberOfSelects: number; + numberOfInserts: number; + numberOfDeletes: number; + numberOfUpdates: number; - constructor() { - } + constructor() { + } } export class StatisticColumnSet { - [column: string]: { - column: null, - min: null, - max: null, - uniqueValues: null, - full: null, - } + [column: string]: { + column: null, + min: null, + max: null, + uniqueValues: null, + full: null, + } - constructor() { - } + constructor() { + } } /** * model for filtered options coming from user input */ export class FilteredUserInput { - [column: string]: {} + [column: string]: {} } export class DashboardData { - column: {}; + column: {}; - constructor() { - } + constructor() { + } } /** * Model for a column of a table */ export class UiColumnDefinition implements FieldDefinition { - //for both - name: string; - id: number; - - //for the data-table - sort: SortState; - dataType: string; - collectionsType: string; - filter: string; - - //for editing columns - primary: boolean; - unique: boolean; - nullable: boolean; - precision: number; - scale: number; - defaultValue: any; - dimension: number; - cardinality: number; - - //for data sources - as: string; - - constructor( - id: number, - name: string, - primary: boolean = null, - nullable: boolean = null, - type: string = null, - collectionsType: string = null, - precision: number = null, - scale: number, - defaultValue: string = null, - dimension: number = -1, - cardinality: number = -1, - as = null) { - this.id = id; - this.name = name; - this.primary = primary; - this.nullable = nullable; - this.dataType = type; - this.collectionsType = collectionsType; - this.precision = precision; - this.scale = scale; - this.defaultValue = defaultValue; - this.dimension = dimension; - this.cardinality = cardinality; - this.as = as; - } - - static fromModel(column: ColumnModel, primaries: number[]) { - return new UiColumnDefinition( - column.id, - column.name, - primaries.includes(column.id), - column.nullable, - column.type.name, - column.collectionsType == null ? null : column.collectionsType.name, - column.precision, - column.scale, - column.defaultValue, - column.dimension, - column.cardinality); - } + //for both + name: string; + id: number; + + //for the data-table + sort: SortState; + dataType: string; + collectionsType: string; + filter: string; + + //for editing columns + primary: boolean; + unique: boolean; + nullable: boolean; + precision: number; + scale: number; + defaultValue: any; + dimension: number; + cardinality: number; + + //for data sources + as: string; + + constructor( + id: number, + name: string, + primary: boolean = null, + nullable: boolean = null, + type: string = null, + collectionsType: string = null, + precision: number = null, + scale: number, + defaultValue: string = null, + dimension: number = -1, + cardinality: number = -1, + as = null) { + this.id = id; + this.name = name; + this.primary = primary; + this.nullable = nullable; + this.dataType = type; + this.collectionsType = collectionsType; + this.precision = precision; + this.scale = scale; + this.defaultValue = defaultValue; + this.dimension = dimension; + this.cardinality = cardinality; + this.as = as; + } + + static fromModel(column: ColumnModel, primaries: number[]) { + return new UiColumnDefinition( + column.id, + column.name, + primaries.includes(column.id), + column.nullable, + column.type.name, + column.collectionsType == null ? null : column.collectionsType.name, + column.precision, + column.scale, + column.defaultValue, + column.dimension, + column.cardinality); + } } export interface PolyType { - name: string; - signatures: number; + name: string; + signatures: number; } /** * model for constraints of a table */ export class TableConstraint { - id: number; - name: string; - type: string; - deferrable: boolean; - initially_deferred: boolean; - columns: string[] = []; - - constructor(id: number, name: string = null, type: string = null, deferrable: boolean = null, initially_deferred: boolean = null) { - this.id = id; - this.name = name; - this.type = type; - this.deferrable = deferrable; - this.initially_deferred = initially_deferred; - } - - /** - * add a column to a constraint (that possibly consists of multiple columns) - */ - addColumn(col: string) { - this.columns.push(col); - } + id: number; + name: string; + type: string; + deferrable: boolean; + initially_deferred: boolean; + columns: string[] = []; + + constructor(id: number, name: string = null, type: string = null, deferrable: boolean = null, initially_deferred: boolean = null) { + this.id = id; + this.name = name; + this.type = type; + this.deferrable = deferrable; + this.initially_deferred = initially_deferred; + } + + /** + * add a column to a constraint (that possibly consists of multiple columns) + */ + addColumn(col: string) { + this.columns.push(col); + } } /** * SQL Index of a table */ export class IndexModel { - constructor( - private namespaceId: number, - private entityId: number, - private name: string, - private storeUniqueName: string, - private method: string, - private columnIds: number[] - ) { - } + constructor( + private namespaceId: number, + private entityId: number, + private name: string, + private storeUniqueName: string, + private method: string, + private columnIds: number[] + ) { + } } export class EntityMeta { - constructor( - namespaceId: number, - entityId: number, - entityName: string, - fields: number[] = [] - ) { - } + constructor( + namespaceId: number, + entityId: number, + entityName: string, + fields: number[] = [] + ) { + } } export class PlacementMeta extends EntityMeta { - constructor( - namespaceId: number, - entityId: number, - entityName: string, - private adapterId: number, - private method: string, - fields: number[] - ) { - super(namespaceId, entityId, entityName, fields); - } + constructor( + namespaceId: number, + entityId: number, + entityName: string, + private adapterId: number, + private method: string, + fields: number[] + ) { + super(namespaceId, entityId, entityName, fields); + } } export class IndexMethodModel { - name: string; - displayName: string; + name: string; + displayName: string; - constructor(name: string, displayName: string) { - this.name = name; - this.displayName = displayName; - } + constructor(name: string, displayName: string) { + this.name = name; + this.displayName = displayName; + } } /** * Status of an import or export operation */ export interface Status { - context: string; - totalRows: number; - currentRow: number; - status: number; + context: string; + totalRows: number; + currentRow: number; + status: number; } /** * Exception in a result message */ export interface ResultException { - detailMessage: string; - message: string; - stackTrace: StackTrace[]; - cause: ResultException; + detailMessage: string; + message: string; + stackTrace: StackTrace[]; + cause: ResultException; } /** * Stacktrace in a ResultException */ export interface StackTrace { - declaringClass: string; - methodName: string; - fileName: string; - lineNumber: number; + declaringClass: string; + methodName: string; + fileName: string; + lineNumber: number; } export class PathAccessRequest { - constructor( - public name: string, - public directoryName: string, - ) { - } + constructor( + public name: string, + public directoryName: string, + ) { + } } export class PartitioningRequest { - constructor( - public schemaName: string = '', - public tableName: string = '', - public method: string = 'NONE',//enum in Java - public numPartitions: number = 2, - public column = '' - ) { - } + constructor( + public schemaName: string = '', + public tableName: string = '', + public method: string = 'NONE',//enum in Java + public numPartitions: number = 2, + public column = '' + ) { + } } export class PartitionFunctionModel { - title: string; - description: string; - columnNames: string[]; - rows: PartitionFunctionColumn[][]; - error: string; - generatedQuery: string; + title: string; + description: string; + columnNames: string[]; + rows: PartitionFunctionColumn[][]; + error: string; + generatedQuery: string; } export class PartitionFunctionColumn { - type: FieldType; - mandatory: boolean; - modifiable: boolean; - value: string; - options: string[]; + type: FieldType; + mandatory: boolean; + modifiable: boolean; + value: string; + options: string[]; } /** * What kind of type to render in the UI, e.g. a number input, a select menu etc. */ export enum FieldType { - STRING = 'STRING', - INTEGER = 'INTEGER', - LIST = 'LIST', - LABEL = 'LABEL' + STRING = 'STRING', + INTEGER = 'INTEGER', + LIST = 'LIST', + LABEL = 'LABEL' } export class ModifyPartitionRequest { - constructor( - public schemaName: string, - public tableName: string, - public partitions: string[], - public storeUniqueName: string - ) { - } + constructor( + public schemaName: string, + public tableName: string, + public partitions: string[], + public storeUniqueName: string + ) { + } } /** * How a ResultSet should be displayed */ export enum DataPresentationType { - TABLE = 'TABLE', - CARD = 'CARD', - GRAPH = 'GRAPH' + TABLE = 'TABLE', + CARD = 'CARD', + GRAPH = 'GRAPH' } diff --git a/src/app/components/data-view/models/sort-state.model.ts b/src/app/components/data-view/models/sort-state.model.ts index 808ce6e1..b8a79e6d 100644 --- a/src/app/components/data-view/models/sort-state.model.ts +++ b/src/app/components/data-view/models/sort-state.model.ts @@ -2,14 +2,14 @@ * models if and how a column is supposed to be sorted */ export class SortState { - direction: SortDirection = SortDirection.DESC; - sorting = false; - //for the PlanBuilder - column: string; + direction: SortDirection = SortDirection.DESC; + sorting = false; + //for the PlanBuilder + column: string; - constructor() { - this.column = ''; - } + constructor() { + this.column = ''; + } } /** @@ -17,28 +17,28 @@ export class SortState { */ export enum SortDirection { - /** - * ascending - */ - ASC = 'ASC', + /** + * ascending + */ + ASC = 'ASC', - /** - * descending - */ - DESC = 'DESC' + /** + * descending + */ + DESC = 'DESC' } export class InputValidation { - cssClass; + cssClass; - constructor( - public valid: boolean, - public message: string = null, - ) { - if (valid) { - this.cssClass = 'is-valid'; - } else { - this.cssClass = 'is-invalid'; + constructor( + public valid: boolean, + public message: string = null, + ) { + if (valid) { + this.cssClass = 'is-valid'; + } else { + this.cssClass = 'is-invalid'; + } } - } } diff --git a/src/app/components/data-view/multiple-switch.pipe.ts b/src/app/components/data-view/multiple-switch.pipe.ts index fdc92a0b..a03e094b 100644 --- a/src/app/components/data-view/multiple-switch.pipe.ts +++ b/src/app/components/data-view/multiple-switch.pipe.ts @@ -1,10 +1,10 @@ import {Pipe, PipeTransform} from '@angular/core'; @Pipe({ - name: 'multipleSwitch', + name: 'multipleSwitch', }) export class MultipleSwitchPipe implements PipeTransform { - transform(cases: T[], value: T): T { - return cases.includes(value) ? value : cases[0]; - } + transform(cases: T[], value: T): T { + return cases.includes(value) ? value : cases[0]; + } } diff --git a/src/app/components/data-view/shared-module.ts b/src/app/components/data-view/shared-module.ts index fda438aa..20d38f19 100644 --- a/src/app/components/data-view/shared-module.ts +++ b/src/app/components/data-view/shared-module.ts @@ -2,13 +2,13 @@ import {MultipleSwitchPipe} from './multiple-switch.pipe'; import {NgModule} from '@angular/core'; @NgModule({ - imports: [], - declarations: [ - MultipleSwitchPipe - ], - exports: [ - MultipleSwitchPipe - ] + imports: [], + declarations: [ + MultipleSwitchPipe + ], + exports: [ + MultipleSwitchPipe + ] }) export class DatesPipeModule { } diff --git a/src/app/components/data-view/view/view.component.ts b/src/app/components/data-view/view/view.component.ts index 07016353..19e0b4c9 100644 --- a/src/app/components/data-view/view/view.component.ts +++ b/src/app/components/data-view/view/view.component.ts @@ -1,4 +1,14 @@ -import {Component, computed, EventEmitter, inject, Output, Signal, signal, ViewEncapsulation, WritableSignal} from '@angular/core'; +import { + Component, + computed, + EventEmitter, + inject, + Output, + Signal, + signal, + ViewEncapsulation, + WritableSignal +} from '@angular/core'; import {Freshness, TimeUnits, ViewType} from '../data-view.model'; import {AdapterModel} from '../../../views/adapters/adapter.model'; import {CatalogService} from '../../../services/catalog.service'; @@ -8,182 +18,182 @@ import {DataModel} from '../../../models/ui-request.model'; import {Result} from '../models/result-set.model'; @Component({ - selector: 'poly-create-view', - templateUrl: './view.component.html', - styleUrls: ['./view.component.scss'], - encapsulation: ViewEncapsulation.None, // new elements in sortable should have margin as well + selector: 'poly-create-view', + templateUrl: './view.component.html', + styleUrls: ['./view.component.scss'], + encapsulation: ViewEncapsulation.None, // new elements in sortable should have margin as well }) export class ViewComponent { - @Output() - viewQueryConsumer = new EventEmitter(); + @Output() + viewQueryConsumer = new EventEmitter(); - private readonly _catalog = inject(CatalogService); - private readonly _toast = inject(ToasterService); + private readonly _catalog = inject(CatalogService); + private readonly _toast = inject(ToasterService); - public readonly $query: WritableSignal = signal(''); - public readonly $showView: WritableSignal = signal(false); - public readonly $result: WritableSignal> = signal(null) - public readonly $viewName: WritableSignal = signal(''); - public readonly $type: WritableSignal = signal(ViewType.VIEW); - public readonly $store: WritableSignal = signal(null); + public readonly $query: WritableSignal = signal(''); + public readonly $showView: WritableSignal = signal(false); + public readonly $result: WritableSignal> = signal(null); + public readonly $viewName: WritableSignal = signal(''); + public readonly $type: WritableSignal = signal(ViewType.VIEW); + public readonly $store: WritableSignal = signal(null); - public readonly $updates: WritableSignal = signal(1); + public readonly $updates: WritableSignal = signal(1); - public readonly $freshness: WritableSignal = signal(Freshness.UPDATE); - public readonly $unit: WritableSignal = signal(TimeUnits.MILLISECONDS); - public readonly $time: WritableSignal = signal(1000); + public readonly $freshness: WritableSignal = signal(Freshness.UPDATE); + public readonly $unit: WritableSignal = signal(TimeUnits.MILLISECONDS); + public readonly $time: WritableSignal = signal(1000); - public readonly $stores: Signal; - public readonly $freshnessMerged: Signal; + public readonly $stores: Signal; + public readonly $freshnessMerged: Signal; + protected readonly ViewType = ViewType; + protected readonly Freshness = Freshness; + protected readonly TimeUnits = TimeUnits; - constructor() { - this.$stores = computed(() => { - const listener = this._catalog.listener(); - return this._catalog.getStores(); - }) - this.$freshnessMerged = computed(() => { - if (this.$freshness() === Freshness.INTERVAL) { - return this.$time() + ' ' + this.$unit(); - } - return '' + this.$updates(); - }) - } + constructor() { + this.$stores = computed(() => { + const listener = this._catalog.listener(); + return this._catalog.getStores(); + }); + this.$freshnessMerged = computed(() => { + if (this.$freshness() === Freshness.INTERVAL) { + return this.$time() + ' ' + this.$unit(); + } + return '' + this.$updates(); + }); + } - openCreateView(result: Result) { - this.$result.set(result); - this.$showView.set(true); - this.$query.set(result.query); - } - createViewCode(doExecute: boolean) { - if (this.checkIfPossible()) { - const info = new ViewInformation(this.$type(), this.$viewName()); - info.initialQuery = this.$query(); - let fullQuery = this.getViewQuery(); - info.tableType = 'VIEW'; + openCreateView(result: Result) { + this.$result.set(result); + this.$showView.set(true); + this.$query.set(result.query); + } - if (this.$type() === ViewType.MATERIALIZED) { - if (this.$result().dataModel === DataModel.DOCUMENT) { - this._toast.error('Materialized views are not yet supported for document queries.'); - return; - } + createViewCode(doExecute: boolean) { + if (this.checkIfPossible()) { + const info = new ViewInformation(this.$type(), this.$viewName()); + info.initialQuery = this.$query(); + let fullQuery = this.getViewQuery(); + info.tableType = 'VIEW'; - fullQuery = this.getMaterializedViewQuery(info); - info.tableType = 'MATERIALIZED'; - } + if (this.$type() === ViewType.MATERIALIZED) { + if (this.$result().dataModel === DataModel.DOCUMENT) { + this._toast.error('Materialized views are not yet supported for document queries.'); + return; + } - info.fullQuery = fullQuery; + fullQuery = this.getMaterializedViewQuery(info); + info.tableType = 'MATERIALIZED'; + } - this.viewQueryConsumer.emit(info); + info.fullQuery = fullQuery; - this.$showView.set(false); - } - } - - private getViewQuery() { - if (this.$result().dataModel === DataModel.DOCUMENT) { - let source; - let pipeline; - - const temp = this.$query().trim().split('.'); - if (temp[0] === 'db') { - temp.shift(); // remove db - } - source = temp[0]; - if (temp[0].includes('getCollection(')) { - source = source - .replace('getCollection(', '') - .replace(')', ''); - } - temp.shift(); // remove collection - temp[0] = temp[0].replace('aggregate(', '').replace('find(', ''); - temp[temp.length - 1] = temp[temp.length - 1].slice(0, -1); // remove last bracket - - if (this.$query().includes('.aggregate(')) { - - pipeline = temp.join('.'); - } else if (this.$query().includes('.find(')) { - const json = JSON.parse('[' + temp.join('.') + ']'); - - pipeline = '['; - if (json.length > 0) { - pipeline += `{ "$match": ${JSON.stringify(json[0])} }`; - } - if (json.length > 1) { - pipeline += `, { "$project": ${JSON.stringify(json[1])} }`; + this.viewQueryConsumer.emit(info); + + this.$showView.set(false); } - pipeline += ']'; - } else { - this._toast.error('This query cannot be used to create a view.'); - return; - } - - return `db.createView(\n\t"${this.$viewName()}",\n\t"${source.replace('"', '')}",\n\t${pipeline}\n)`; - } else { - if (this.$query().startsWith('\n')) { - this.$query.set(this.$query().replace('\n', '')); - } - return `CREATE VIEW ${this.$viewName()} AS\n${this.$query()} `; } - } - private getMaterializedViewQuery(info: ViewInformation) { - if (this.$query().startsWith('\n')) { - this.$query.set(this.$query().replace('\n', '')); + private getViewQuery() { + if (this.$result().dataModel === DataModel.DOCUMENT) { + let source; + let pipeline; + + const temp = this.$query().trim().split('.'); + if (temp[0] === 'db') { + temp.shift(); // remove db + } + source = temp[0]; + if (temp[0].includes('getCollection(')) { + source = source + .replace('getCollection(', '') + .replace(')', ''); + } + temp.shift(); // remove collection + temp[0] = temp[0].replace('aggregate(', '').replace('find(', ''); + temp[temp.length - 1] = temp[temp.length - 1].slice(0, -1); // remove last bracket + + if (this.$query().includes('.aggregate(')) { + + pipeline = temp.join('.'); + } else if (this.$query().includes('.find(')) { + const json = JSON.parse('[' + temp.join('.') + ']'); + + pipeline = '['; + if (json.length > 0) { + pipeline += `{ "$match": ${JSON.stringify(json[0])} }`; + } + if (json.length > 1) { + pipeline += `, { "$project": ${JSON.stringify(json[1])} }`; + } + pipeline += ']'; + } else { + this._toast.error('This query cannot be used to create a view.'); + return; + } + + return `db.createView(\n\t"${this.$viewName()}",\n\t"${source.replace('"', '')}",\n\t${pipeline}\n)`; + } else { + if (this.$query().startsWith('\n')) { + this.$query.set(this.$query().replace('\n', '')); + } + return `CREATE VIEW ${this.$viewName()} AS\n${this.$query()} `; + } } - let query = `CREATE MATERIALIZED VIEW ${this.$viewName()} AS\n${this.$query()}\nON STORE ${this.$stores}\nFRESHNESS ${this.$freshness()}`; + + private getMaterializedViewQuery(info: ViewInformation) { + if (this.$query().startsWith('\n')) { + this.$query.set(this.$query().replace('\n', '')); + } + let query = `CREATE MATERIALIZED VIEW ${this.$viewName()} AS\n${this.$query()}\nON STORE ${this.$stores}\nFRESHNESS ${this.$freshness()}`; - info.stores = this.$store().name; - info.freshness = this.$freshnessMerged(); + info.stores = this.$store().name; + info.freshness = this.$freshnessMerged(); - if (this.$freshness() === Freshness.UPDATE) { - query += ` ${this.$updates()}`; - info.interval = this.$updates(); + if (this.$freshness() === Freshness.UPDATE) { + query += ` ${this.$updates()}`; + info.interval = this.$updates(); - } else if (this.$freshness() === Freshness.INTERVAL) { - query += ` ${this.$time()} ${this.$unit()}`; - info.interval = this.$time(); - info.timeUnit = this.$unit(); + } else if (this.$freshness() === Freshness.INTERVAL) { + query += ` ${this.$time()} ${this.$unit()}`; + info.interval = this.$time(); + info.timeUnit = this.$unit(); + } + return query; } - return query; - } - checkIfPossible() { - if (this.$viewName().trim() === '') { - this._toast.warn('Please provide a name for the new view. The new view was not created.', 'missing view name', ToastDuration.INFINITE); - return false; - } + checkIfPossible() { + if (this.$viewName().trim() === '') { + this._toast.warn('Please provide a name for the new view. The new view was not created.', 'missing view name', ToastDuration.INFINITE); + return false; + } - if (!this.$viewName().match('[a-zA-Z][a-zA-Z0-9-_]*')) { - this._toast.warn('Please provide a valid name for the new view. The new view was not created.', 'invalid view name', ToastDuration.INFINITE); - return false; - } - const entity = this._catalog.getEntityFromName('public', this.$viewName()); - if (entity) { - this._toast.warn('A table or view with this name already exists. Please choose another name.', 'invalid table name', ToastDuration.INFINITE); - return false; + if (!this.$viewName().match('[a-zA-Z][a-zA-Z0-9-_]*')) { + this._toast.warn('Please provide a valid name for the new view. The new view was not created.', 'invalid view name', ToastDuration.INFINITE); + return false; + } + const entity = this._catalog.getEntityFromName('public', this.$viewName()); + if (entity) { + this._toast.warn('A table or view with this name already exists. Please choose another name.', 'invalid table name', ToastDuration.INFINITE); + return false; + } + return true; } - return true; - } - - getStore(value: string) { - return this.$stores().filter(s => s.name === value)[0]; - } - handleModalChange($event: boolean) { + getStore(value: string) { + return this.$stores().filter(s => s.name === value)[0]; + } - } + handleModalChange($event: boolean) { - protected readonly ViewType = ViewType; - protected readonly Freshness = Freshness; - protected readonly TimeUnits = TimeUnits; + } } diff --git a/src/app/components/delete-confirm/delete-confirm.component.ts b/src/app/components/delete-confirm/delete-confirm.component.ts index 6cd5f1a7..5b81013e 100644 --- a/src/app/components/delete-confirm/delete-confirm.component.ts +++ b/src/app/components/delete-confirm/delete-confirm.component.ts @@ -1,27 +1,27 @@ import {Component, EventEmitter, OnInit, Output} from '@angular/core'; @Component({ - selector: 'app-delete-confirm', - templateUrl: './delete-confirm.component.html', - styleUrls: ['./delete-confirm.component.scss'] + selector: 'app-delete-confirm', + templateUrl: './delete-confirm.component.html', + styleUrls: ['./delete-confirm.component.scss'] }) export class DeleteConfirmComponent implements OnInit { - @Output() delete: EventEmitter = new EventEmitter(); - confirm = false; + @Output() delete: EventEmitter = new EventEmitter(); + confirm = false; - constructor() { - } + constructor() { + } - ngOnInit(): void { - } + ngOnInit(): void { + } - onClick() { - if (!this.confirm) { - this.confirm = true; - } else { - this.delete.emit(); + onClick() { + if (!this.confirm) { + this.confirm = true; + } else { + this.delete.emit(); + } } - } } diff --git a/src/app/components/docker/dockeredit/dockeredit.component.scss b/src/app/components/docker/dockeredit/dockeredit.component.scss index df0eb916..529ef641 100644 --- a/src/app/components/docker/dockeredit/dockeredit.component.scss +++ b/src/app/components/docker/dockeredit/dockeredit.component.scss @@ -1,8 +1,8 @@ // From Bootstrap 5.3, remove once Polypheny-UI bootstrap is upgraded .our-bg-success-subtle { - background-color: #d1e7dd !important; + background-color: #d1e7dd !important; } .our-bg-danger-subtle { - background-color: #f8d7da !important; + background-color: #f8d7da !important; } diff --git a/src/app/components/docker/dockeredit/dockeredit.component.ts b/src/app/components/docker/dockeredit/dockeredit.component.ts index 9bc9ecf3..c2fc791f 100644 --- a/src/app/components/docker/dockeredit/dockeredit.component.ts +++ b/src/app/components/docker/dockeredit/dockeredit.component.ts @@ -1,205 +1,213 @@ import {Component, EventEmitter, inject, Input, OnDestroy, OnInit, Output} from '@angular/core'; import {CrudService} from '../../../services/crud.service'; -import {DockerInstance, DockerReconnectResponse, DockerRemoveResponse, DockerStatus, DockerUpdateResponse, Handshake, HandshakeAndInstance} from '../../../models/docker.model'; +import { + DockerInstance, + DockerReconnectResponse, + DockerRemoveResponse, + DockerStatus, + DockerUpdateResponse, + Handshake, + HandshakeAndInstance +} from '../../../models/docker.model'; import {ToasterService} from '../../toast-exposer/toaster.service'; @Component({ - selector: 'app-dockeredit', - templateUrl: './dockeredit.component.html', - styleUrls: ['./dockeredit.component.scss'] + selector: 'app-dockeredit', + templateUrl: './dockeredit.component.html', + styleUrls: ['./dockeredit.component.scss'] }) export class DockereditComponent implements OnInit, OnDestroy { - private readonly _crud = inject(CrudService); - private readonly _toast = inject(ToasterService); - - host: string; - alias: string; - registry: string; - communicationPort: number; - handshakePort: number; - proxyPort: number; - handshake: Handshake = null; - timeoutId: number = null; - lock = false; - connected: boolean | string = 'checking...'; - updateLock = false; - modified = false; - error: string; - - @Input() id: number; - @Output() done = new EventEmitter(); - - constructor() { - } - - ngOnInit(): void { - this._crud.getDockerInstance(this.id).subscribe({ - next: res => { - this.updateValues(res); - } - , - error: err => { - console.log(err); - } - }); - } - - ngOnDestroy(): void { - if (this.timeoutId !== null) { - clearTimeout(this.timeoutId); + private readonly _crud = inject(CrudService); + private readonly _toast = inject(ToasterService); + + host: string; + alias: string; + registry: string; + communicationPort: number; + handshakePort: number; + proxyPort: number; + handshake: Handshake = null; + timeoutId: number = null; + lock = false; + connected: boolean | string = 'checking...'; + updateLock = false; + modified = false; + error: string; + + @Input() id: number; + @Output() done = new EventEmitter(); + + constructor() { } - } - - updateValues(instance: DockerInstance) { - this.host = instance.host; - this.alias = instance.alias; - this.registry = instance.registry; - this.connected = instance.connected; - this.communicationPort = instance.communicationPort; - this.handshakePort = instance.handshakePort; - this.proxyPort = instance.proxyPort; - this.modified = false; - } - - updateHandshake() { - if (this.timeoutId === null) { - return; - } - - this._crud.getHandshake(this.host).subscribe( - res => { - const r = res; - - this.handshake = r.handshake; - - if (r.instance.host !== undefined) { - this.updateValues(r.instance); - } - if (this.handshake.status === 'RUNNING') { - if (this.timeoutId !== null) { - this.timeoutId = setTimeout(() => this.updateHandshake(), 1000); + ngOnInit(): void { + this._crud.getDockerInstance(this.id).subscribe({ + next: res => { + this.updateValues(res); } - } else { - this.timeoutId = null; - if (this.handshake.status === 'SUCCESS') { - this.handshake = null; - this._toast.success('Successfully updated docker instance \'' + this.alias + '\''); + , + error: err => { + console.log(err); } - } - }, - err => { - console.log(err); + }); + } + + ngOnDestroy(): void { + if (this.timeoutId !== null) { + clearTimeout(this.timeoutId); } - ); - } + } - reconnectToDockerInstance() { - if (this.updateLock) { - return; + updateValues(instance: DockerInstance) { + this.host = instance.host; + this.alias = instance.alias; + this.registry = instance.registry; + this.connected = instance.connected; + this.communicationPort = instance.communicationPort; + this.handshakePort = instance.handshakePort; + this.proxyPort = instance.proxyPort; + this.modified = false; } - this.updateLock = true; - this._crud.reconnectToDockerInstance(this.id).subscribe( - res => { - const r = res; - if (r.error !== '') { - this.error = r.error; - this.updateLock = false; + + updateHandshake() { + if (this.timeoutId === null) { return; - } - this.handshake = r.handshake; - this.timeoutId = setTimeout( - () => this.updateHandshake(), - 1000, - ); - this.updateLock = false; - }, - err => { - console.log(err); - this.updateLock = false; } - ); - } - updateDockerInstance() { - if (this.updateLock) { - return; + this._crud.getHandshake(this.host).subscribe( + res => { + const r = res; + + this.handshake = r.handshake; + + if (r.instance.host !== undefined) { + this.updateValues(r.instance); + } + + if (this.handshake.status === 'RUNNING') { + if (this.timeoutId !== null) { + this.timeoutId = setTimeout(() => this.updateHandshake(), 1000); + } + } else { + this.timeoutId = null; + if (this.handshake.status === 'SUCCESS') { + this.handshake = null; + this._toast.success('Successfully updated docker instance \'' + this.alias + '\''); + } + } + }, + err => { + console.log(err); + } + ); } - this.updateLock = true; - this._crud.updateDockerInstance(this.id, this.host, this.alias, this.registry).subscribe( - res => { - const r = res; - if (r.error !== '') { - this.error = r.error; - this.updateLock = false; + + reconnectToDockerInstance() { + if (this.updateLock) { return; - } - this.updateValues(r.instance); - if (r.handshake.status !== undefined) { - this.handshake = r.handshake; - this.timeoutId = setTimeout( - () => this.updateHandshake(), - 1000, - ); - } else { - this._toast.success("Successfully updated config for '" + this.alias + "'"); - } - this.modified = false; - this.updateLock = false; - }, - err => { - this.updateLock = false; - console.log(err); } - ); - } - - testConnection() { - this.connected = 'checking connection'; - this._crud.testDockerInstance(this.id).subscribe( - res => { - const dockerStatus = res; - // TODO: check that instanceId matches - this.connected = dockerStatus.successful; - if (dockerStatus.errorMessage !== '') { - this.error = dockerStatus.errorMessage; - } - }, - err => { - this.connected = false; - console.log(err); + this.updateLock = true; + this._crud.reconnectToDockerInstance(this.id).subscribe( + res => { + const r = res; + if (r.error !== '') { + this.error = r.error; + this.updateLock = false; + return; + } + this.handshake = r.handshake; + this.timeoutId = setTimeout( + () => this.updateHandshake(), + 1000, + ); + this.updateLock = false; + }, + err => { + console.log(err); + this.updateLock = false; + } + ); + } + + updateDockerInstance() { + if (this.updateLock) { + return; } - ); - } - - removeDockerInstance() { - this._crud.removeDockerInstance(this.id).subscribe( - res => { - const d = res; - if (d.error === '') { - this._toast.success("Deleted docker instance '" + this.alias + "'"); - } else { - this._toast.error(d.error); - } - this.done.emit(d.instances); - }, - err => { - console.log(err); + this.updateLock = true; + this._crud.updateDockerInstance(this.id, this.host, this.alias, this.registry).subscribe( + res => { + const r = res; + if (r.error !== '') { + this.error = r.error; + this.updateLock = false; + return; + } + this.updateValues(r.instance); + if (r.handshake.status !== undefined) { + this.handshake = r.handshake; + this.timeoutId = setTimeout( + () => this.updateHandshake(), + 1000, + ); + } else { + this._toast.success('Successfully updated config for \'' + this.alias + '\''); + } + this.modified = false; + this.updateLock = false; + }, + err => { + this.updateLock = false; + console.log(err); + } + ); + } + + testConnection() { + this.connected = 'checking connection'; + this._crud.testDockerInstance(this.id).subscribe( + res => { + const dockerStatus = res; + // TODO: check that instanceId matches + this.connected = dockerStatus.successful; + if (dockerStatus.errorMessage !== '') { + this.error = dockerStatus.errorMessage; + } + }, + err => { + this.connected = false; + console.log(err); + } + ); + } + + removeDockerInstance() { + this._crud.removeDockerInstance(this.id).subscribe( + res => { + const d = res; + if (d.error === '') { + this._toast.success('Deleted docker instance \'' + this.alias + '\''); + } else { + this._toast.error(d.error); + } + this.done.emit(d.instances); + }, + err => { + console.log(err); + } + ); + } + + emitDone() { + if (this.handshake !== null) { + this._crud.cancelHandshake(this.host).subscribe( + res => { + }, + err => { + console.log(err); + } + ); } - ); - } - - emitDone() { - if (this.handshake !== null) { - this._crud.cancelHandshake(this.host).subscribe( - res => { - }, - err => { - console.log(err); - } - ); + this.done.emit(); } - this.done.emit(); - } } diff --git a/src/app/components/docker/dockerhandshake/dockerhandshake.component.ts b/src/app/components/docker/dockerhandshake/dockerhandshake.component.ts index 72863922..4ffceebe 100644 --- a/src/app/components/docker/dockerhandshake/dockerhandshake.component.ts +++ b/src/app/components/docker/dockerhandshake/dockerhandshake.component.ts @@ -3,29 +3,29 @@ import {Handshake} from '../../../models/docker.model'; import {UtilService} from '../../../services/util.service'; @Component({ - selector: 'app-dockerhandshake', - templateUrl: './dockerhandshake.component.html', - styleUrls: ['./dockerhandshake.component.scss'] + selector: 'app-dockerhandshake', + templateUrl: './dockerhandshake.component.html', + styleUrls: ['./dockerhandshake.component.scss'] }) export class DockerhandshakeComponent implements OnInit { - public readonly _util = inject(UtilService); + public readonly _util = inject(UtilService); - @Input() handshake: Handshake; - @Output() cancel = new EventEmitter(); - @Output() redo = new EventEmitter(); + @Input() handshake: Handshake; + @Output() cancel = new EventEmitter(); + @Output() redo = new EventEmitter(); - constructor() { - } + constructor() { + } - ngOnInit(): void { - } + ngOnInit(): void { + } - cancelHandshake() { - this.cancel.emit(); - } + cancelHandshake() { + this.cancel.emit(); + } - redoHandshake() { - this.redo.emit(); - } + redoHandshake() { + this.redo.emit(); + } } diff --git a/src/app/components/docker/dockernew/dockernew.component.ts b/src/app/components/docker/dockernew/dockernew.component.ts index 04b0e649..2e37b1eb 100644 --- a/src/app/components/docker/dockernew/dockernew.component.ts +++ b/src/app/components/docker/dockernew/dockernew.component.ts @@ -4,140 +4,140 @@ import {CrudService} from '../../../services/crud.service'; import {ToasterService} from '../../toast-exposer/toaster.service'; @Component({ - selector: 'app-dockernew', - templateUrl: './dockernew.component.html', - styleUrls: ['./dockernew.component.scss'] + selector: 'app-dockernew', + templateUrl: './dockernew.component.html', + styleUrls: ['./dockernew.component.scss'] }) export class DockernewComponent implements OnInit, OnDestroy { - private readonly _crud = inject(CrudService); - private readonly _toast = inject(ToasterService); + private readonly _crud = inject(CrudService); + private readonly _toast = inject(ToasterService); - host: string; - alias: string; - registry = ''; - communicationPort: number; - handshakePort: number; - proxyPort: number; - handshake: Handshake = null; - timeoutId: number = null; - aliasModified = false; - dockerSetupResult: DockerSetupResponse = null; + host: string; + alias: string; + registry = ''; + communicationPort: number; + handshakePort: number; + proxyPort: number; + handshake: Handshake = null; + timeoutId: number = null; + aliasModified = false; + dockerSetupResult: DockerSetupResponse = null; - @Output() done = new EventEmitter(); + @Output() done = new EventEmitter(); - constructor() { - } - - ngOnInit(): void { - } - - ngOnDestroy(): void { - if (this.timeoutId != null) { - clearTimeout(this.timeoutId); + constructor() { } - } - hostInput() { - if (!this.aliasModified) { - this.alias = this.host; + ngOnInit(): void { } - } - - aliasInput() { - this.aliasModified = true; - } - - addDockerInstance() { - this._crud.addDockerInstance(this.host, this.alias, this.registry, this.communicationPort, this.handshakePort, this.proxyPort).subscribe({ - next: res => { - this.dockerSetupResult = res; - if (this.dockerSetupResult.success) { - this.success(this.dockerSetupResult.instances); - } - if (this.dockerSetupResult.handshake.status !== undefined) { - this.handshake = this.dockerSetupResult.handshake; - this.timeoutId = setTimeout(() => this.updateHandshake(), 1000); + ngOnDestroy(): void { + if (this.timeoutId != null) { + clearTimeout(this.timeoutId); } - }, - error: err => { - console.log(err); - } - }); - } - - updateHandshake() { - if (this.timeoutId === null) { - return; } - this._crud.getHandshake(this.host).subscribe( - res => { - const r = res; + hostInput() { + if (!this.aliasModified) { + this.alias = this.host; + } + } - this.handshake = r.handshake; + aliasInput() { + this.aliasModified = true; + } - if (this.handshake.status === 'RUNNING') { - if (this.timeoutId !== null) { - this.timeoutId = setTimeout(() => this.updateHandshake(), 1000); + addDockerInstance() { + this._crud.addDockerInstance(this.host, this.alias, this.registry, this.communicationPort, this.handshakePort, this.proxyPort).subscribe({ + next: res => { + this.dockerSetupResult = res; + if (this.dockerSetupResult.success) { + this.success(this.dockerSetupResult.instances); + } + + if (this.dockerSetupResult.handshake.status !== undefined) { + this.handshake = this.dockerSetupResult.handshake; + this.timeoutId = setTimeout(() => this.updateHandshake(), 1000); + } + }, + error: err => { + console.log(err); } - } else { - this.timeoutId = null; - if (this.handshake.status === 'SUCCESS') { - this.success(undefined); + }); + } + + updateHandshake() { + if (this.timeoutId === null) { + return; + } + + this._crud.getHandshake(this.host).subscribe( + res => { + const r = res; + + this.handshake = r.handshake; + + if (this.handshake.status === 'RUNNING') { + if (this.timeoutId !== null) { + this.timeoutId = setTimeout(() => this.updateHandshake(), 1000); + } + } else { + this.timeoutId = null; + if (this.handshake.status === 'SUCCESS') { + this.success(undefined); + } + } + }, + err => { + console.log(err); } - } - }, - err => { - console.log(err); + ); + } + + redoHandshake() { + if (this.timeoutId !== null) { + return; } - ); - } - redoHandshake() { - if (this.timeoutId !== null) { - return; + this._crud.startHandshake(this.host).subscribe( + res => { + this.handshake = res; + this.timeoutId = setTimeout( + () => this.updateHandshake(), + 1000, + ); + }, + err => { + console.log(err); + } + ); } - this._crud.startHandshake(this.host).subscribe( - res => { - this.handshake = res; - this.timeoutId = setTimeout( - () => this.updateHandshake(), - 1000, - ); - }, - err => { - console.log(err); + cancelHandshake() { + if (this.handshake !== null) { + this.handshake = null; + if (this.timeoutId !== null) { + clearTimeout(this.timeoutId); + this.timeoutId = null; + } + this._crud.cancelHandshake(this.host).subscribe( + res => { + }, + err => { + console.log(err); + } + ); } - ); - } - - cancelHandshake() { - if (this.handshake !== null) { - this.handshake = null; - if (this.timeoutId !== null) { - clearTimeout(this.timeoutId); - this.timeoutId = null; - } - this._crud.cancelHandshake(this.host).subscribe( - res => { - }, - err => { - console.log(err); - } - ); } - } - success(instances: DockerInstance[]) { - this._toast.success('Successfully added docker instance "' + this.alias + '"'); - this.done.emit(instances); - } + success(instances: DockerInstance[]) { + this._toast.success('Successfully added docker instance "' + this.alias + '"'); + this.done.emit(instances); + } - cancel() { - this.cancelHandshake(); - this.done.emit(); - } + cancel() { + this.cancelHandshake(); + this.done.emit(); + } } diff --git a/src/app/components/docker/dockersettings/dockersettings.component.ts b/src/app/components/docker/dockersettings/dockersettings.component.ts index 7d27aafd..47c51559 100644 --- a/src/app/components/docker/dockersettings/dockersettings.component.ts +++ b/src/app/components/docker/dockersettings/dockersettings.component.ts @@ -3,56 +3,56 @@ import {DockerSettings} from '../../../models/docker.model'; import {CrudService} from '../../../services/crud.service'; @Component({ - selector: 'app-dockersettings', - templateUrl: './dockersettings.component.html', - styleUrls: ['./dockersettings.component.scss'] + selector: 'app-dockersettings', + templateUrl: './dockersettings.component.html', + styleUrls: ['./dockersettings.component.scss'] }) export class DockersettingsComponent implements OnInit { - private readonly _crud = inject(CrudService); - - registry: string; - modified = false; - - @Output() done = new EventEmitter(); - - constructor() { - } - - loadValues(settings: DockerSettings) { - this.registry = settings.registry; - this.modified = false; - } - - toDockerSettings(): DockerSettings { - return {'registry': this.registry}; - } - - ngOnInit(): void { - this._crud.getDockerSettings().subscribe({ - next: res => { - this.loadValues(res); - }, - error: err => { - console.log(err); - } - } - ); - } - - saveSettings() { - this._crud.changeDockerSettings(this.toDockerSettings()).subscribe({ - next: res => { - this.loadValues(res); - this.close(); - }, - error: err => { - console.log(err); - } - }); - } - - close() { - this.done.emit(); - } + private readonly _crud = inject(CrudService); + + registry: string; + modified = false; + + @Output() done = new EventEmitter(); + + constructor() { + } + + loadValues(settings: DockerSettings) { + this.registry = settings.registry; + this.modified = false; + } + + toDockerSettings(): DockerSettings { + return {'registry': this.registry}; + } + + ngOnInit(): void { + this._crud.getDockerSettings().subscribe({ + next: res => { + this.loadValues(res); + }, + error: err => { + console.log(err); + } + } + ); + } + + saveSettings() { + this._crud.changeDockerSettings(this.toDockerSettings()).subscribe({ + next: res => { + this.loadValues(res); + this.close(); + }, + error: err => { + console.log(err); + } + }); + } + + close() { + this.done.emit(); + } } diff --git a/src/app/components/dynamic-forms/dynamic-forms.component.spec.ts b/src/app/components/dynamic-forms/dynamic-forms.component.spec.ts index 066880c2..0b2b30b2 100644 --- a/src/app/components/dynamic-forms/dynamic-forms.component.spec.ts +++ b/src/app/components/dynamic-forms/dynamic-forms.component.spec.ts @@ -3,23 +3,23 @@ import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {DynamicFormsComponent} from './dynamic-forms.component'; describe('DynamicFormsComponent', () => { - let component: DynamicFormsComponent; - let fixture: ComponentFixture; + let component: DynamicFormsComponent; + let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [DynamicFormsComponent] - }) - .compileComponents(); - })); + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [DynamicFormsComponent] + }) + .compileComponents(); + })); - beforeEach(() => { - fixture = TestBed.createComponent(DynamicFormsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(DynamicFormsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); }); diff --git a/src/app/components/dynamic-forms/dynamic-forms.component.ts b/src/app/components/dynamic-forms/dynamic-forms.component.ts index f2a5b039..773b7e4b 100644 --- a/src/app/components/dynamic-forms/dynamic-forms.component.ts +++ b/src/app/components/dynamic-forms/dynamic-forms.component.ts @@ -2,73 +2,73 @@ import {Component, Input, OnInit} from '@angular/core'; import {UntypedFormControl, UntypedFormGroup, Validators} from '@angular/forms'; @Component({ - selector: 'app-dynamic-forms', - templateUrl: './dynamic-forms.component.html', - styleUrls: ['./dynamic-forms.component.scss'] + selector: 'app-dynamic-forms', + templateUrl: './dynamic-forms.component.html', + styleUrls: ['./dynamic-forms.component.scss'] }) export class DynamicFormsComponent implements OnInit { - // Quelle Tutorial: https://juristr.com/blog/2017/10/demystify-dynamic-angular-forms/ + // Quelle Tutorial: https://juristr.com/blog/2017/10/demystify-dynamic-angular-forms/ - @Input() formObj; - formObjAsString; - form: UntypedFormGroup; - submitted = false; + @Input() formObj; + formObjAsString; + form: UntypedFormGroup; + submitted = false; - // objectProps; + // objectProps; - constructor() { - } - - ngOnInit() { - // setup the form - const formGroup = {}; - for (const group of this.formObj.groups) { - for (const i of group.groups) { - formGroup[i.key] = new UntypedFormControl(i.value || '', this.mapValidators(i.validation)); - } + constructor() { } - this.form = new UntypedFormGroup(formGroup); + ngOnInit() { + // setup the form + const formGroup = {}; + for (const group of this.formObj.groups) { + for (const i of group.groups) { + formGroup[i.key] = new UntypedFormControl(i.value || '', this.mapValidators(i.validation)); + } + } - this.formObjAsString = JSON.stringify(this.formObj, undefined, 2); - } + this.form = new UntypedFormGroup(formGroup); - private mapValidators(validators) { - const formValidators = []; + this.formObjAsString = JSON.stringify(this.formObj, undefined, 2); + } - if (validators) { - for (const validation of Object.keys(validators)) { - if (validation === 'required') { - formValidators.push(Validators.required); - } else if (validation === 'min') { - formValidators.push(Validators.min(validators[validation])); - } else if (validation === 'email') { - formValidators.push(Validators.email); + private mapValidators(validators) { + const formValidators = []; + + if (validators) { + for (const validation of Object.keys(validators)) { + if (validation === 'required') { + formValidators.push(Validators.required); + } else if (validation === 'min') { + formValidators.push(Validators.min(validators[validation])); + } else if (validation === 'email') { + formValidators.push(Validators.email); + } + } } - } - } - return formValidators; - } + return formValidators; + } - onSubmit(form, e) { - // e.target.classList.add('was-validated'); - this.submitted = true; - } + onSubmit(form, e) { + // e.target.classList.add('was-validated'); + this.submitted = true; + } - inputValidation(key) { - if (this.submitted && this.form.get(key).invalid) { - return {'is-invalid': true}; - } else if (this.submitted) { - return {'is-valid': true}; + inputValidation(key) { + if (this.submitted && this.form.get(key).invalid) { + return {'is-invalid': true}; + } else if (this.submitted) { + return {'is-valid': true}; + } } - } - updateRange(key, event) { - // this.form.get(key).value = event.target.value; - event.target.previousElementSibling.innerText = event.target.value; - return true; - } + updateRange(key, event) { + // this.form.get(key).value = event.target.value; + event.target.previousElementSibling.innerText = event.target.value; + return true; + } } diff --git a/src/app/components/editor/editor.component.ts b/src/app/components/editor/editor.component.ts index fc113021..974faba3 100644 --- a/src/app/components/editor/editor.component.ts +++ b/src/app/components/editor/editor.component.ts @@ -1,4 +1,16 @@ -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'; @@ -12,184 +24,184 @@ import {SidebarNode} from '../../models/sidebar-node.model'; import {CatalogService} from '../../services/catalog.service'; @Component({ - selector: 'app-editor', - templateUrl: './editor.component.html', - styleUrls: ['./editor.component.scss'] + selector: 'app-editor', + templateUrl: './editor.component.html', + styleUrls: ['./editor.component.scss'] }) //ace editor: see: https://medium.com/@ofir3322/create-an-online-ide-with-angular-6-nodejs-part-1-163a939a7929 export class EditorComponent implements OnInit, AfterViewInit, OnChanges { - public readonly _catalog = inject(CatalogService); - - @ViewChild('editor', {static: false}) codeEditorElmRef: ElementRef; - private codeEditor: ace.Ace.Editor; - @Input() readonly ? = false; - @Input() theme ? = 'tomorrow'; - @Input() language ? = 'pgsql'; - @Input() initOptions ?: { [key: string]: any }; - @Input() autocomplete ? = true; - @Input() useParentHeight ? = true; - @Input() code ?; - - suggestions: string[] = []; - private readonly supportedLanguages = ['pgsql', 'sql', 'java', 'python', 'markdown', 'pig']; - - constructor() { - effect(() => { - const catalog = this._catalog.listener(); - untracked(() => { - const map = this.computeSuggestions(this._catalog.getSchemaTree('', true, 3)); - map.forEach((v, k) => { - this.suggestions.push(v); + public readonly _catalog = inject(CatalogService); + + @ViewChild('editor', {static: false}) codeEditorElmRef: ElementRef; + private codeEditor: ace.Ace.Editor; + @Input() readonly ? = false; + @Input() theme ? = 'tomorrow'; + @Input() language ? = 'pgsql'; + @Input() initOptions ?: { [key: string]: any }; + @Input() autocomplete ? = true; + @Input() useParentHeight ? = true; + @Input() code ?; + + suggestions: string[] = []; + private readonly supportedLanguages = ['pgsql', 'sql', 'java', 'python', 'markdown', 'pig']; + + constructor() { + effect(() => { + const catalog = this._catalog.listener(); + untracked(() => { + const map = this.computeSuggestions(this._catalog.getSchemaTree('', true, 3)); + map.forEach((v, k) => { + this.suggestions.push(v); + }); + }); }); - }); - }); - } + } + + ngOnInit() { - ngOnInit() { + } - } + ngOnChanges(changes: SimpleChanges): void { + if (changes.lang && !changes.lang.firstChange) { + this.updateLanguage(); + } + if (changes.autocomplete && !changes.autocomplete.firstChange) { + this.updateAutocomplete(); + } + } - ngOnChanges(changes: SimpleChanges): void { - if (changes.lang && !changes.lang.firstChange) { - this.updateLanguage(); + ngAfterViewInit(): void { + this.initEditor(); + if (this.code) { + this.codeEditor.setValue(this.code, -1); + } + this.codeEditor.resize(); } - if (changes.autocomplete && !changes.autocomplete.firstChange) { - this.updateAutocomplete(); + + blur() { + this.codeEditor.blur(); } - } - ngAfterViewInit(): void { - this.initEditor(); - if (this.code) { - this.codeEditor.setValue(this.code, -1); + focus() { + this.codeEditor.focus(); } - this.codeEditor.resize(); - } - - blur() { - this.codeEditor.blur(); - } - - focus() { - this.codeEditor.focus(); - } - - initEditor() { - const element = this.codeEditorElmRef.nativeElement; - const editorOptions: Partial = { - highlightActiveLine: true - }; - - //from: https://github.com/angular-ui/ui-ace/issues/44 - //setting paths for ace editor dependencies - const defaultPath = ace.config.get('basePath'); - // set your path here - const path = defaultPath.indexOf('../node_modules/ace-builds/src-min-noconflict') === -1 ? './js/ace' : defaultPath; - ace.config.set('basePath', path); - ace.config.set('modePath', path); - ace.config.set('themePath', path); - ace.config.set('workerPath', path); - - this.codeEditor = ace.edit(element, editorOptions); - this.codeEditor.setTheme('ace/theme/' + this.theme); - this.updateLanguage(); - this.codeEditor.setShowFoldWidgets(true); // for the scope fold feature - this.codeEditor.setShowPrintMargin(false); // https://stackoverflow.com/questions/14907184/is-there-a-way-to-hide-the-vertical-ruler-in-ace-editor - if (this.readonly === true) { - this.codeEditor.setReadOnly(true); - // from https://stackoverflow.com/questions/32806060/is-there-a-programmatic-way-to-hide-the-cursor-in-ace-editor - this.codeEditor.renderer.setShowGutter(false); - // from https://stackoverflow.com/questions/28283344/is-there-a-way-to-hide-the-line-numbers-in-ace-editor - this.codeEditor.setHighlightActiveLine(false); + + initEditor() { + const element = this.codeEditorElmRef.nativeElement; + const editorOptions: Partial = { + highlightActiveLine: true + }; + + //from: https://github.com/angular-ui/ui-ace/issues/44 + //setting paths for ace editor dependencies + const defaultPath = ace.config.get('basePath'); + // set your path here + const path = defaultPath.indexOf('../node_modules/ace-builds/src-min-noconflict') === -1 ? './js/ace' : defaultPath; + ace.config.set('basePath', path); + ace.config.set('modePath', path); + ace.config.set('themePath', path); + ace.config.set('workerPath', path); + + this.codeEditor = ace.edit(element, editorOptions); + this.codeEditor.setTheme('ace/theme/' + this.theme); + this.updateLanguage(); + this.codeEditor.setShowFoldWidgets(true); // for the scope fold feature + this.codeEditor.setShowPrintMargin(false); // https://stackoverflow.com/questions/14907184/is-there-a-way-to-hide-the-vertical-ruler-in-ace-editor + if (this.readonly === true) { + this.codeEditor.setReadOnly(true); + // from https://stackoverflow.com/questions/32806060/is-there-a-programmatic-way-to-hide-the-cursor-in-ace-editor + this.codeEditor.renderer.setShowGutter(false); + // from https://stackoverflow.com/questions/28283344/is-there-a-way-to-hide-the-line-numbers-in-ace-editor + this.codeEditor.setHighlightActiveLine(false); + } + if (this.autocomplete) { + this.setAutocomplete(); + } + if (this.initOptions) { + this.codeEditor.setOptions(this.initOptions); + } } - if (this.autocomplete) { - this.setAutocomplete(); + + getCode() { + return this.codeEditor.getValue(); } - if (this.initOptions) { - this.codeEditor.setOptions(this.initOptions); + + setCode(code: string) { + if (this.codeEditor) { + this.codeEditor.setValue(code, 1); + } } - } - getCode() { - return this.codeEditor.getValue(); - } + // from: https://stackoverflow.com/questions/30041816/ace-editor-autocomplete-custom-strings + setAutocomplete() { + this.codeEditor.setOptions({enableLiveAutocompletion: true}); + const self = this; + const staticWordCompleter = { + getCompletions: function (editor, session, pos, prefix, callback) { + const wordList = self.suggestions; + callback(null, wordList.map(function (word) { + return { + caption: word, + value: word, + meta: 'static' + }; + })); + } + }; + this.codeEditor['completers'].push(staticWordCompleter); + } - setCode(code: string) { - if (this.codeEditor) { - this.codeEditor.setValue(code, 1); + /** + * Compute a map containing all schemas, tables and columns + */ + computeSuggestions(tree: SidebarNode[]) { + let map = new Map(); + tree.forEach((v, k) => { + map = this.suggestionBuilder(v, map); + }); + return map; } - } - - // from: https://stackoverflow.com/questions/30041816/ace-editor-autocomplete-custom-strings - setAutocomplete() { - this.codeEditor.setOptions({enableLiveAutocompletion: true}); - const self = this; - const staticWordCompleter = { - getCompletions: function (editor, session, pos, prefix, callback) { - const wordList = self.suggestions; - callback(null, wordList.map(function (word) { - return { - caption: word, - value: word, - meta: 'static' - }; - })); - } - }; - this.codeEditor['completers'].push(staticWordCompleter); - } - - /** - * Compute a map containing all schemas, tables and columns - */ - computeSuggestions(tree: SidebarNode[]) { - let map = new Map(); - tree.forEach((v, k) => { - map = this.suggestionBuilder(v, map); - }); - return map; - } - - /** - * recursive function to build map with all schemas, tables and columns - */ - suggestionBuilder(node: SidebarNode, map: Map) { - map.set(node.name, node.name); - if (node.children.length > 0) { - node.children.forEach((v, n) => { - map = this.suggestionBuilder(v, map); - }); + + /** + * recursive function to build map with all schemas, tables and columns + */ + suggestionBuilder(node: SidebarNode, map: Map) { + map.set(node.name, node.name); + if (node.children.length > 0) { + node.children.forEach((v, n) => { + map = this.suggestionBuilder(v, map); + }); + } + return map; } - return map; - } - - /** - * Change the syntax highlighting language to this.lang, if it is supported. - * Otherwise, sql is used. - */ - updateLanguage() { - if (this.supportedLanguages.includes(this.language)) { - this.codeEditor.getSession().setMode('ace/mode/' + this.language); - } else { - this.codeEditor.getSession().setMode('ace/mode/sql'); + /** + * Change the syntax highlighting language to this.lang, if it is supported. + * Otherwise, sql is used. + */ + updateLanguage() { + if (this.supportedLanguages.includes(this.language)) { + this.codeEditor.getSession().setMode('ace/mode/' + this.language); + } else { + this.codeEditor.getSession().setMode('ace/mode/sql'); + + } } - } - updateAutocomplete() { - if (this.autocomplete && !this.codeEditor['completers']) { - this.setAutocomplete(); - } else { - this.codeEditor.setOptions({enableLiveAutocompletion: this.autocomplete}); + updateAutocomplete() { + if (this.autocomplete && !this.codeEditor['completers']) { + this.setAutocomplete(); + } else { + this.codeEditor.setOptions({enableLiveAutocompletion: this.autocomplete}); + } } - } - 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); - } + 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); + } } diff --git a/src/app/components/graph/graph.component.ts b/src/app/components/graph/graph.component.ts index c5619391..af4c39b1 100644 --- a/src/app/components/graph/graph.component.ts +++ b/src/app/components/graph/graph.component.ts @@ -2,221 +2,221 @@ import {Component, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core' import {hexToRgba} from '@coreui/utils'; @Component({ - selector: 'app-graph', - templateUrl: './graph.component.html', - styleUrls: ['./graph.component.scss'] + selector: 'app-graph', + templateUrl: './graph.component.html', + styleUrls: ['./graph.component.scss'] }) export class GraphComponent implements OnInit, OnChanges { - _chartType: string; - @Input() chartType: string; + _chartType: string; + @Input() chartType: string; - _data; - @Input() data: Array; + _data; + @Input() data: Array; - _labels; - @Input() labels: Array; + _labels; + @Input() labels: Array; - _colorList; - @Input() colorList: Array; + _colorList; + @Input() colorList: Array; - //@Input() config?:any; + //@Input() config?:any; - _min: number; - @Input() min: number; + _min: number; + @Input() min: number; - _max: number; - @Input() max: number; + _max: number; + @Input() max: number; - _xLabel: string; - @Input() xLabel: string; + _xLabel: string; + @Input() xLabel: string; - _yLabel: string; - @Input() yLabel: string; + _yLabel: string; + @Input() yLabel: string; - @Input() maintainAspectRatio = true; + @Input() maintainAspectRatio = true; - options: any = { - animation: false, - responsive: true, - maintainAspectRatio: true, - tooltips: { - enabled: false, - }, - layout: { - padding: { - left: 16, - right: 16, - top: 16, - bottom: 16 - } - }, - scales: { - y: { - scaleLabel: { - display: false, - labelString: '' + options: any = { + animation: false, + responsive: true, + maintainAspectRatio: true, + tooltips: { + enabled: false, }, - ticks: { - //values are set by updateOptions() - //suggestedMin: 0, - //suggestedMax: 0 - } - }, - x: { - scaleLabel: { - display: false, - labelString: '' - } - } - } - }; + layout: { + padding: { + left: 16, + right: 16, + top: 16, + bottom: 16 + } + }, + scales: { + y: { + scaleLabel: { + display: false, + labelString: '' + }, + ticks: { + //values are set by updateOptions() + //suggestedMin: 0, + //suggestedMax: 0 + } + }, + x: { + scaleLabel: { + display: false, + labelString: '' + } + } + } + }; - colors;//will be assigned in function + colors;//will be assigned in function - doughnutPolarColors = [{ - backgroundColor: [], - borderColor: [], - borderWidth: 1 - }]; + doughnutPolarColors = [{ + backgroundColor: [], + borderColor: [], + borderWidth: 1 + }]; - barColors = []; + barColors = []; - legend = true; + legend = true; - constructor() { - } + constructor() { + } - ngOnInit() { - const numberOfColors = 9; + ngOnInit() { + const numberOfColors = 9; - const iterableColorList = this.generateIterableArray(this.colorList); - for (let i = 0; i < numberOfColors; i++) { - this.doughnutPolarColors[0].backgroundColor[i] = hexToRgba(iterableColorList.next(), 60); - this.doughnutPolarColors[0].borderColor[i] = iterableColorList.lastUsed(); - this.barColors[i] = { - borderWidth: 1, - backgroundColor: (iterableColorList.lastUsed() ? hexToRgba(iterableColorList.lastUsed(), 60) : null) - }; - } - } - - /** - * ngOnChanges instead of @Input setters is needed to make sure that - * the changes on 'chartType' are applied first, since the changes on - * other variables depend on it - */ - ngOnChanges(changes: SimpleChanges) { - if (changes['chartType']) { - this.setChartType(changes['chartType'].currentValue); - } - if (changes['data']) { - this._data = this.mapData(changes['data'].currentValue); - } - if (changes['labels']) { - this._labels = this.mapLabel(changes['labels'].currentValue); - } - if (changes['colors']) { - this._colorList = changes['colors'].currentValue; - } - if (changes['min']) { - this._min = changes['min'].currentValue; - this.updateOptions(); - } - if (changes['max']) { - this._max = changes['max'].currentValue; - this.updateOptions(); - } - if (changes['xLabel']) { - this._xLabel = changes['xLabel'].currentValue; - this.updateOptions(); - } - if (changes['yLabel']) { - this._yLabel = changes['yLabel'].currentValue; - this.updateOptions(); - } - if (changes['maintainAspectRatio']) { - this.options.maintainAspectRatio = changes['maintainAspectRatio'].currentValue; + const iterableColorList = this.generateIterableArray(this.colorList); + for (let i = 0; i < numberOfColors; i++) { + this.doughnutPolarColors[0].backgroundColor[i] = hexToRgba(iterableColorList.next(), 60); + this.doughnutPolarColors[0].borderColor[i] = iterableColorList.lastUsed(); + this.barColors[i] = { + borderWidth: 1, + backgroundColor: (iterableColorList.lastUsed() ? hexToRgba(iterableColorList.lastUsed(), 60) : null) + }; + } } - } - setChartType(chartType) { - chartType = chartType.toLowerCase() || 'line'; - if (chartType === 'polararea') { - chartType = 'polarArea'; - } - if (chartType === 'doughnut' || chartType === 'polarArea') { - this.colors = this.doughnutPolarColors; - } else if (chartType === 'bar') { - this.colors = this.barColors; - } else { - this.colors = undefined; - } - this._chartType = chartType; - const showAxes = ['bar', 'line'].includes(this._chartType); - this.options.scales.x.display = showAxes; - this.options.scales.y.display = showAxes; - } - - mapData(data) { - // map hashmap to array - const data2 = []; - for (const key of Object.keys(data)) { - data2.push(data[key]); - } - return data2; - } - - mapLabel(labels) { - let labels2 = labels; - if (['line', 'bar'].includes(this._chartType) && !labels) { - const length = this._data[0].data.length; - labels2 = Array(length).fill(''); + /** + * ngOnChanges instead of @Input setters is needed to make sure that + * the changes on 'chartType' are applied first, since the changes on + * other variables depend on it + */ + ngOnChanges(changes: SimpleChanges) { + if (changes['chartType']) { + this.setChartType(changes['chartType'].currentValue); + } + if (changes['data']) { + this._data = this.mapData(changes['data'].currentValue); + } + if (changes['labels']) { + this._labels = this.mapLabel(changes['labels'].currentValue); + } + if (changes['colors']) { + this._colorList = changes['colors'].currentValue; + } + if (changes['min']) { + this._min = changes['min'].currentValue; + this.updateOptions(); + } + if (changes['max']) { + this._max = changes['max'].currentValue; + this.updateOptions(); + } + if (changes['xLabel']) { + this._xLabel = changes['xLabel'].currentValue; + this.updateOptions(); + } + if (changes['yLabel']) { + this._yLabel = changes['yLabel'].currentValue; + this.updateOptions(); + } + if (changes['maintainAspectRatio']) { + this.options.maintainAspectRatio = changes['maintainAspectRatio'].currentValue; + } } - return labels2; - } - - updateOptions() { - if (['line', 'bar'].includes(this._chartType)) { - if (this._min) { - this.options.scales.y.ticks.suggestedMin = this._min; - } - if (this._max) { - this.options.scales.y.ticks.suggestedMax = this._max; - } + + setChartType(chartType) { + chartType = chartType.toLowerCase() || 'line'; + if (chartType === 'polararea') { + chartType = 'polarArea'; + } + if (chartType === 'doughnut' || chartType === 'polarArea') { + this.colors = this.doughnutPolarColors; + } else if (chartType === 'bar') { + this.colors = this.barColors; + } else { + this.colors = undefined; + } + this._chartType = chartType; + const showAxes = ['bar', 'line'].includes(this._chartType); + this.options.scales.x.display = showAxes; + this.options.scales.y.display = showAxes; } - if (this._xLabel) { - this.options.scales.x.scaleLabel.display = true; - this.options.scales.x.scaleLabel.labelString = this._xLabel; + + mapData(data) { + // map hashmap to array + const data2 = []; + for (const key of Object.keys(data)) { + data2.push(data[key]); + } + return data2; } - if (this._yLabel) { - this.options.scales.y.scaleLabel.display = true; - this.options.scales.y.scaleLabel.labelString = this._yLabel; + + mapLabel(labels) { + let labels2 = labels; + if (['line', 'bar'].includes(this._chartType) && !labels) { + const length = this._data[0].data.length; + labels2 = Array(length).fill(''); + } + return labels2; } - } - - generateIterableArray = (arr: any) => ({ - nextIndex: 0, - arr, - next() { - if (this.arr.length === 0) { - return '#000000'; - } - if (this.nextIndex >= (this.arr.length)) { - this.nextIndex = 0; - } - return this.arr[this.nextIndex++]; - }, - lastUsed() { - if (this.nextIndex === 0) { - return this.arr[0]; - } else { - return this.arr[this.nextIndex - 1]; - } + + updateOptions() { + if (['line', 'bar'].includes(this._chartType)) { + if (this._min) { + this.options.scales.y.ticks.suggestedMin = this._min; + } + if (this._max) { + this.options.scales.y.ticks.suggestedMax = this._max; + } + } + if (this._xLabel) { + this.options.scales.x.scaleLabel.display = true; + this.options.scales.x.scaleLabel.labelString = this._xLabel; + } + if (this._yLabel) { + this.options.scales.y.scaleLabel.display = true; + this.options.scales.y.scaleLabel.labelString = this._yLabel; + } } - }) + + generateIterableArray = (arr: any) => ({ + nextIndex: 0, + arr, + next() { + if (this.arr.length === 0) { + return '#000000'; + } + if (this.nextIndex >= (this.arr.length)) { + this.nextIndex = 0; + } + return this.arr[this.nextIndex++]; + }, + lastUsed() { + if (this.nextIndex === 0) { + return this.arr[0]; + } else { + return this.arr[this.nextIndex - 1]; + } + } + }); } diff --git a/src/app/components/information-manager/information-manager.component.ts b/src/app/components/information-manager/information-manager.component.ts index cb7044c4..fb6565fa 100644 --- a/src/app/components/information-manager/information-manager.component.ts +++ b/src/app/components/information-manager/information-manager.component.ts @@ -4,84 +4,84 @@ import {KeyValue} from '@angular/common'; import {InformationService} from '../../services/information.service'; @Component({ - selector: 'app-information-manager', - templateUrl: './information-manager.component.html', - styleUrls: ['./information-manager.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'app-information-manager', + templateUrl: './information-manager.component.html', + styleUrls: ['./information-manager.component.scss'], + encapsulation: ViewEncapsulation.None }) export class InformationManagerComponent implements OnInit { - @Input() data: InformationPage; - refreshingPage = false; - refreshingGroup = []; - width = signal(1080); - zoom = computed(() => { - if (this.width() < 1200 || this.data.fullWidth) { - return 12; - } else { - return 4; - } - }); - - @HostListener('window:resize', ['$event']) - onResize(event?) { - untracked(() => { - this.width.set(window.innerWidth); + @Input() data: InformationPage; + refreshingPage = false; + refreshingGroup = []; + width = signal(1080); + zoom = computed(() => { + if (this.width() < 1200 || this.data.fullWidth) { + return 12; + } else { + return 4; + } }); - } + + @HostListener('window:resize', ['$event']) + onResize(event?) { + untracked(() => { + this.width.set(window.innerWidth); + }); + } - constructor( - private _information: InformationService - ) { - this.onResize(); - } + constructor( + private _information: InformationService + ) { + this.onResize(); + } - ngOnInit() { - } + ngOnInit() { + } - getCardClass(color) { - switch (color) { - case 'BLUE': - return 'bg-primary'; - case 'LIGHTBLUE': - return 'bg-info'; - case 'YELLOW': - return 'bg-warning'; - case 'RED': - return 'bg-danger'; - case 'GREEN': - return 'bg-success'; - default: - return ''; + getCardClass(color) { + switch (color) { + case 'BLUE': + return 'bg-primary'; + case 'LIGHTBLUE': + return 'bg-info'; + case 'YELLOW': + return 'bg-warning'; + case 'RED': + return 'bg-danger'; + case 'GREEN': + return 'bg-success'; + default: + return ''; + } } - } - /** order groups within a page, respectively information-elements within a group - * items with lower order value are rendered first, then this with higher values, then thows where uiOrder is null ( -> 0) - */ - public order(a: KeyValue, b: KeyValue) { - let out = 0; - if (a.value.uiOrder !== 0 && b.value.uiOrder === 0) { - out = -1; - } else if (a.value.uiOrder === 0 && b.value.uiOrder !== 0) { - out = 1; - } else if (a.value.uiOrder > b.value.uiOrder) { - out = 1; - } else if (a.value.uiOrder < b.value.uiOrder) { - out = -1; + /** order groups within a page, respectively information-elements within a group + * items with lower order value are rendered first, then this with higher values, then thows where uiOrder is null ( -> 0) + */ + public order(a: KeyValue, b: KeyValue) { + let out = 0; + if (a.value.uiOrder !== 0 && b.value.uiOrder === 0) { + out = -1; + } else if (a.value.uiOrder === 0 && b.value.uiOrder !== 0) { + out = 1; + } else if (a.value.uiOrder > b.value.uiOrder) { + out = 1; + } else if (a.value.uiOrder < b.value.uiOrder) { + out = -1; + } + return out; } - return out; - } - refreshPage() { - this.refreshingPage = true; - this._information.refreshPage(this.data.id).subscribe().add(() => this.refreshingPage = false); - } + refreshPage() { + this.refreshingPage = true; + this._information.refreshPage(this.data.id).subscribe().add(() => this.refreshingPage = false); + } - refreshGroup(id: string) { - this.refreshingGroup[id] = true; - this._information.refreshGroup(id).subscribe().add(() => this.refreshingGroup[id] = false); - } + refreshGroup(id: string) { + this.refreshingGroup[id] = true; + this._information.refreshGroup(id).subscribe().add(() => this.refreshingGroup[id] = false); + } } 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 5848f74b..567466a5 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 @@ -5,160 +5,160 @@ import {ToasterService} from '../../toast-exposer/toaster.service'; import {ProgressbarType} from 'ngx-bootstrap/progressbar'; @Component({ - selector: 'app-render-item', - templateUrl: './render-item.component.html', - styleUrls: ['./render-item.component.scss'] + selector: 'app-render-item', + templateUrl: './render-item.component.html', + styleUrls: ['./render-item.component.scss'] }) export class RenderItemComponent implements OnInit { - @Input() li: InformationObject; - executingInformationAction = false; + @Input() li: InformationObject; + executingInformationAction = false; - constructor( - private _infoService: InformationService, - private _toast: ToasterService - ) { - } - - ngOnInit() { - } + constructor( + private _infoService: InformationService, + private _toast: ToasterService + ) { + } - displayProgressValue(li) { - if ((li.min === undefined || li.min === 0) && (li.max === undefined || li.max === 100)) { - return li.value + '%'; - } else { - li.max = li.max || 100; - return li.value + '/' + li.max; + ngOnInit() { } - } - - getProgressColor(li): ProgressbarType { - const col = li.color || 'dynamic'; - switch (col) { - case 'BLUE': - return 'info'; - case 'GREEN': - return 'success'; - case 'YELLOW': - return 'warning'; - case 'RED': - return 'danger'; - case 'DYNAMIC': - if (li.value === undefined) { - return 'info'; + + displayProgressValue(li) { + if ((li.min === undefined || li.min === 0) && (li.max === undefined || li.max === 100)) { + return li.value + '%'; } else { - li.min = li.min || 0; - li.max = li.max || 100; - const current = li.value / li.max; - if (current < 0.25) { - return 'info'; - } else if (current < 0.5) { - return 'success'; - } else if (current < 0.75) { - return 'warning'; - } else { - return 'danger'; - } + li.max = li.max || 100; + return li.value + '/' + li.max; } - default: - return 'info'; } - } - - 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'; + + getProgressColor(li): ProgressbarType { + const col = li.color || 'dynamic'; + switch (col) { + case 'BLUE': + return 'info'; + case 'GREEN': + return 'success'; + case 'YELLOW': + return 'warning'; + case 'RED': + return 'danger'; + case 'DYNAMIC': + if (li.value === undefined) { + return 'info'; + } else { + li.min = li.min || 0; + li.max = li.max || 100; + const current = li.value / li.max; + if (current < 0.25) { + return 'info'; + } else if (current < 0.5) { + return 'success'; + } else if (current < 0.75) { + return 'warning'; + } else { + return 'danger'; + } + } + default: + return 'info'; + } } - } - - executeInformationAction(i: InformationObject) { - this.executingInformationAction = true; - this._infoService.executeAction(i).subscribe({ - next: res => { - const result = res; - if (result.errorMsg) { - this._toast.warn(result.errorMsg); - } else if (result.successMsg) { - this._toast.success(result.successMsg); + + 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'; } - }, error: err => { - console.log(err); - this._toast.error(err.message); - } - }).add(() => this.executingInformationAction = false); - } - - displayTime(nanoSecs: number) { - const text = []; - if (Math.floor(nanoSecs / 3.6e12) > 1) { - text.push(Math.floor(nanoSecs / 3.6e12) + 'h'); - nanoSecs = nanoSecs % 3.6e12; } - if (Math.floor(nanoSecs / 6e10) > 1) { - text.push(Math.floor(nanoSecs / 6e10) + 'min'); - nanoSecs = nanoSecs % 6e10; + + executeInformationAction(i: InformationObject) { + this.executingInformationAction = true; + this._infoService.executeAction(i).subscribe({ + next: res => { + const result = res; + if (result.errorMsg) { + this._toast.warn(result.errorMsg); + } else if (result.successMsg) { + this._toast.success(result.successMsg); + } + }, error: err => { + console.log(err); + this._toast.error(err.message); + } + }).add(() => this.executingInformationAction = false); + } + + displayTime(nanoSecs: number) { + const text = []; + if (Math.floor(nanoSecs / 3.6e12) > 1) { + text.push(Math.floor(nanoSecs / 3.6e12) + 'h'); + nanoSecs = nanoSecs % 3.6e12; + } + if (Math.floor(nanoSecs / 6e10) > 1) { + text.push(Math.floor(nanoSecs / 6e10) + 'min'); + nanoSecs = nanoSecs % 6e10; + } + if (Math.floor(nanoSecs / 1e9) > 1) { + text.push(Math.floor(nanoSecs / 1e9) + 's'); + nanoSecs = nanoSecs % 1e9; + } + if (Math.floor(nanoSecs / 1e6) > 1) { + text.push(Math.floor(nanoSecs / 1e6) + 'ms'); + nanoSecs = nanoSecs % 1e6; + } + text.push(nanoSecs + 'ns'); + return text.join(' '); } - if (Math.floor(nanoSecs / 1e9) > 1) { - text.push(Math.floor(nanoSecs / 1e9) + 's'); - nanoSecs = nanoSecs % 1e9; + + showTotalDuration(d: InformationObject) { + return d.name == null && d.children && d.children.length !== 1; } - if (Math.floor(nanoSecs / 1e6) > 1) { - text.push(Math.floor(nanoSecs / 1e6) + 'ms'); - nanoSecs = nanoSecs % 1e6; + + castDuration(d: Duration): InformationObject { + return d; } - text.push(nanoSecs + 'ns'); - return text.join(' '); - } - - showTotalDuration(d: InformationObject) { - return d.name == null && d.children && d.children.length !== 1; - } - - castDuration(d: Duration): InformationObject { - return d; - } - - /** - * Get the width in percent - * Min-width is 5px (set by CSS) - */ - getProgressWidth(parent: Duration, child: Duration) { - let total = 0; - for (const c of parent.children) { - total += c.duration; + + /** + * Get the width in percent + * Min-width is 5px (set by CSS) + */ + getProgressWidth(parent: Duration, child: Duration) { + let total = 0; + for (const c of parent.children) { + total += c.duration; + } + return child.duration / total * 100; } - return child.duration / total * 100; - } - - getDurationColor(i: number) { - //alternate between blue and yellow - const colors = ['', 'bg-warning']; - return colors[i % colors.length]; - } - - parameterWarning(i: InformationObject, tooltip) { - let show = false; - for (const p of Object.keys(i.parameters)) { - if (!i.parameters[p]) { - show = true; - } + + getDurationColor(i: number) { + //alternate between blue and yellow + const colors = ['', 'bg-warning']; + return colors[i % colors.length]; } - if (show) { - tooltip.show(); + + parameterWarning(i: InformationObject, tooltip) { + let show = false; + for (const p of Object.keys(i.parameters)) { + if (!i.parameters[p]) { + show = true; + } + } + if (show) { + tooltip.show(); + } } - } - trackBy(index: any, item: any) { - return index; - } + trackBy(index: any, item: any) { + return index; + } } diff --git a/src/app/components/json/json-editor.component.ts b/src/app/components/json/json-editor.component.ts index 845c110d..5841289f 100644 --- a/src/app/components/json/json-editor.component.ts +++ b/src/app/components/json/json-editor.component.ts @@ -2,260 +2,260 @@ import {Component, EventEmitter, Input, OnInit, Output,} from '@angular/core'; @Component({ - selector: 'app-json-editor', - templateUrl: './json-editor.component.html', - styleUrls: ['./json-editor.component.scss'] + selector: 'app-json-editor', + templateUrl: './json-editor.component.html', + styleUrls: ['./json-editor.component.scss'] }) export class JsonEditorComponent implements OnInit { - @Output() valid: boolean; - - constructor() { - this.data = []; - } - - data: Pair[]; - @Input() empty: boolean; - @Input() json: {}; - @Output() valueChange = new EventEmitter(); - @Output() validChange = new EventEmitter(); - show = false; - private debounce: any; - private debounceDelay = 200; - showError: boolean; - - - private static tryParse(value: string | number | {}) { - try { - return JSON.parse(value.toString().replace('\"', '"').replace('"', '\"')); - } catch (e) { - return null; + @Output() valid: boolean; + + constructor() { + this.data = []; } - } - private static getPair(data: any) { - if (data instanceof Object) { - const temp = []; - for (const [key, value] of Object.entries(data)) { - let val = value; - if (value instanceof Object) { - val = this.getPair(value); + data: Pair[]; + @Input() empty: boolean; + @Input() json: {}; + @Output() valueChange = new EventEmitter(); + @Output() validChange = new EventEmitter(); + show = false; + private debounce: any; + private debounceDelay = 200; + showError: boolean; + + + private static tryParse(value: string | number | {}) { + try { + return JSON.parse(value.toString().replace('\"', '"').replace('"', '\"')); + } catch (e) { + return null; + } + } + + private static getPair(data: any) { + if (data instanceof Object) { + const temp = []; + for (const [key, value] of Object.entries(data)) { + let val = value; + if (value instanceof Object) { + val = this.getPair(value); + } + + temp.push(new Pair(key, val)); + } + return temp; + } else { + return data; } + } + + getType(el: Pair) { + if (el.value instanceof Array) { + if (el.value.some(e => e instanceof Pair)) { + return Type.Object; + } else { + return Type.Array; + } + } + return Type.Value; + } + + ngOnInit() { + this.addInitialValues(); + } - temp.push(new Pair(key, val)); - } - return temp; - } else { - return data; + changeHappened() { + const json = this.generateJson(this.data); + this.valueChange.emit(JSON.stringify(json)); + this.validChanged(); } - } - - getType(el: Pair) { - if (el.value instanceof Array) { - if (el.value.some(e => e instanceof Pair)) { - return Type.Object; - } else { - return Type.Array; - } + + private generateJson(raw: any) { + const data = {}; + for (const entry of raw) { + const parsed = JsonEditorComponent.tryParse(entry.value); + + if (!isNaN(entry.value)) { + data[entry.key] = Number(entry.value); + } else if (entry.value instanceof Array && entry.value.some(el => el instanceof Pair)) { + data[entry.key] = this.generateJson(entry.value); + } else if (parsed !== null) { + data[entry.key] = parsed; + } else { + data[entry.key] = entry.value; + } + } + return data; } - return Type.Value; - } - - ngOnInit() { - this.addInitialValues(); - } - - changeHappened() { - const json = this.generateJson(this.data); - this.valueChange.emit(JSON.stringify(json)); - this.validChanged(); - } - - private generateJson(raw: any) { - const data = {}; - for (const entry of raw) { - const parsed = JsonEditorComponent.tryParse(entry.value); - - if (!isNaN(entry.value)) { - data[entry.key] = Number(entry.value); - } else if (entry.value instanceof Array && entry.value.some(el => el instanceof Pair)) { - data[entry.key] = this.generateJson(entry.value); - } else if (parsed !== null) { - data[entry.key] = parsed; - } else { - data[entry.key] = entry.value; - } + + removeColumn(i: String) { + const splits = i.split('_'); + let temp = this.data; + for (let j = 0; j < splits.length; j++) { + if (j === splits.length - 1) { + temp.splice(Number(splits[j]), 1); + } else { + temp = temp[Number(splits[j])].value as Pair[]; + } + } + this.changeHappened(); } - return data; - } - - removeColumn(i: String) { - const splits = i.split('_'); - let temp = this.data; - for (let j = 0; j < splits.length; j++) { - if (j === splits.length - 1) { - temp.splice(Number(splits[j]), 1); - } else { - temp = temp[Number(splits[j])].value as Pair[]; - } + + addMainColumn(type: Type) { + this.executeAdd(this.data, type); } - this.changeHappened(); - } - - addMainColumn(type: Type) { - this.executeAdd(this.data, type); - } - - executeAdd(arr: Pair[], type: Type) { - if (type === 0) { - arr.push(new Pair('', '')); - } else { - arr.push(new Pair('', [new Pair('', '')])); + + executeAdd(arr: Pair[], type: Type) { + if (type === 0) { + arr.push(new Pair('', '')); + } else { + arr.push(new Pair('', [new Pair('', '')])); + } } - } - - addColumn($event: Info) { - const splits = $event.index.split('_'); - let temp = this.data; - for (let j = 0; j < splits.length; j++) { - if (j === splits.length - 1) { - temp = temp[Number(splits[j])].value as Pair[]; - this.executeAdd(temp, $event.type); - } else { - temp = temp[Number(splits[j])].value as Pair[]; - } + + addColumn($event: Info) { + const splits = $event.index.split('_'); + let temp = this.data; + for (let j = 0; j < splits.length; j++) { + if (j === splits.length - 1) { + temp = temp[Number(splits[j])].value as Pair[]; + this.executeAdd(temp, $event.type); + } else { + temp = temp[Number(splits[j])].value as Pair[]; + } + } + this.changeHappened(); + } + + normalize(value: string | number | {}) { + if (value instanceof Object) { + return JSON.stringify(value); + } else { + return value; + } } - this.changeHappened(); - } - - normalize(value: string | number | {}) { - if (value instanceof Object) { - return JSON.stringify(value); - } else { - return value; + + addInitialValues() { + try { + const data = JSON.parse(this.json.toString().replace('"', '\"')); + this.data = []; + for (const [key, value] of Object.entries(data)) { + const val = JsonEditorComponent.getPair(value); + this.data.push(new Pair(key, val)); + } + + } catch (e) { + console.log('could not translate'); + } + + if (this.empty && this.data.length === 0) { + this.addMainColumn(0); + } } - } - - addInitialValues() { - try { - const data = JSON.parse(this.json.toString().replace('"', '\"')); - this.data = []; - for (const [key, value] of Object.entries(data)) { - const val = JsonEditorComponent.getPair(value); - this.data.push(new Pair(key, val)); - } - - } catch (e) { - console.log('could not translate'); + + changeOrder(index: string, dir: number) { + const splits = index.split('_'); + let temp = this.data; + for (let j = 0; j < splits.length; j++) { + if (j === splits.length - 1) { + this.changeColumnOrder(temp, Number(splits[j]), dir); + } else { + temp = temp[Number(splits[j])].value as Pair[]; + } + } } - if (this.empty && this.data.length === 0) { - this.addMainColumn(0); + changeColumnOrder(data: Pair[], index: number, direction: number) { + if (index + direction < 0 || index + direction >= data.length) { + return; + } + const temp = data[index]; + data.splice(index, 1); + data.splice(index + direction, 0, temp); + this.changeHappened(); } - } - - changeOrder(index: string, dir: number) { - const splits = index.split('_'); - let temp = this.data; - for (let j = 0; j < splits.length; j++) { - if (j === splits.length - 1) { - this.changeColumnOrder(temp, Number(splits[j]), dir); - } else { - temp = temp[Number(splits[j])].value as Pair[]; - } + + setMenuShow(doShow: boolean, instant = false) { + if (instant) { + this.show = doShow; + return; + } + if (!doShow) { + this.debounce = setTimeout(() => { + this.show = false; + }, this.debounceDelay); + } else { + this.show = true; + } } - } - changeColumnOrder(data: Pair[], index: number, direction: number) { - if (index + direction < 0 || index + direction >= data.length) { - return; + menuEnter() { + if (this.show) { + clearTimeout(this.debounce); + } } - const temp = data[index]; - data.splice(index, 1); - data.splice(index + direction, 0, temp); - this.changeHappened(); - } - - setMenuShow(doShow: boolean, instant = false) { - if (instant) { - this.show = doShow; - return; + + isValid() { + return this.data.reduce((c, next) => c && next.isValid(), true); } - if (!doShow) { - this.debounce = setTimeout(() => { - this.show = false; - }, this.debounceDelay); - } else { - this.show = true; + + validChanged() { + const hasDuplicates = new Set(this.data.map(e => e.key)).size === this.data.length; + this.valid = this.data.reduce((c, next) => c && next.isValid(), true) && hasDuplicates; + this.showError = !hasDuplicates; + this.validChange.emit(this.valid); } - } - menuEnter() { - if (this.show) { - clearTimeout(this.debounce); + getIsDuplicate(i: number) { + const keys = this.data.map(k => k.key); + const name = keys[i]; + keys.splice(i); + return keys.includes(name); } - } - - isValid() { - return this.data.reduce((c, next) => c && next.isValid(), true); - } - - validChanged() { - const hasDuplicates = new Set(this.data.map(e => e.key)).size === this.data.length; - this.valid = this.data.reduce((c, next) => c && next.isValid(), true) && hasDuplicates; - this.showError = !hasDuplicates; - this.validChange.emit(this.valid); - } - - getIsDuplicate(i: number) { - const keys = this.data.map(k => k.key); - const name = keys[i]; - keys.splice(i); - return keys.includes(name); - } } export class Pair { - constructor(key: string, value: string | number | {} | Pair[]) { - this.id = Pair.getAndIncrementId(); - this.key = key; - this.value = value; - } - - static idBuilder = 0; - private id: number; - key: string; - value: string | number | {} | Pair[]; - - static getAndIncrementId() { - const id = Pair.idBuilder; - Pair.idBuilder++; - return id; - } - - isValid() { - let temp = this.key.trim() !== ''; - if (this.value instanceof Array && this.value[0] instanceof Pair) { - temp &&= new Set(this.value.map(e => e.key)).size === this.value.length && this.value.map(p => p.isValid()).reduce((c, next) => c && next, true); + constructor(key: string, value: string | number | {} | Pair[]) { + this.id = Pair.getAndIncrementId(); + this.key = key; + this.value = value; + } + + static idBuilder = 0; + private id: number; + key: string; + value: string | number | {} | Pair[]; + + static getAndIncrementId() { + const id = Pair.idBuilder; + Pair.idBuilder++; + return id; + } + + isValid() { + let temp = this.key.trim() !== ''; + if (this.value instanceof Array && this.value[0] instanceof Pair) { + temp &&= new Set(this.value.map(e => e.key)).size === this.value.length && this.value.map(p => p.isValid()).reduce((c, next) => c && next, true); + } + return temp; } - return temp; - } } export class Info { - index: string; - type: Type; + index: string; + type: Type; - constructor(index: string, type: Type) { - this.index = index; - this.type = type; - } + constructor(index: string, type: Type) { + this.index = index; + this.type = type; + } } export enum Type { - Value, Object, Array + Value, Object, Array } diff --git a/src/app/components/json/json-elem/json-elem.component.ts b/src/app/components/json/json-elem/json-elem.component.ts index e048ce73..c79f344b 100644 --- a/src/app/components/json/json-elem/json-elem.component.ts +++ b/src/app/components/json/json-elem/json-elem.component.ts @@ -1,210 +1,219 @@ -import {ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges,} from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, +} from '@angular/core'; import {Info, Pair, Type} from '../json-editor.component'; @Component({ - selector: 'app-json-elem', - templateUrl: './json-elem.component.html', - styleUrls: ['./json-elem.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'app-json-elem', + templateUrl: './json-elem.component.html', + styleUrls: ['./json-elem.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class JsonElemComponent implements OnInit, OnChanges { - static debounceDelay = 200; - - @Input() el: Pair; - @Input() index: number; - @Input() length: number; - @Input() indent: number; - show = false; - @Input() type: Type; - keys = []; - - - @Output() changed = new EventEmitter(); - @Output() remove = new EventEmitter(); - @Output() add = new EventEmitter(); - @Output() up = new EventEmitter(); - @Output() down = new EventEmitter(); - @Output() inputChange = new EventEmitter; - private debounce: any; - valid = false; - @Output() validChanged = new EventEmitter; - @Input() errorMessage: string; - @Input() isDuplicate: boolean; - dupKeyError = 'Only unique keys allowed.'; - childDupStatus = []; - - ngOnChanges(changes: SimpleChanges) { - console.log(changes); - } - - ngOnInit(): void { - this.updateChildren(); - this.inputChanged(); - } - - fakeArray(length: number): Array { - if (length >= 0) { - return new Array(length); - } - } - - removeColumn(i: string) { - this.show = false; - this.remove.emit(i); - } - - addColumn(i: Info) { - this.show = false; - this.add.emit(this.prefixInfo(i)); - } - - upColumn(i: string) { - this.show = false; - this.up.emit(i); - } - - downColumn(i: string) { - this.show = false; - this.down.emit(i); - } - - - changeHappened() { - this.updateChildren(); - this.changed.emit(); - } - - setMenuShow(doShow: boolean, instant = false) { - if (instant) { - this.show = doShow; - return; - } - if (!doShow) { - this.debounce = setTimeout(() => { + static debounceDelay = 200; + + @Input() el: Pair; + @Input() index: number; + @Input() length: number; + @Input() indent: number; + show = false; + @Input() type: Type; + keys = []; + + + @Output() changed = new EventEmitter(); + @Output() remove = new EventEmitter(); + @Output() add = new EventEmitter(); + @Output() up = new EventEmitter(); + @Output() down = new EventEmitter(); + @Output() inputChange = new EventEmitter; + private debounce: any; + valid = false; + @Output() validChanged = new EventEmitter; + @Input() errorMessage: string; + @Input() isDuplicate: boolean; + dupKeyError = 'Only unique keys allowed.'; + childDupStatus = []; + + ngOnChanges(changes: SimpleChanges) { + console.log(changes); + } + + ngOnInit(): void { + this.updateChildren(); + this.inputChanged(); + } + + fakeArray(length: number): Array { + if (length >= 0) { + return new Array(length); + } + } + + removeColumn(i: string) { + this.show = false; + this.remove.emit(i); + } + + addColumn(i: Info) { + this.show = false; + this.add.emit(this.prefixInfo(i)); + } + + upColumn(i: string) { + this.show = false; + this.up.emit(i); + } + + downColumn(i: string) { + this.show = false; + this.down.emit(i); + } + + + changeHappened() { + this.updateChildren(); + this.changed.emit(); + } + + setMenuShow(doShow: boolean, instant = false) { + if (instant) { + this.show = doShow; + return; + } + if (!doShow) { + this.debounce = setTimeout(() => { + this.show = false; + }, JsonElemComponent.debounceDelay); + } else { + this.show = true; + } + } + + menuEnter() { + if (this.show) { + clearTimeout(this.debounce); + } + } + + isObject(value: string | number | {} | Pair[]) { + return value instanceof Array; + } + + asString(index: Number) { + return String(index); + } + + isValue() { + return this.type === Type.Value; + } + + prefixInfo(info: Info) { + return new Info(this.index + '_' + info.index, info.type); + } + + addInitialColumn(index: string, type: Type) { this.show = false; - }, JsonElemComponent.debounceDelay); - } else { - this.show = true; - } - } - - menuEnter() { - if (this.show) { - clearTimeout(this.debounce); - } - } - - isObject(value: string | number | {} | Pair[]) { - return value instanceof Array; - } - - asString(index: Number) { - return String(index); - } - - isValue() { - return this.type === Type.Value; - } - - prefixInfo(info: Info) { - return new Info(this.index + '_' + info.index, info.type); - } - - addInitialColumn(index: string, type: Type) { - this.show = false; - this.add.emit(new Info(index, type)); - } - - getType(el: any) { - if (el.value instanceof Array) { - if (el.value.some(e => e instanceof Pair)) { - return Type.Object; - } else { - return Type.Array; - } - } - return Type.Value; - } - - isSelfValid() { - const temp = this.el.key.trim() !== ''; - if (temp) { - this.errorMessage = ''; - } - return temp; - } - - inputChanged() { - this.valid = this.isSelfValid(); - - if (!this.isValue()) { - const temp = this.el.value instanceof Array && new Set(this.el.value.map(e => e.key)).size === this.el.value.length; - if (!temp) { - this.errorMessage = this.dupKeyError; - } - this.valid &&= temp && this.el.value instanceof Array && this.el.value.reduce((c, next) => c && next.isValid(), true); - } - this.validChanged.emit(); - } - - showValid() { - if (!this.isSelfValid()) { - return false; - } - if (!this.isValue()) { - const temp = this.el.value instanceof Array && new Set(this.el.value.map(e => e.key)).size === this.el.value.length; - if (!temp) { - this.errorMessage = this.dupKeyError; - } - return temp; - } - return true; - } - - updateChildren() { - if (this.isValue()) { - return; - } - const keys = (this.el.value as Pair[]).map(e => e.key); - console.log(keys); - const temp = []; - const copy = keys; - - const status = []; - - let count = 0; - for (const key of keys) { - if (temp.includes(key)) { - status.push(true); - } else { - copy.splice(count); - temp.push(key); - status.push(copy.includes(key)); - } - console.log(copy); - console.log(temp); - console.log(status); - count++; - } - - this.childDupStatus = status; - console.log(this.childDupStatus); - - } - - getIsDuplicate(i: number) { - const keys = (this.el.value as Pair[]).map(k => k.key); - const name = keys[i]; - keys.splice(i); - return keys.includes(name); - } - - protected readonly Pair = Pair; - - asPair(el: string): Pair { - return el as unknown as Pair; - } + this.add.emit(new Info(index, type)); + } + + getType(el: any) { + if (el.value instanceof Array) { + if (el.value.some(e => e instanceof Pair)) { + return Type.Object; + } else { + return Type.Array; + } + } + return Type.Value; + } + + isSelfValid() { + const temp = this.el.key.trim() !== ''; + if (temp) { + this.errorMessage = ''; + } + return temp; + } + + inputChanged() { + this.valid = this.isSelfValid(); + + if (!this.isValue()) { + const temp = this.el.value instanceof Array && new Set(this.el.value.map(e => e.key)).size === this.el.value.length; + if (!temp) { + this.errorMessage = this.dupKeyError; + } + this.valid &&= temp && this.el.value instanceof Array && this.el.value.reduce((c, next) => c && next.isValid(), true); + } + this.validChanged.emit(); + } + + showValid() { + if (!this.isSelfValid()) { + return false; + } + if (!this.isValue()) { + const temp = this.el.value instanceof Array && new Set(this.el.value.map(e => e.key)).size === this.el.value.length; + if (!temp) { + this.errorMessage = this.dupKeyError; + } + return temp; + } + return true; + } + + updateChildren() { + if (this.isValue()) { + return; + } + const keys = (this.el.value as Pair[]).map(e => e.key); + console.log(keys); + const temp = []; + const copy = keys; + + const status = []; + + let count = 0; + for (const key of keys) { + if (temp.includes(key)) { + status.push(true); + } else { + copy.splice(count); + temp.push(key); + status.push(copy.includes(key)); + } + console.log(copy); + console.log(temp); + console.log(status); + count++; + } + + this.childDupStatus = status; + console.log(this.childDupStatus); + + } + + getIsDuplicate(i: number) { + const keys = (this.el.value as Pair[]).map(k => k.key); + const name = keys[i]; + keys.splice(i); + return keys.includes(name); + } + + protected readonly Pair = Pair; + + asPair(el: string): Pair { + return el as unknown as Pair; + } } diff --git a/src/app/components/left-sidebar/left-sidebar.component.scss b/src/app/components/left-sidebar/left-sidebar.component.scss index 7580d538..003cc0ba 100644 --- a/src/app/components/left-sidebar/left-sidebar.component.scss +++ b/src/app/components/left-sidebar/left-sidebar.component.scss @@ -67,14 +67,14 @@ margin-right: 1px; } -.sidebar-toggle{ +.sidebar-toggle { position: absolute; top: 1rem; transition: right .2s; right: -50px; background-color: #3c4b64; - &.open{ + &.open { right: -10px; top: 0.1rem; display: inline-block; @@ -85,11 +85,11 @@ } } -.toggle-children-placeholder{ +.toggle-children-placeholder { width: 13px !important; } -.node-content-wrapper:hover{ +.node-content-wrapper:hover { background: #ebedef; color: #3d434a; } diff --git a/src/app/components/left-sidebar/left-sidebar.component.spec.ts b/src/app/components/left-sidebar/left-sidebar.component.spec.ts index ed64a380..4f6493ad 100644 --- a/src/app/components/left-sidebar/left-sidebar.component.spec.ts +++ b/src/app/components/left-sidebar/left-sidebar.component.spec.ts @@ -3,23 +3,23 @@ import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {LeftSidebarComponent} from './left-sidebar.component'; describe('LeftSidebarComponent', () => { - let component: LeftSidebarComponent; - let fixture: ComponentFixture; + let component: LeftSidebarComponent; + let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [LeftSidebarComponent] - }) - .compileComponents(); - })); + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [LeftSidebarComponent] + }) + .compileComponents(); + })); - beforeEach(() => { - fixture = TestBed.createComponent(LeftSidebarComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(LeftSidebarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); }); diff --git a/src/app/components/left-sidebar/left-sidebar.component.ts b/src/app/components/left-sidebar/left-sidebar.component.ts index e5f2f1ac..e4f7e758 100644 --- a/src/app/components/left-sidebar/left-sidebar.component.ts +++ b/src/app/components/left-sidebar/left-sidebar.component.ts @@ -8,161 +8,161 @@ import {CatalogState} from '../../models/catalog.model'; @Component({ - selector: 'app-left-sidebar', - templateUrl: './left-sidebar.component.html', - styleUrls: ['./left-sidebar.component.scss'] + selector: 'app-left-sidebar', + templateUrl: './left-sidebar.component.html', + styleUrls: ['./left-sidebar.component.scss'] }) //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); - - constructor() { - this.router = this._router; - //this.nodes = nodes; - this.options = { - actionMapping: { - mouse: { - click: (tree, node, $event) => { - this._sidebar.selectedNodeId = node.data.id; - if (node.data.action !== null) { - node.data.action(tree, node, $event); + private readonly _router = inject(Router); + public readonly _sidebar = inject(LeftSidebarService); + public readonly _catalog = inject(CatalogService); + + constructor() { + this.router = this._router; + //this.nodes = nodes; + this.options = { + actionMapping: { + mouse: { + click: (tree, node, $event) => { + this._sidebar.selectedNodeId = node.data.id; + if (node.data.action !== null) { + node.data.action(tree, node, $event); + } + if (node.data.routerLink && node.data.allowRouting) { + this._router.navigate([node.data.routerLink]).then(r => null); + } + if (node.data.isAutoExpand()) { + node.toggleExpanded(); + //TREE_ACTIONS.TOGGLE_EXPANDED(tree, node, $event); + } + if (node.data.isAutoActive()) { + node.setIsActive(true, false); + } + }, + drop: (tree, node, $event, {from, to}) => { + if (node.data.dropAction !== null) { + node.data.dropAction(tree, node, $event, {from, to}); + } + }, + + }, + }, + allowDrag: (node) => node.data.allowDrag, + allowDrop: (element, {parent, index}) => { + return element.data.allowDropFrom && parent.data.allowDropTo && element.data.id !== parent.data.id; } - if (node.data.routerLink && node.data.allowRouting) { - this._router.navigate([node.data.routerLink]).then(r => null); + }; + + this._sidebar.getError().subscribe( + error => { + this.error = error; + } + ); + } + + static readonly EXPAND_SHOWN_ROUTES: String[] = [ + '/views/monitoring', '/views/config', '/views/uml', '/views/querying/console', + '/views/querying/algebra', '/views/notebooks']; + + @ViewChild('tree', {static: false}) treeComponent: TreeComponent; + nodes = []; + buttons = []; + options; + error; + router; + + protected readonly CatalogState = CatalogState; + + ngOnInit() { + } + + ngAfterViewInit(): void { + const treeModel: TreeModel = this.treeComponent.treeModel; + + // todo 2-way-binding https://angular2-tree.readme.io/docs/save-restore + + $('#search-tree').on('keyup', function (e) { + if (e.which === 27) { // esc + $(this).val(''); } - if (node.data.isAutoExpand()) { - node.toggleExpanded(); - //TREE_ACTIONS.TOGGLE_EXPANDED(tree, node, $event); + treeModel.filterNodes((node) => { + return node.data.name.toLowerCase().includes($(this).val().toLowerCase()); + }); + }); + + this._sidebar.getNodes().subscribe( + nodes => { + this.nodes = nodes; + if (nodes.length === 0) { + this.treeComponent.treeModel.activeNodeIds = {}; + // this.treeComponent.treeModel.setFocusedNode(null); + // this.treeComponent.treeModel.expandedNodeIds = {}; + } + this.expandAll(); } - if (node.data.isAutoActive()) { - node.setIsActive(true, false); + ); + + this._sidebar.getInactiveNode().subscribe( + inactiveNode => { + if (inactiveNode !== null) { + this.treeComponent.treeModel.getNodeById(inactiveNode).setIsActive(false, true); + } } - }, - drop: (tree, node, $event, {from, to}) => { - if (node.data.dropAction !== null) { - node.data.dropAction(tree, node, $event, {from, to}); + ); + + this._sidebar.getResetSubject().subscribe( + collapse => { + if (collapse === true) { + this.reset(); + } else { + this.treeComponent.treeModel.activeNodeIds = {}; + } } - }, - - }, - }, - allowDrag: (node) => node.data.allowDrag, - allowDrop: (element, {parent, index}) => { - return element.data.allowDropFrom && parent.data.allowDropTo && element.data.id !== parent.data.id; - } - }; - - this._sidebar.getError().subscribe( - error => { - this.error = error; - } - ); - } - - static readonly EXPAND_SHOWN_ROUTES: String[] = [ - '/views/monitoring', '/views/config', '/views/uml', '/views/querying/console', - '/views/querying/algebra', '/views/notebooks']; - - @ViewChild('tree', {static: false}) treeComponent: TreeComponent; - nodes = []; - buttons = []; - options; - error; - router; - - protected readonly CatalogState = CatalogState; - - ngOnInit() { - } - - ngAfterViewInit(): void { - const treeModel: TreeModel = this.treeComponent.treeModel; - - // todo 2-way-binding https://angular2-tree.readme.io/docs/save-restore - - $('#search-tree').on('keyup', function (e) { - if (e.which === 27) { // esc - $(this).val(''); - } - treeModel.filterNodes((node) => { - return node.data.name.toLowerCase().includes($(this).val().toLowerCase()); - }); - }); - - this._sidebar.getNodes().subscribe( - nodes => { - this.nodes = nodes; - if (nodes.length === 0) { - this.treeComponent.treeModel.activeNodeIds = {}; - // this.treeComponent.treeModel.setFocusedNode(null); - // this.treeComponent.treeModel.expandedNodeIds = {}; - } - this.expandAll(); - } - ); + ); - this._sidebar.getInactiveNode().subscribe( - inactiveNode => { - if (inactiveNode !== null) { - this.treeComponent.treeModel.getNodeById(inactiveNode).setIsActive(false, true); - } - } - ); - - this._sidebar.getResetSubject().subscribe( - collapse => { - if (collapse === true) { - this.reset(); - } else { - this.treeComponent.treeModel.activeNodeIds = {}; - } + this._sidebar.getTopButtonSubject().subscribe(buttons => this.buttons = buttons); + } + + isExpandAndCollapseShown() { + for (const route of LeftSidebarComponent.EXPAND_SHOWN_ROUTES) { + if (this.router.url.startsWith(route)) { + return false; + } } - ); + return true; + } - this._sidebar.getTopButtonSubject().subscribe(buttons => this.buttons = buttons); - } + expandAll() { + this.treeComponent.treeModel.expandAll(); + } - isExpandAndCollapseShown() { - for (const route of LeftSidebarComponent.EXPAND_SHOWN_ROUTES) { - if (this.router.url.startsWith(route)) { - return false; - } + collapseAll() { + this.treeComponent.treeModel.collapseAll(); } - return true; - } - - expandAll() { - this.treeComponent.treeModel.expandAll(); - } - - collapseAll() { - this.treeComponent.treeModel.collapseAll(); - } - - /** - * Reset tree completely, set all active nodes to inactive, collapse all - */ - reset() { - // from: https://angular2-tree.readme.io/discuss/583cc18bf0f9af0f007218ff - this.treeComponent.treeModel.setFocusedNode(null); - this.treeComponent.treeModel.expandedNodeIds = {}; - this.treeComponent.treeModel.activeNodeIds = {}; - } - - needsButton() { - return this.router.url.startsWith('/views/schema-editing/'); - } - - hasChildren(nodes: any[]): boolean { - for (const node of nodes) { - if (node.children.length > 0) { - return true; - } + + /** + * Reset tree completely, set all active nodes to inactive, collapse all + */ + reset() { + // from: https://angular2-tree.readme.io/discuss/583cc18bf0f9af0f007218ff + this.treeComponent.treeModel.setFocusedNode(null); + this.treeComponent.treeModel.expandedNodeIds = {}; + this.treeComponent.treeModel.activeNodeIds = {}; + } + + needsButton() { + return this.router.url.startsWith('/views/schema-editing/'); + } + + hasChildren(nodes: any[]): boolean { + for (const node of nodes) { + if (node.children.length > 0) { + return true; + } + } + return false; } - return false; - } } diff --git a/src/app/components/left-sidebar/left-sidebar.service.ts b/src/app/components/left-sidebar/left-sidebar.service.ts index 037aa4aa..1853bcbf 100644 --- a/src/app/components/left-sidebar/left-sidebar.service.ts +++ b/src/app/components/left-sidebar/left-sidebar.service.ts @@ -11,257 +11,257 @@ import {SidebarButton} from '../../models/sidebar-button.model'; import {CatalogService} from '../../services/catalog.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) //docs: https://angular2-tree.readme.io/docs/ export class LeftSidebarService { - private readonly _informationService = inject(InformationService); - private readonly _configService = inject(ConfigService); - private readonly _breadcrumb = inject(BreadcrumbService); - public readonly _catalog = inject(CatalogService); + private readonly _informationService = inject(InformationService); + private readonly _configService = inject(ConfigService); + private readonly _breadcrumb = inject(BreadcrumbService); + public readonly _catalog = inject(CatalogService); - @Input() schemaEdit: boolean; - router: Router; - public readonly isVisible: WritableSignal = signal(true); + @Input() schemaEdit: boolean; + router: Router; + public readonly isVisible: WritableSignal = signal(true); - constructor() { + constructor() { - this.nodes.subscribe(nodes => { - if (this.selectedNodeId && !nodes.some(node => node.id === this.selectedNodeId)) { - this.selectedNodeId = null; - } - }); - } + this.nodes.subscribe(nodes => { + if (this.selectedNodeId && !nodes.some(node => node.id === this.selectedNodeId)) { + this.selectedNodeId = null; + } + }); + } - nodes: BehaviorSubject = new BehaviorSubject([]); - error: BehaviorSubject = new BehaviorSubject(null); - //node that should be set inactive: - private inactiveNode: BehaviorSubject = new BehaviorSubject(null); - private resetSubject = new BehaviorSubject(false); - private topButtonSubject = new BehaviorSubject([]); - dataModel: string; - selectedNodeId: any; + nodes: BehaviorSubject = new BehaviorSubject([]); + error: BehaviorSubject = new BehaviorSubject(null); + //node that should be set inactive: + private inactiveNode: BehaviorSubject = new BehaviorSubject(null); + private resetSubject = new BehaviorSubject(false); + private topButtonSubject = new BehaviorSubject([]); + dataModel: string; + selectedNodeId: any; - /** - * Sort function to sort SidebarNodes alphabetically - */ - public sortNodes = (a: SidebarNode, b: SidebarNode) => { - if (a.name < b.name) { - return -1; - } - if (a.name > b.name) { - return 1; - } - return 0; - } + /** + * Sort function to sort SidebarNodes alphabetically + */ + public sortNodes = (a: SidebarNode, b: SidebarNode) => { + if (a.name < b.name) { + return -1; + } + if (a.name > b.name) { + return 1; + } + return 0; + }; - private mapPages(res: Object, mode: string) { - const pages = res; - const nodes: SidebarNode[] = []; - let routerLink = ''; - const labels = new Map(); - const nonLabel: SidebarNode[] = []; - for (const p of pages) { - switch (mode) { - case 'config': - routerLink = '/views/config/' + p.id; - break; - case 'information': - routerLink = '/views/monitoring/' + p.id; - break; - default: - console.error('sidebarNode with unknown group'); - } - if (p.label) { - if (!labels.has(p.label)) { - labels.set(p.label, []); + private mapPages(res: Object, mode: string) { + const pages = res; + const nodes: SidebarNode[] = []; + let routerLink = ''; + const labels = new Map(); + const nonLabel: SidebarNode[] = []; + for (const p of pages) { + switch (mode) { + case 'config': + routerLink = '/views/config/' + p.id; + break; + case 'information': + routerLink = '/views/monitoring/' + p.id; + break; + default: + console.error('sidebarNode with unknown group'); + } + if (p.label) { + if (!labels.has(p.label)) { + labels.set(p.label, []); + } + labels.get(p.label).push(new SidebarNode(p.id, p.name, p.icon, routerLink)); + } else { + nonLabel.push(new SidebarNode(p.id, p.name, p.icon, routerLink)); + } + } + for (const p of nonLabel) { + nodes.push(new SidebarNode(p.id, p.name, p.icon, p.routerLink)); + } + for (const [k, v] of labels) { + nodes.push(new SidebarNode(k, k).asSeparator()); + for (const p of labels.get(k)) { + nodes.push(new SidebarNode(p.id, p.name, p.icon, p.routerLink)); + } } - labels.get(p.label).push(new SidebarNode(p.id, p.name, p.icon, routerLink)); - } else { - nonLabel.push(new SidebarNode(p.id, p.name, p.icon, routerLink)); - } + return nodes; } - for (const p of nonLabel) { - nodes.push(new SidebarNode(p.id, p.name, p.icon, p.routerLink)); + + listInformationManagerPages() { + return this._informationService.getPageList().subscribe({ + next: res => { + this.nodes.next(this.mapPages(res, 'information')); + this.error.next(null); + } + , + error: err => { + this.nodes.next([]); + this.error.next('Could not get page list.'); + console.log(err); + } + }); } - for (const [k, v] of labels) { - nodes.push(new SidebarNode(k, k).asSeparator()); - for (const p of labels.get(k)) { - nodes.push(new SidebarNode(p.id, p.name, p.icon, p.routerLink)); - } + + listConfigManagerPages() { + return this._configService.getPageList().subscribe({ + next: res => { + this.nodes.next(this.mapPages(res, 'config')); + this.error.next(null); + } + , + error: err => { + this.nodes.next([]); + this.error.next('Could not get page list.'); + console.log(err); + } + }); } - return nodes; - } - listInformationManagerPages() { - return this._informationService.getPageList().subscribe({ - next: res => { - this.nodes.next(this.mapPages(res, 'information')); - this.error.next(null); - } - , - error: err => { - this.nodes.next([]); - this.error.next('Could not get page list.'); - console.log(err); - } - }); - } + getNodes() { + return this.nodes; + } - listConfigManagerPages() { - return this._configService.getPageList().subscribe({ - next: res => { - this.nodes.next(this.mapPages(res, 'config')); + setNodes(n: SidebarNode[]) { + n = [].concat(n); // convert to array if it is not an array + this.nodes.next(n); this.error.next(null); - } - , - error: err => { - this.nodes.next([]); - this.error.next('Could not get page list.'); - console.log(err); - } - }); - } + } - getNodes() { - return this.nodes; - } + getError() { + return this.error; + } - setNodes(n: SidebarNode[]) { - n = [].concat(n); // convert to array if it is not an array - this.nodes.next(n); - this.error.next(null); - } + setError(msg: string) { + this.error.next(msg); + this.nodes.next([]); + } - getError() { - return this.error; - } + open() { + this.isVisible.set(true); + } - setError(msg: string) { - this.error.next(msg); - this.nodes.next([]); - } + hide() { + this.isVisible.set(false); + } - open() { - this.isVisible.set(true); - } + close() { + this.nodes.next([]); + this.hide(); + } - hide() { - this.isVisible.set(false); - } + /** + * Reset tree completely, set all active nodes to inactive + * @param collapse collapse tree if true + */ + reset(collapse = false) { + this.resetSubject.next(collapse); + } - close() { - this.nodes.next([]); - this.hide(); - } + getResetSubject() { + return this.resetSubject; + } - /** - * Reset tree completely, set all active nodes to inactive - * @param collapse collapse tree if true - */ - reset(collapse = false) { - this.resetSubject.next(collapse); - } + /** + * Retrieve a schemaTree using the _crud service and apply it to the left sidebar + */ + setSchema(_router: Router, routerLinkRoot: string, views: boolean, depth: number, showTable: boolean, schemaEdit?: boolean, dataModels: DataModel[] = [DataModel.RELATIONAL, DataModel.DOCUMENT, DataModel.GRAPH]) { + this.error.next(null); + const schema = []; + this.router = _router; + for (const s of this._catalog.getSchemaTree(routerLinkRoot, views, depth, schemaEdit, dataModels)) { + schema.push(SidebarNode.fromJson(s)); + } + //Schema editing view - getResetSubject() { - return this.resetSubject; - } + if (this.router.url.startsWith('/views/schema-editing/')) { + //function to define node behavior + const nodeBehavior = (tree, node, $event) => { + if (node.data.routerLink !== '') { + const rLink = [node.data.routerLink]; + const rname = [node.data.id]; + if (node.data.children.length === 0 && node.data.dataModel !== DataModel.GRAPH) { + const url = ['/views/schema-editing/']; + const fullChildLink = (url.concat(rname)); + this._breadcrumb.setBreadcrumbsSchema([ + new BreadcrumbItem('Schema', '/views/schema-editing/'), + new BreadcrumbItem(((node.data.id).split('.'))[0], node.data.routerLink), + new BreadcrumbItem(node.data.name)], node.data.id); + _router.navigate(fullChildLink); + } else { + const fullLink = rLink.concat(rname); + this._breadcrumb.setBreadcrumbs([ + new BreadcrumbItem('Schema', '/views/schema-editing/'), + new BreadcrumbItem(node.data.name)]); + _router.navigate(fullLink); + } + if (node.isCollapsed) { + node.expand(); + } else if (!node.isCollapsed && node.isActive === true) { + node.collapse(); + } + node.setIsActive(true, false); + } else { + node.toggleExpanded(); + } + }; - /** - * Retrieve a schemaTree using the _crud service and apply it to the left sidebar - */ - setSchema(_router: Router, routerLinkRoot: string, views: boolean, depth: number, showTable: boolean, schemaEdit?: boolean, dataModels: DataModel[] = [DataModel.RELATIONAL, DataModel.DOCUMENT, DataModel.GRAPH]) { - this.error.next(null); - const schema = []; - this.router = _router; - for (const s of this._catalog.getSchemaTree(routerLinkRoot, views, depth, schemaEdit, dataModels)) { - schema.push(SidebarNode.fromJson(s)); - } - //Schema editing view + schema.forEach((val: SidebarNode, key) => { + val.routerLink = routerLinkRoot; + val.disableRouting(); + val.setAutoExpand(false); + val.setAction(nodeBehavior); + val.children.forEach((v: SidebarNode, k) => { + v.routerLink = routerLinkRoot + val.id; + v.disableRouting(); + v.setAutoExpand(false); + v.setAction(nodeBehavior); + }); + }); + } + //Uml view + else if (depth === 1) { - if (this.router.url.startsWith('/views/schema-editing/')) { - //function to define node behavior - const nodeBehavior = (tree, node, $event) => { - if (node.data.routerLink !== '') { - const rLink = [node.data.routerLink]; - const rname = [node.data.id]; - if (node.data.children.length === 0 && node.data.dataModel !== DataModel.GRAPH) { - const url = ['/views/schema-editing/']; - const fullChildLink = (url.concat(rname)); - this._breadcrumb.setBreadcrumbsSchema([ - new BreadcrumbItem('Schema', '/views/schema-editing/'), - new BreadcrumbItem(((node.data.id).split('.'))[0], node.data.routerLink), - new BreadcrumbItem(node.data.name)], node.data.id); - _router.navigate(fullChildLink); - } else { - const fullLink = rLink.concat(rname); - this._breadcrumb.setBreadcrumbs([ - new BreadcrumbItem('Schema', '/views/schema-editing/'), - new BreadcrumbItem(node.data.name)]); - _router.navigate(fullLink); - } - if (node.isCollapsed) { - node.expand(); - } else if (!node.isCollapsed && node.isActive === true) { - node.collapse(); - } - node.setIsActive(true, false); - } else { - node.toggleExpanded(); + schema.forEach((val, key) => { + val.routerLink = routerLinkRoot + val.id; + }); } - }; + this.setNodes(schema); - schema.forEach((val: SidebarNode, key) => { - val.routerLink = routerLinkRoot; - val.disableRouting(); - val.setAutoExpand(false); - val.setAction(nodeBehavior); - val.children.forEach((v: SidebarNode, k) => { - v.routerLink = routerLinkRoot + val.id; - v.disableRouting(); - v.setAutoExpand(false); - v.setAction(nodeBehavior); - }); - }); + this.open(); } - //Uml view - else if (depth === 1) { - schema.forEach((val, key) => { - val.routerLink = routerLinkRoot + val.id; - }); + /** + * sets a SidebarNode with id nodeId to inactive + */ + setInactive(nodeId: string) { + this.inactiveNode.next(nodeId); } - this.setNodes(schema); - - this.open(); - } - - /** - * sets a SidebarNode with id nodeId to inactive - */ - setInactive(nodeId: string) { - this.inactiveNode.next(nodeId); - } - /** - * Call this function to subscribe to the BehaviorSubject inactiveNode - */ - getInactiveNode() { - return this.inactiveNode; - } + /** + * Call this function to subscribe to the BehaviorSubject inactiveNode + */ + getInactiveNode() { + return this.inactiveNode; + } - getTopButtonSubject() { - return this.topButtonSubject; - } + getTopButtonSubject() { + return this.topButtonSubject; + } - setTopButtons(buttons: SidebarButton[]) { - this.topButtonSubject.next(buttons); - } + setTopButtons(buttons: SidebarButton[]) { + this.topButtonSubject.next(buttons); + } - toggle() { - this.isVisible.update(vis => !vis); - } + toggle() { + this.isVisible.update(vis => !vis); + } } diff --git a/src/app/components/loading-screen/loading-screen.component.ts b/src/app/components/loading-screen/loading-screen.component.ts index 6ec3f389..4b012068 100644 --- a/src/app/components/loading-screen/loading-screen.component.ts +++ b/src/app/components/loading-screen/loading-screen.component.ts @@ -2,19 +2,19 @@ import {Component, OnInit} from '@angular/core'; import {LoadingScreenService} from './loading-screen.service'; @Component({ - selector: 'app-loading-screen', - templateUrl: './loading-screen.component.html', - styleUrls: ['./loading-screen.component.scss'] + selector: 'app-loading-screen', + templateUrl: './loading-screen.component.html', + styleUrls: ['./loading-screen.component.scss'] }) export class LoadingScreenComponent implements OnInit { - show = false; + show = false; - constructor(private _loading: LoadingScreenService) { - } + constructor(private _loading: LoadingScreenService) { + } - ngOnInit(): void { - this._loading.onVisibilityChange().subscribe(res => this.show = res); - } + ngOnInit(): void { + this._loading.onVisibilityChange().subscribe(res => this.show = res); + } } diff --git a/src/app/components/loading-screen/loading-screen.service.ts b/src/app/components/loading-screen/loading-screen.service.ts index 668b7c3d..74a3c7b7 100644 --- a/src/app/components/loading-screen/loading-screen.service.ts +++ b/src/app/components/loading-screen/loading-screen.service.ts @@ -2,28 +2,28 @@ import {Injectable} from '@angular/core'; import {BehaviorSubject} from 'rxjs'; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class LoadingScreenService { - private isLoading = new BehaviorSubject(false); + private isLoading = new BehaviorSubject(false); - constructor() { - } + constructor() { + } - show() { - this.isLoading.next(true); - } + show() { + this.isLoading.next(true); + } - hide() { - this.isLoading.next(false); - } + hide() { + this.isLoading.next(false); + } - onVisibilityChange() { - return this.isLoading; - } + onVisibilityChange() { + return this.isLoading; + } - isShown() { - return this.isLoading.getValue(); - } + isShown() { + return this.isLoading.getValue(); + } } diff --git a/src/app/components/right-sidebar/right-sidebar.component.ts b/src/app/components/right-sidebar/right-sidebar.component.ts index 1ed9cc30..19d8a294 100644 --- a/src/app/components/right-sidebar/right-sidebar.component.ts +++ b/src/app/components/right-sidebar/right-sidebar.component.ts @@ -4,69 +4,69 @@ import {UntypedFormControl, UntypedFormGroup} from '@angular/forms'; import {RightSidebarToRelationalalgebraService} from '../../services/right-sidebar-to-relationalalgebra.service'; @Component({ - selector: 'app-right-sidebar', - templateUrl: './right-sidebar.component.html', - styleUrls: ['./right-sidebar.component.scss'] + selector: 'app-right-sidebar', + templateUrl: './right-sidebar.component.html', + styleUrls: ['./right-sidebar.component.scss'] }) export class RightSidebarComponent implements OnInit { - private readonly _settings = inject(WebuiSettingsService); - private readonly _RsToRa = inject(RightSidebarToRelationalalgebraService); + private readonly _settings = inject(WebuiSettingsService); + private readonly _RsToRa = inject(RightSidebarToRelationalalgebraService); - @Input() - reload: () => void; - settings: Map = this._settings.getSettings(); - form: UntypedFormGroup; + @Input() + reload: () => void; + settings: Map = this._settings.getSettings(); + form: UntypedFormGroup; - settingsGR = this._settings.getSettingsGR(); - formGR: UntypedFormGroup; - public buttonName = 'connect'; + settingsGR = this._settings.getSettingsGR(); + formGR: UntypedFormGroup; + public buttonName = 'connect'; - constructor() { - const controls = {}; - this.settings.forEach((val: Setting, key: string) => { - controls[key] = new UntypedFormControl(val.value); - }); - this.form = new UntypedFormGroup(controls); + constructor() { + const controls = {}; + this.settings.forEach((val: Setting, key: string) => { + controls[key] = new UntypedFormControl(val.value); + }); + this.form = new UntypedFormGroup(controls); - const controlsGR = {}; - this.settingsGR.forEach((val, key) => { - controlsGR[key] = new UntypedFormControl(val); - }); - this.formGR = new UntypedFormGroup(controlsGR); + const controlsGR = {}; + this.settingsGR.forEach((val, key) => { + controlsGR[key] = new UntypedFormControl(val); + }); + this.formGR = new UntypedFormGroup(controlsGR); - } + } - ngOnInit() { - } + ngOnInit() { + } - saveSettings() { - this.settings.forEach((val, key) => { - this._settings.setSetting(key, this.form.value[key]); - }); - location.reload(); - } + saveSettings() { + this.settings.forEach((val, key) => { + this._settings.setSetting(key, this.form.value[key]); + }); + location.reload(); + } - resetSettings() { - this._settings.reset(); - return false; - } + resetSettings() { + this._settings.reset(); + return false; + } - saveSettingsGR() { - this.settingsGR.forEach((val, key) => { - this._settings.setSettingGR(key, this.formGR.value[key]); - }); - location.reload(); - } + saveSettingsGR() { + this.settingsGR.forEach((val, key) => { + this._settings.setSettingGR(key, this.formGR.value[key]); + }); + location.reload(); + } - public connectToRA(): void { - if (this.buttonName === 'connect') { - this.buttonName = 'disconnect'; - } else { - this.buttonName = 'connect'; + public connectToRA(): void { + if (this.buttonName === 'connect') { + this.buttonName = 'disconnect'; + } else { + this.buttonName = 'connect'; + } + this._RsToRa.toggle(); } - this._RsToRa.toggle(); - } } diff --git a/src/app/components/toast-exposer/toast-exposer.component.scss b/src/app/components/toast-exposer/toast-exposer.component.scss index 373cf916..e79c7f57 100644 --- a/src/app/components/toast-exposer/toast-exposer.component.scss +++ b/src/app/components/toast-exposer/toast-exposer.component.scss @@ -1,5 +1,4 @@ - #stackTraceModal { h5 { margin: 0; diff --git a/src/app/components/toast-exposer/toast-exposer.component.ts b/src/app/components/toast-exposer/toast-exposer.component.ts index dec75f8d..34dab17c 100644 --- a/src/app/components/toast-exposer/toast-exposer.component.ts +++ b/src/app/components/toast-exposer/toast-exposer.component.ts @@ -4,37 +4,37 @@ import {ToasterComponent, ToasterPlacement} from '@coreui/angular'; import {ToastComponent} from './toast/toast.component'; @Component({ - selector: 'app-toast-exposer', - templateUrl: './toast-exposer.component.html', - styleUrls: ['./toast-exposer.component.scss'] + selector: 'app-toast-exposer', + templateUrl: './toast-exposer.component.html', + styleUrls: ['./toast-exposer.component.scss'] }) export class ToastExposerComponent implements OnInit { - placement = ToasterPlacement.TopEnd; + placement = ToasterPlacement.TopEnd; - @ViewChild(ToasterComponent) toaster!: ToasterComponent; + @ViewChild(ToasterComponent) toaster!: ToasterComponent; - private readonly _toaster = inject(ToasterService); + private readonly _toaster = inject(ToasterService); - constructor() { - this._toaster.toasts.subscribe({ - next: toast => { - const options = { - title: toast.title, - delay: 5000, - placement: this.placement, - color: toast.type, - autohide: true - }; - const componentRef: ComponentRef = this.toaster.addToast(ToastComponent, {...options}); - componentRef.instance.toast.next(toast); - } - }); - } + constructor() { + this._toaster.toasts.subscribe({ + next: toast => { + const options = { + title: toast.title, + delay: 5000, + placement: this.placement, + color: toast.type, + autohide: true + }; + const componentRef: ComponentRef = this.toaster.addToast(ToastComponent, {...options}); + componentRef.instance.toast.next(toast); + } + }); + } - ngOnInit() { - } + ngOnInit() { + } } diff --git a/src/app/components/toast-exposer/toast/toast.component.ts b/src/app/components/toast-exposer/toast/toast.component.ts index 66d6bdcd..03e83690 100644 --- a/src/app/components/toast-exposer/toast/toast.component.ts +++ b/src/app/components/toast-exposer/toast/toast.component.ts @@ -8,57 +8,57 @@ import {ToastComponent as ToastParent, ToasterService} from '@coreui/angular'; import {BehaviorSubject} from 'rxjs'; @Component({ - selector: 'app-toast', - templateUrl: './toast.component.html', - styleUrls: ['./toast.component.scss'], - providers: [{provide: ToastParent, useExisting: forwardRef(() => ToastComponent)}] + selector: 'app-toast', + templateUrl: './toast.component.html', + styleUrls: ['./toast.component.scss'], + providers: [{provide: ToastParent, useExisting: forwardRef(() => ToastComponent)}] }) export class ToastComponent extends ToastParent { - @Input() closeButton = true; - @Input() title = ''; + @Input() closeButton = true; + @Input() title = ''; - exception: ResultException; - @ViewChild('stackTraceModal', {static: false}) public stackTraceModal: ModalDirective; + exception: ResultException; + @ViewChild('stackTraceModal', {static: false}) public stackTraceModal: ModalDirective; - public toast: BehaviorSubject = new BehaviorSubject(null); + public toast: BehaviorSubject = new BehaviorSubject(null); - constructor( - public override hostElement: ElementRef, - public override renderer: Renderer2, - public override toasterService: ToasterService, - public override changeDetectorRef: ChangeDetectorRef) { - super(hostElement, renderer, toasterService, changeDetectorRef); - } + constructor( + public override hostElement: ElementRef, + public override renderer: Renderer2, + public override toasterService: ToasterService, + public override changeDetectorRef: ChangeDetectorRef) { + super(hostElement, renderer, toasterService, changeDetectorRef); + } - ngOnInit(): void { - } + ngOnInit(): void { + } - /** - * show newest toasts before the older ones - */ - orderToasts(a: KeyValue, b: KeyValue) { - return b.value.time - a.value.time; - } + /** + * show newest toasts before the older ones + */ + orderToasts(a: KeyValue, b: KeyValue) { + return b.value.time - a.value.time; + } - getToastClass(toast: Toast) { - let cssClass = toast.type; - if (toast.exception) { - cssClass = cssClass + ' exception'; + getToastClass(toast: Toast) { + let cssClass = toast.type; + if (toast.exception) { + cssClass = cssClass + ' exception'; + } + return cssClass; } - return cssClass; - } - showStackTraceModal(toast: Toast) { - if (toast.exception) { - this.exception = toast.exception; - this.stackTraceModal.show(); + showStackTraceModal(toast: Toast) { + if (toast.exception) { + this.exception = toast.exception; + this.stackTraceModal.show(); + } } - } - copyGeneratedQuery(toast: Toast) { - //this._util.clipboard(toast.generatedQuery); - } + copyGeneratedQuery(toast: Toast) { + //this._util.clipboard(toast.generatedQuery); + } } diff --git a/src/app/components/toast-exposer/toaster.model.ts b/src/app/components/toast-exposer/toaster.model.ts index a956e61c..a6d51c1e 100644 --- a/src/app/components/toast-exposer/toaster.model.ts +++ b/src/app/components/toast-exposer/toaster.model.ts @@ -1,45 +1,45 @@ import {ResultException} from '../data-view/models/result-set.model'; export class Toast { - title: string; - message: string; - generatedQuery: string; - delay: number; - timeAsString: String;//timeAsString when toast is shown, for the gui - time: Date; - type: String; - hash: string; - exception: ResultException; + title: string; + message: string; + generatedQuery: string; + delay: number; + timeAsString: String;//timeAsString when toast is shown, for the gui + time: Date; + type: String; + hash: string; + exception: ResultException; - /** - * A toast message - * @param title title of the message - * @param message message - * @param generatedQuery Generated query - * @param delay After how many seconds the message should fade out. The message will be displayed permanently if delay = 0 - * @param type Set the type of the message, e.g. 'bg-success', 'bg-warning', 'bg-danger' - */ - constructor(title: string, message: string, generatedQuery: string, delay: number = 0, type: String = '') { - this.title = title; - this.message = message; - this.generatedQuery = generatedQuery; - const d = new Date(); - this.time = d; - this.timeAsString = this.twoDigits(d.getHours()) + ':' + this.twoDigits(d.getMinutes()) + ':' + this.twoDigits(d.getSeconds()); - this.type = type; - this.delay = delay;//default 0 -> not removed automatically. if > 0: removed after n miliseconds - this.hash = this.timeAsString + this.message; - } + /** + * A toast message + * @param title title of the message + * @param message message + * @param generatedQuery Generated query + * @param delay After how many seconds the message should fade out. The message will be displayed permanently if delay = 0 + * @param type Set the type of the message, e.g. 'bg-success', 'bg-warning', 'bg-danger' + */ + constructor(title: string, message: string, generatedQuery: string, delay: number = 0, type: String = '') { + this.title = title; + this.message = message; + this.generatedQuery = generatedQuery; + const d = new Date(); + this.time = d; + this.timeAsString = this.twoDigits(d.getHours()) + ':' + this.twoDigits(d.getMinutes()) + ':' + this.twoDigits(d.getSeconds()); + this.type = type; + this.delay = delay;//default 0 -> not removed automatically. if > 0: removed after n miliseconds + this.hash = this.timeAsString + this.message; + } - twoDigits(n: number): string { - const s = n.toString(); - if (s.length === 1) { - return '0' + s; + twoDigits(n: number): string { + const s = n.toString(); + if (s.length === 1) { + return '0' + s; + } + return s; } - return s; - } - setException(e: ResultException) { - this.exception = e; - } + setException(e: ResultException) { + this.exception = e; + } } diff --git a/src/app/components/toast-exposer/toaster.service.ts b/src/app/components/toast-exposer/toaster.service.ts index 03e68ddc..cc7c2a39 100644 --- a/src/app/components/toast-exposer/toaster.service.ts +++ b/src/app/components/toast-exposer/toaster.service.ts @@ -4,108 +4,108 @@ import {Subject} from 'rxjs'; import {Toast} from './toaster.model'; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class ToasterService { - public toasts: Subject = new Subject(); + public toasts: Subject = new Subject(); - constructor() { - } - - /** - * Generate a toast message - * @param title Title of the message - * @param message Message - * @param generatedQuery Generated query - * @param delay After how many seconds the message should fade out. The message will be displayed permanently if delay = 0 - * @param type Set the type of the message, e.g. 'bg-success', 'bg-warning', 'bg-danger' - */ - private generateToast(title: string, message: string, generatedQuery: string, delay: number, type: String = '') { - const t: Toast = new Toast(title, message, generatedQuery, delay, type); - this.toasts.next(t); - } - - /** - * Generate a success toast message - * @param message Message - * @param generatedQuery Generated query that can be copied to the clipboard - * @param title Title of the message, default: 'success'. If null, it will be set to 'success' - * @param duration Optional. Set the duration of the toast message. Default: NORMAL - */ - success(message: string, generatedQuery: string = null, title = 'success', duration: ToastDuration = ToastDuration.NORMAL) { - if (!title) { - title = 'success'; + constructor() { } - this.generateToast(title, message, generatedQuery, duration.valueOf(), 'bg-success'); - } - /** - * Generate an info toast message - * @param message Message - * @param generatedQuery Generated query that can be copied to the clipboard - * @param title Title of the message, default: 'info'. If null, it will be set to 'info' - * @param duration Optional. Set the duration of the toast message. Default: NORMAL - */ - info(message: string, generatedQuery: string = null, title = 'info', duration: ToastDuration = ToastDuration.NORMAL) { - if (!title) { - title = 'info'; + /** + * Generate a toast message + * @param title Title of the message + * @param message Message + * @param generatedQuery Generated query + * @param delay After how many seconds the message should fade out. The message will be displayed permanently if delay = 0 + * @param type Set the type of the message, e.g. 'bg-success', 'bg-warning', 'bg-danger' + */ + private generateToast(title: string, message: string, generatedQuery: string, delay: number, type: String = '') { + const t: Toast = new Toast(title, message, generatedQuery, delay, type); + this.toasts.next(t); } - this.generateToast(title, message, generatedQuery, duration.valueOf(), 'bg-info'); - } - /** - * Generate a warning toast message. Use this method for errors caught by the UI. - * @param message Message - * @param title Title of the message, default: 'warning'. If null, it will be set to 'warning' - * @param duration Optional. Set the duration of the toast message. Default LONG - */ - warn(message: string, title = 'warning', duration: ToastDuration = ToastDuration.LONG) { - if (!title) { - title = 'warning'; + /** + * Generate a success toast message + * @param message Message + * @param generatedQuery Generated query that can be copied to the clipboard + * @param title Title of the message, default: 'success'. If null, it will be set to 'success' + * @param duration Optional. Set the duration of the toast message. Default: NORMAL + */ + success(message: string, generatedQuery: string = null, title = 'success', duration: ToastDuration = ToastDuration.NORMAL) { + if (!title) { + title = 'success'; + } + this.generateToast(title, message, generatedQuery, duration.valueOf(), 'bg-success'); } - this.generateToast(title, message, null, duration.valueOf(), 'bg-warning'); - } - /** - * Generate a error toast message. Use this method for uncaught errors from the backend. - * @param message Message - * @param title Title of the message, default: 'error'. If null, it will be set to 'error' - * @param duration Optional. Set the duration of the toast message. Default LONG - */ - error(message: string, title = 'error', duration: ToastDuration = ToastDuration.LONG) { - if (!title) { - title = 'error'; + /** + * Generate an info toast message + * @param message Message + * @param generatedQuery Generated query that can be copied to the clipboard + * @param title Title of the message, default: 'info'. If null, it will be set to 'info' + * @param duration Optional. Set the duration of the toast message. Default: NORMAL + */ + info(message: string, generatedQuery: string = null, title = 'info', duration: ToastDuration = ToastDuration.NORMAL) { + if (!title) { + title = 'info'; + } + this.generateToast(title, message, generatedQuery, duration.valueOf(), 'bg-info'); } - this.generateToast(title, message, null, duration.valueOf(), 'bg-danger'); - } - /** - * Generate an warning toast message. Use this method for ResultSets containing an error message (and optionally an exception with Stacktrace) - * If the ResultSet contains a StackTrace, it will appear in a modal when clicking on the toast message - * @param result ResultSet with the error message - * @param message Additional message to the exception message (optional) - * @param title Title of the message, default: 'error'. If null, it will be set to 'error' - * @param duration Optional. Set the duration of the toast message. Default LONG - */ - exception(result: Result, message: string = null, title = 'error', duration = ToastDuration.LONG) { - let msg = result.error; - if (message) { - if (message.endsWith(' ')) { - msg = message + msg; - } else { - msg = message + ' ' + msg; - } + /** + * Generate a warning toast message. Use this method for errors caught by the UI. + * @param message Message + * @param title Title of the message, default: 'warning'. If null, it will be set to 'warning' + * @param duration Optional. Set the duration of the toast message. Default LONG + */ + warn(message: string, title = 'warning', duration: ToastDuration = ToastDuration.LONG) { + if (!title) { + title = 'warning'; + } + this.generateToast(title, message, null, duration.valueOf(), 'bg-warning'); } - if (!title) { - title = 'error'; + + /** + * Generate a error toast message. Use this method for uncaught errors from the backend. + * @param message Message + * @param title Title of the message, default: 'error'. If null, it will be set to 'error' + * @param duration Optional. Set the duration of the toast message. Default LONG + */ + error(message: string, title = 'error', duration: ToastDuration = ToastDuration.LONG) { + if (!title) { + title = 'error'; + } + this.generateToast(title, message, null, duration.valueOf(), 'bg-danger'); } - const t: Toast = new Toast(title, msg, result.query, duration, 'bg-warning'); - if (result.exception) { - t.setException(result.exception); + + /** + * Generate an warning toast message. Use this method for ResultSets containing an error message (and optionally an exception with Stacktrace) + * If the ResultSet contains a StackTrace, it will appear in a modal when clicking on the toast message + * @param result ResultSet with the error message + * @param message Additional message to the exception message (optional) + * @param title Title of the message, default: 'error'. If null, it will be set to 'error' + * @param duration Optional. Set the duration of the toast message. Default LONG + */ + exception(result: Result, message: string = null, title = 'error', duration = ToastDuration.LONG) { + let msg = result.error; + if (message) { + if (message.endsWith(' ')) { + msg = message + msg; + } else { + msg = message + ' ' + msg; + } + } + if (!title) { + title = 'error'; + } + const t: Toast = new Toast(title, msg, result.query, duration, 'bg-warning'); + if (result.exception) { + t.setException(result.exception); + } + this.toasts.next(t); } - this.toasts.next(t); - } } @@ -117,8 +117,8 @@ export class ToasterService { * LONG: a longer message, default for warning and error messages */ export enum ToastDuration { - INFINITE = 0, - SHORT = 2, - NORMAL = 5, - LONG = 10 + INFINITE = 0, + SHORT = 2, + NORMAL = 5, + LONG = 10 } diff --git a/src/app/containers/default-layout/default-layout.component.ts b/src/app/containers/default-layout/default-layout.component.ts index ebf2bee6..fb92c94f 100644 --- a/src/app/containers/default-layout/default-layout.component.ts +++ b/src/app/containers/default-layout/default-layout.component.ts @@ -9,85 +9,85 @@ import {freeSet} from '@coreui/icons'; import {WebuiSettingsService} from '../../services/webui-settings.service'; @Component({ - selector: 'app-dashboard', - templateUrl: './default-layout.component.html', - styleUrls: ['./default-layout.component.scss'] + selector: 'app-dashboard', + templateUrl: './default-layout.component.html', + styleUrls: ['./default-layout.component.scss'] }) export class DefaultLayoutComponent implements OnDestroy, AfterContentChecked { - public navItems = navItems; - public sidebarMinimized = true; - private changes: MutationObserver; - public element: HTMLElement; - icons = freeSet; - hover = signal(false); - modal = signal(false); - - - constructor( - public _sidebar: LeftSidebarService, - public _information: InformationService, - public _settings: WebuiSettingsService, - public _crud: CrudService, - public _plugin: PluginService, - public _left: LeftSidebarService, - private changeDetector: ChangeDetectorRef, - @Inject(DOCUMENT) _document?: any, - ) { - - this.changes = new MutationObserver(() => { - this.sidebarMinimized = _document.body.classList.contains('sidebar-minimized'); - }); - this.element = _document.body; - this.changes.observe(this.element, { - attributes: true, - attributeFilter: ['class'] - }); - - } - - ngAfterContentChecked(): void { - this.changeDetector.detectChanges(); - } - - ngOnDestroy(): void { - this.changes.disconnect(); - } - - getConnectedColor() { - return this._information.connected ? 'success' : 'danger'; - } - - exploreByExampleEnabled() { - return this._plugin.getEnabledPlugins().includes('explore-by-example'); - - } - - toggleDropdown() { - console.log('enter'); - } - - getConnectionText() { - return this._information.connected ? 'Connected to
' + this._settings.getConnection('crud.rest') + '' : 'Disconnected'; - } - - isConnected() { - return this._information.connected; - } - - reconnect() { - this._information.manualReconnect(); - console.log('reconnecting'); - } - - getConnectedSymbol() { - return this._information.connected ? 'cil-check' : (this.hover ? 'cil-sync' : 'cil-warning'); - } - - openSettings() { - this.modal.set(true); - } - - handleModalChange($event: boolean) { - this.modal.set($event); - } + public navItems = navItems; + public sidebarMinimized = true; + private changes: MutationObserver; + public element: HTMLElement; + icons = freeSet; + hover = signal(false); + modal = signal(false); + + + constructor( + public _sidebar: LeftSidebarService, + public _information: InformationService, + public _settings: WebuiSettingsService, + public _crud: CrudService, + public _plugin: PluginService, + public _left: LeftSidebarService, + private changeDetector: ChangeDetectorRef, + @Inject(DOCUMENT) _document?: any, + ) { + + this.changes = new MutationObserver(() => { + this.sidebarMinimized = _document.body.classList.contains('sidebar-minimized'); + }); + this.element = _document.body; + this.changes.observe(this.element, { + attributes: true, + attributeFilter: ['class'] + }); + + } + + ngAfterContentChecked(): void { + this.changeDetector.detectChanges(); + } + + ngOnDestroy(): void { + this.changes.disconnect(); + } + + getConnectedColor() { + return this._information.connected ? 'success' : 'danger'; + } + + exploreByExampleEnabled() { + return this._plugin.getEnabledPlugins().includes('explore-by-example'); + + } + + toggleDropdown() { + console.log('enter'); + } + + getConnectionText() { + return this._information.connected ? 'Connected to
' + this._settings.getConnection('crud.rest') + '' : 'Disconnected'; + } + + isConnected() { + return this._information.connected; + } + + reconnect() { + this._information.manualReconnect(); + console.log('reconnecting'); + } + + getConnectedSymbol() { + return this._information.connected ? 'cil-check' : (this.hover ? 'cil-sync' : 'cil-warning'); + } + + openSettings() { + this.modal.set(true); + } + + handleModalChange($event: boolean) { + this.modal.set($event); + } } 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 index e8cdb532..60dd4eab 100644 --- a/src/app/explain-visualizer/components/plan-node/plan-node.component.ts +++ b/src/app/explain-visualizer/components/plan-node/plan-node.component.ts @@ -8,251 +8,251 @@ 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'], + 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(); + // 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) { } - if (this.currentCompactView !== this.viewOptions.showCompactView) { - this.currentCompactView = this.viewOptions.showCompactView; - this.calculateBar(); - } + ngOnInit() { + this.currentHighlightType = this.viewOptions.highlightType; + this.calculateBar(); + this.calculateProps(); + this.calculateDuration(); + this.calculateTags(); - if (this.currentExpandedView !== this.showDetails) { - this.currentExpandedView = this.showDetails; - this.calculateBar(); + this.plannerRowEstimateDirection = this.node[this._planService.PLANNER_ESTIMATE_DIRECTION]; + this.plannerRowEstimateValue = _.round(this.node[this._planService.PLANNER_ESTIMATE_FACTOR], 1); } - } - getFormattedQuery() { - const keyItems: Array = []; + ngDoCheck() { + if (this.currentHighlightType !== this.viewOptions.highlightType) { + this.currentHighlightType = this.viewOptions.highlightType; + this.calculateBar(); + } - // 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] + ' '); - } + if (this.currentCompactView !== this.viewOptions.showCompactView) { + this.currentCompactView = this.viewOptions.showCompactView; + this.calculateBar(); + } - // 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(',')); + if (this.currentExpandedView !== this.showDetails) { + this.currentExpandedView = this.showDetails; + this.calculateBar(); + } } - // 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(')', '')); - } + getFormattedQuery() { + const keyItems: Array = []; - 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; - } + // 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] + ' '); + } - // expanded view width trumps others - if (this.currentExpandedView) { - this.barContainerWidth = this.EXPANDED_WIDTH; + // 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; + } - 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); + // expanded view width trumps others + if (this.currentExpandedView) { + this.barContainerWidth = this.EXPANDED_WIDTH; } - 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); + + + 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; } - 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); + + if (this.barWidth < 0) { + this.barWidth = 0; } - break; - } - if (this.barWidth < 0) { - this.barWidth = 0; + this.backgroundColor = this._colorService.numberToColorHsl(1 - this.barWidth / this.barContainerWidth); } - 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); + calculateDuration() { + this.executionTimePercent = (_.round((this.node[this._planService.ACTUAL_DURATION_PROP] / this.plan.planStats.executionTime) * 100)); } - 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); + + // 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(); } - } - getNodeTypeDescription() { - return this._helpService.getNodeTypeDescription(this.node[this._planService.NODE_TYPE_PROP]); - } + 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); + } + } - 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, ''); + getNodeTypeDescription() { + return this._helpService.getNodeTypeDescription(this.node[this._planService.NODE_TYPE_PROP]); } - // return (this.node[this._planService.NODE_TYPE_PROP]).toUpperCase(); - return (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, ''); + } - getTagName(tagName: String) { - if (this.viewOptions.viewMode === ViewMode.DOT && !this.showDetails) { - return tagName.charAt(0); + // return (this.node[this._planService.NODE_TYPE_PROP]).toUpperCase(); + return (this.node[this._planService.NODE_TYPE_PROP]); } - return tagName; - } - shouldShowPlannerEstimate() { - if (this.viewOptions.showPlannerEstimate && this.showDetails) { - return true; + getTagName(tagName: String) { + if (this.viewOptions.viewMode === ViewMode.DOT && !this.showDetails) { + return tagName.charAt(0); + } + return tagName; } - if (this.viewOptions.viewMode === ViewMode.DOT) { - return false; - } + shouldShowPlannerEstimate() { + if (this.viewOptions.showPlannerEstimate && this.showDetails) { + return true; + } - return this.viewOptions.showPlannerEstimate; - } + if (this.viewOptions.viewMode === ViewMode.DOT) { + return false; + } - shouldShowNodeBarLabel() { - if (this.showDetails) { - return true; + return this.viewOptions.showPlannerEstimate; } - if (this.viewOptions.viewMode === ViewMode.DOT) { - return false; - } + shouldShowNodeBarLabel() { + if (this.showDetails) { + return true; + } - return true; - } + if (this.viewOptions.viewMode === ViewMode.DOT) { + return false; + } + + return true; + } - getDataModelShort() { - return this.node[this._planService.MODEL].toUpperCase().substring(0, 1); - } + getDataModelShort() { + return this.node[this._planService.MODEL].toUpperCase().substring(0, 1); + } } 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 index 9488be9c..71c65465 100644 --- a/src/app/explain-visualizer/components/plan-view/plan-view.component.ts +++ b/src/app/explain-visualizer/components/plan-view/plan-view.component.ts @@ -4,61 +4,61 @@ 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'], + 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; + 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 - }; + viewOptions: any = { + showPlanStats: true, + showHighlightBar: true, + showPlannerEstimate: false, + showTags: true, + highlightType: HighlightType.NONE, + viewMode: ViewMode.FULL + }; - showPlannerEstimate = true; + showPlannerEstimate = true; - highlightTypes = HighlightType; // exposing the enum to the view - viewModes = ViewMode; + highlightTypes = HighlightType; // exposing the enum to the view + viewModes = ViewMode; - constructor(private _planService: PlanService) { + constructor(private _planService: PlanService) { - } + } - initPlan() { - this.plan = this._planService.createPlan('name', JSON.parse(this.planObject), this.query); + 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 - }; - } + 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(); - } + ngOnInit() { + this.initPlan(); + } - toggleHighlight(type: HighlightType) { - this.viewOptions.highlightType = type; - } + toggleHighlight(type: HighlightType) { + this.viewOptions.highlightType = type; + } - analyzePlan() { - this._planService.analyzePlan(this.plan); - } + 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 index 35d8a161..09212d10 100644 --- a/src/app/explain-visualizer/explain-visualizer.module.ts +++ b/src/app/explain-visualizer/explain-visualizer.module.ts @@ -8,34 +8,34 @@ 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"; +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 - ] + 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 index f558b7fb..85d9ddd9 100644 --- a/src/app/explain-visualizer/models/enums.ts +++ b/src/app/explain-visualizer/models/enums.ts @@ -1,18 +1,18 @@ export class HighlightType { - static NONE = 'none'; - static CPU = 'cpu cost'; - static ROWS = 'row cost'; - static COST = 'io cost'; + static NONE = 'none'; + static CPU = 'cpu cost'; + static ROWS = 'row cost'; + static COST = 'io cost'; } export enum EstimateDirection { - over, - under, - equal + over, + under, + equal } export class ViewMode { - static FULL = 'full'; - static COMPACT = 'compact'; - static DOT = 'dot'; + 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 index 37ae207d..a17e0b23 100644 --- a/src/app/explain-visualizer/models/iplan.ts +++ b/src/app/explain-visualizer/models/iplan.ts @@ -1,17 +1,17 @@ export class IPlan { - id: string; - name: string; - content: any; - query: string; - createdOn: Date; - planStats: any; - formattedQuery: string; + 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; - } + 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 index 4bb8a8d8..b37f30f9 100644 --- a/src/app/explain-visualizer/pipes.ts +++ b/src/app/explain-visualizer/pipes.ts @@ -4,43 +4,43 @@ import * as moment from 'moment'; @Pipe({name: 'momentDate'}) export class MomentDatePipe implements PipeTransform { - transform(value: string, args: string[]): any { - return moment(value).format('LLL'); - } + 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 = ''; + 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(); + 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; } - return duration; - } } @Pipe({name: 'durationUnit'}) export class DurationUnitPipe implements PipeTransform { - transform(value: number) { - let unit = ''; + 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'; + 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; } - return unit; - } } diff --git a/src/app/explain-visualizer/services/color.service.ts b/src/app/explain-visualizer/services/color.service.ts index e62cab8b..fba28340 100644 --- a/src/app/explain-visualizer/services/color.service.ts +++ b/src/app/explain-visualizer/services/color.service.ts @@ -1,65 +1,65 @@ import {Injectable} from '@angular/core'; @Injectable({ - providedIn: 'root' + 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; + /** + * 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)]; - } + 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); + } - hue2rgb(p, q, t) { - if (t < 0) { - t += 1; - } - if (t > 1) { - t -= 1; - } - if (t < 1 / 6) { - return p + (q - p) * 6 * t; + return [Math.floor(r * 255), Math.floor(g * 255), Math.floor(b * 255)]; } - if (t < 1 / 2) { - return q; - } - if (t < 2 / 3) { - return p + (q - p) * (2 / 3 - t) * 6; + + 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; } - 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] + ')'; - } + // 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 index a5a7b5ae..aab4507d 100644 --- a/src/app/explain-visualizer/services/help.service.ts +++ b/src/app/explain-visualizer/services/help.service.ts @@ -1,36 +1,36 @@ import {Injectable} from '@angular/core'; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class HelpService { - getNodeTypeDescription(nodeType: string) { - return NODE_DESCRIPTIONS[nodeType.toUpperCase()]; - } + 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 + '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 + '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 + '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 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 + '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 + '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. + '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 + '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 index f040698b..245f043a 100644 --- a/src/app/explain-visualizer/services/plan.service.ts +++ b/src/app/explain-visualizer/services/plan.service.ts @@ -5,223 +5,223 @@ import * as _ from 'lodash'; import {EstimateDirection} from '../models/enums'; @Injectable({ - providedIn: 'root' + 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])); - } + // 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(); } - 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; + getPlan(id: string): IPlan { + return JSON.parse(localStorage.getItem(id)); } - 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(); - } + 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; + } - // recursively walk down the plan to compute various metrics - processNode(node) { - this.calculatePlannerEstimate(node); - this.calculateActuals(node); + isJsonString(str) { + try { + JSON.parse(str); + } catch (e) { + return false; + } + return true; + } - _.each(node, (value, key) => { - this.calculateMaximums(node, key, value); + 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; - if (key === this.PLANS_PROP) { - _.each(value, (val) => { - this.processNode(val); - }); - } - }); - } + this.findOutlierNodes(plan.content.Plan); - 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; + + deletePlan(plan: IPlan) { + localStorage.removeItem(plan.id); } - if (key === this.ACTUAL_CPU_PROP && this._maxCpu < value) { - this._maxCpu = value; + + deleteAllPlans() { + localStorage.clear(); } - } - findOutlierNodes(node) { - node[this.SLOWEST_NODE_PROP] = false; - node[this.LARGEST_NODE_PROP] = false; - node[this.COSTLIEST_NODE_PROP] = false; + // recursively walk down the plan to compute various metrics + processNode(node) { + this.calculatePlannerEstimate(node); + this.calculateActuals(node); - 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; + _.each(node, (value, key) => { + this.calculateMaximums(node, key, value); + + if (key === this.PLANS_PROP) { + _.each(value, (val) => { + this.processNode(val); + }); + } + }); } - if (node[this.ACTUAL_CPU_PROP] === this._maxCpu && this._maxCpu > 0) { - node[this.MOST_CPU_NODE_PROP] = true; + + 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; + } } - _.each(node, (value, key) => { - if (key === this.PLANS_PROP) { - _.each(value, (val) => { - this.findOutlierNodes(val); + 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; + // 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]; } - 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]; + // 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 index a90f80d4..73d8911a 100644 --- a/src/app/explain-visualizer/services/syntax-highlight.service.ts +++ b/src/app/explain-visualizer/services/syntax-highlight.service.ts @@ -3,189 +3,189 @@ import * as _ from 'lodash'; import hljs from 'highlight.js'; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class SyntaxHighlightService { - OPEN_TAG = ' _OPEN_TAG_'; - CLOSE_TAG = '_CLOSE_TAG_'; + OPEN_TAG = ' _OPEN_TAG_'; + CLOSE_TAG = '_CLOSE_TAG_'; - highlight(code: string, keyItems: Array) { - hljs.registerLanguage('sql', LANG_SQL); - /*hljs.configure({ - tabReplace: ' ' - });*/ + 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}`); - }); + // 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'), ''); + 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; - } + 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' - }, + const COMMENT_MODE = hljs.COMMENT('--', '$'); + return { + case_insensitive: true, + illegal: /[<>{}*]/, 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 + { + 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 ] - }, - hljs.C_BLOCK_COMMENT_MODE, - COMMENT_MODE - ] - }; + }; }; diff --git a/src/app/models/catalog.model.ts b/src/app/models/catalog.model.ts index 766fe355..de98b52c 100644 --- a/src/app/models/catalog.model.ts +++ b/src/app/models/catalog.model.ts @@ -3,38 +3,38 @@ import {PolyType} from '../components/data-view/models/result-set.model'; import {AdapterModel, AdapterType, PartitionType, PlacementType} from '../views/adapters/adapter.model'; export enum CatalogState { - INIT, - LOADING, - UP_TO_DATE + INIT, + LOADING, + UP_TO_DATE } export class IdEntity { - id: number; - name: string; + id: number; + name: string; - constructor(id: number, name: string) { - this.id = id; - this.name = name; - } + constructor(id: number, name: string) { + this.id = id; + this.name = name; + } } // tslint:disable-next-line:no-empty-interface export class NamespaceModel extends IdEntity { - dataModel: DataModel; - caseSensitive: boolean; + dataModel: DataModel; + caseSensitive: boolean; } export class EntityModel extends IdEntity { - namespaceId: number; - dataModel: DataModel; - entityType: EntityType; - modifiable: boolean; + namespaceId: number; + dataModel: DataModel; + entityType: EntityType; + modifiable: boolean; } // tslint:disable-next-line:no-empty-interface export interface TableModel extends EntityModel { - primaryKey: number; + primaryKey: number; } // tslint:disable-next-line:no-empty-interface @@ -49,151 +49,151 @@ export interface GraphModel extends EntityModel { // tslint:disable-next-line:no-empty-interface export interface FieldModel extends IdEntity { - entityId: number; + entityId: number; } export interface ColumnModel extends FieldModel { - type: PolyType; - collectionsType: PolyType; - nullable: boolean; - position: number; - unique: boolean; - precision: number; - scale: number; - defaultValue: string; - dimension: number; - cardinality: number; + type: PolyType; + collectionsType: PolyType; + nullable: boolean; + position: number; + unique: boolean; + precision: number; + scale: number; + defaultValue: string; + dimension: number; + cardinality: number; } export interface LogicalSnapshotModel { - id: number; - namespaces: NamespaceModel[]; - entities: EntityModel[]; - fields: FieldModel[]; - keys: KeyModel[]; - constraints: ConstraintModel[]; - placements: AllocationPlacementModel[]; - partitions: AllocationPartitionModel[]; - allocations: AllocationEntityModel[]; - allocColumns: AllocationColumnModel[]; - adapters: AdapterModel[]; - adapterTemplates: AdapterTemplateModel[]; + id: number; + namespaces: NamespaceModel[]; + entities: EntityModel[]; + fields: FieldModel[]; + keys: KeyModel[]; + constraints: ConstraintModel[]; + placements: AllocationPlacementModel[]; + partitions: AllocationPartitionModel[]; + allocations: AllocationEntityModel[]; + allocColumns: AllocationColumnModel[]; + adapters: AdapterModel[]; + adapterTemplates: AdapterTemplateModel[]; } export interface KeyModel extends IdEntity { - entityId: number; - namespaceId: number; - columnIds: number[]; - isPrimary: boolean; + entityId: number; + namespaceId: number; + columnIds: number[]; + isPrimary: boolean; } export interface ForeignKeyModel extends KeyModel { - referencedIds: number[]; - referencedEntityId: number; + referencedIds: number[]; + referencedEntityId: number; } export interface ConstraintModel extends IdEntity { - keyId: number; - type: string; + keyId: number; + type: string; } export interface AllocationEntityModel extends IdEntity { - logicalEntityId: number; - placementId: number; - partitionId: number; + logicalEntityId: number; + placementId: number; + partitionId: number; } export interface AllocationPlacementModel extends IdEntity { - logicalEntityId: number; - adapterId: number; - partitionType: PartitionType; + logicalEntityId: number; + adapterId: number; + partitionType: PartitionType; } export interface AllocationPartitionModel extends IdEntity { - logicalEntityId: number; + logicalEntityId: number; } export interface AllocationColumnModel extends IdEntity { - namespaceId: number; - placementId: number; - logicalTableId: number; - placementType: PlacementType; - position: number; - adapterId: number; + namespaceId: number; + placementId: number; + logicalTableId: number; + placementType: PlacementType; + position: number; + adapterId: number; } export interface AdapterTemplateModel { - adapterName: string; - adapterType: AdapterType; - settings: AdapterSettingModel[]; - description: string; - modes: DeployMode[]; - persistent: boolean; + adapterName: string; + adapterType: AdapterType; + settings: AdapterSettingModel[]; + description: string; + modes: DeployMode[]; + persistent: boolean; } export enum DeployMode { - EMBEDDED = 'EMBEDDED', - DOCKER = 'DOCKER', - REMOTE = 'REMOTE', - ALL = 'ALL' + EMBEDDED = 'EMBEDDED', + DOCKER = 'DOCKER', + REMOTE = 'REMOTE', + ALL = 'ALL' } export interface AdapterSettingModel { - subOf: string; - name: string; - nameAlias: string; - alias: any; - description: string; - defaultValue: string; - canBeNull: boolean; - required: boolean; - modifiable: boolean; - options: string[]; - fileNames: string[]; - dynamic: boolean; - position: number; - appliesTo: DeployMode[]; + subOf: string; + name: string; + nameAlias: string; + alias: any; + description: string; + defaultValue: string; + canBeNull: boolean; + required: boolean; + modifiable: boolean; + options: string[]; + fileNames: string[]; + dynamic: boolean; + position: number; + appliesTo: DeployMode[]; } export class AdapterSettingValueModel { - name: string; - value: string; + name: string; + value: string; - constructor(name: string, value: string) { - this.name = name; - this.value = value; - } + constructor(name: string, value: string) { + this.name = name; + this.value = value; + } } export enum EntityType { - ENTITY = 'ENTITY', - SOURCE = 'SOURCE', - VIEW = 'VIEW', - MATERIALIZED_VIEW = 'MATERIALIZED_VIEW' + ENTITY = 'ENTITY', + SOURCE = 'SOURCE', + VIEW = 'VIEW', + MATERIALIZED_VIEW = 'MATERIALIZED_VIEW' } //// UTIL export interface AssetsModel { - RELATIONAL_ICON: string; - DOCUMENT_ICON: string; - GRAPH_ICON: string; - TABLE_ICON: string; - COLLECTION_ICON: string; - VIEW_ICON: string; - SOURCE_ICON: string; + RELATIONAL_ICON: string; + DOCUMENT_ICON: string; + GRAPH_ICON: string; + TABLE_ICON: string; + COLLECTION_ICON: string; + VIEW_ICON: string; + SOURCE_ICON: string; } //// REQUESTS export class NamespaceRequest { - dataModels: DataModel[] = null; + dataModels: DataModel[] = null; - constructor(dataModels: DataModel[] = null) { - this.dataModels = dataModels; - } + constructor(dataModels: DataModel[] = null) { + this.dataModels = dataModels; + } } diff --git a/src/app/models/docker.model.ts b/src/app/models/docker.model.ts index e8a0d07b..fc5bccba 100644 --- a/src/app/models/docker.model.ts +++ b/src/app/models/docker.model.ts @@ -1,58 +1,58 @@ export interface Handshake { - lastErrorMessage: string; - hostname: string; - execCommand: string; - containerExists: string; - status: string; - runCommand: string; + lastErrorMessage: string; + hostname: string; + execCommand: string; + containerExists: string; + status: string; + runCommand: string; } export interface DockerInstance { - id: number; - host: string; - alias: string; - connected: boolean; - registry: string; - communicationPort: number; - handshakePort: number; - proxyPort: number; - numberOfContainers: number; + id: number; + host: string; + alias: string; + connected: boolean; + registry: string; + communicationPort: number; + handshakePort: number; + proxyPort: number; + numberOfContainers: number; } export interface DockerStatus { - successful: boolean; - errorMessage: string; - instanceId: number; + successful: boolean; + errorMessage: string; + instanceId: number; } export interface DockerSetupResponse { - error: string; - success: boolean; - handshake: Handshake; - instances: DockerInstance[]; + error: string; + success: boolean; + handshake: Handshake; + instances: DockerInstance[]; } export interface DockerUpdateResponse { - error: string; - instance: DockerInstance; - handshake: Handshake; + error: string; + instance: DockerInstance; + handshake: Handshake; } export interface DockerReconnectResponse { - error: string; - handshake: Handshake; + error: string; + handshake: Handshake; } export interface DockerRemoveResponse { - error: string; - instances: DockerInstance[]; + error: string; + instances: DockerInstance[]; } export interface HandshakeAndInstance { - handshake: Handshake; - instance: DockerInstance; + handshake: Handshake; + instance: DockerInstance; } export interface DockerSettings { - registry: string; + registry: string; } diff --git a/src/app/models/information-page.model.ts b/src/app/models/information-page.model.ts index d1e4b700..3dda1635 100644 --- a/src/app/models/information-page.model.ts +++ b/src/app/models/information-page.model.ts @@ -1,76 +1,76 @@ import {ResultException} from '../components/data-view/models/result-set.model'; export interface InformationPage { - mansonry?: boolean; - groups: Map; - name?: string; - id?: string; - description?: string; - refreshable: boolean; - fullWidth?: boolean; + mansonry?: boolean; + groups: Map; + name?: string; + id?: string; + description?: string; + refreshable: boolean; + fullWidth?: boolean; } export interface InformationGroup { - color?: string; - informationObjects: InformationObject[]; - refreshable: boolean; + color?: string; + informationObjects: InformationObject[]; + refreshable: boolean; } export interface InformationObject extends Duration { - type?: string; - label?: string; - routerLink?: any; - button?: any[]; - badge?: string; - isCollapsed?: boolean; - items?: any[]; - color?: string; - value?: any; - min?: number; - max?: number; - step?: number; - html?: string; - //config - webUiGroup?: string; - key?: string; - //information - id?: string; - groupId?: string; - //graph: - data?: any; - labels?: string[]; - colors?: string[]; - graphType?: string; - //debugger - queryPlan: string; - //code - code?: string; - language?: string; - //table - rows?: string[]; - //InformationDuration - //=> extended by Duration interface - //action - parameters: any; - //exception - exception: ResultException; - //keyValuePair - keyValuePairs: Map; - //InformationText - text: string; + type?: string; + label?: string; + routerLink?: any; + button?: any[]; + badge?: string; + isCollapsed?: boolean; + items?: any[]; + color?: string; + value?: any; + min?: number; + max?: number; + step?: number; + html?: string; + //config + webUiGroup?: string; + key?: string; + //information + id?: string; + groupId?: string; + //graph: + data?: any; + labels?: string[]; + colors?: string[]; + graphType?: string; + //debugger + queryPlan: string; + //code + code?: string; + language?: string; + //table + rows?: string[]; + //InformationDuration + //=> extended by Duration interface + //action + parameters: any; + //exception + exception: ResultException; + //keyValuePair + keyValuePairs: Map; + //InformationText + text: string; } export interface InformationResponse { - errorMsg?: string; - successMsg?: string; + errorMsg?: string; + successMsg?: string; } export interface Duration { - name: string; - duration: number; - limit: number; - sequence: number; - isChild: boolean; - children: Duration[];//Durations map - noProgressBar: boolean; + name: string; + duration: number; + limit: number; + sequence: number; + isChild: boolean; + children: Duration[];//Durations map + noProgressBar: boolean; } diff --git a/src/app/models/sidebar-button.model.ts b/src/app/models/sidebar-button.model.ts index f37d82de..d82af228 100644 --- a/src/app/models/sidebar-button.model.ts +++ b/src/app/models/sidebar-button.model.ts @@ -1,11 +1,11 @@ export class SidebarButton { - name: string; - isOutline: boolean; - clickEvent: ($event?) => any; + name: string; + isOutline: boolean; + clickEvent: ($event?) => any; - constructor(name: string, clickEvent: ($event?) => any, isOutline = false) { - this.name = name; - this.clickEvent = clickEvent; - this.isOutline = isOutline; - } + constructor(name: string, clickEvent: ($event?) => any, isOutline = false) { + this.name = name; + this.clickEvent = clickEvent; + this.isOutline = isOutline; + } } diff --git a/src/app/models/sidebar-node.model.ts b/src/app/models/sidebar-node.model.ts index 5bcbb6a3..b93fba9f 100644 --- a/src/app/models/sidebar-node.model.ts +++ b/src/app/models/sidebar-node.model.ts @@ -2,130 +2,133 @@ import {TreeModel, TreeNode} from '@ali-hm/angular-tree-component'; export class SidebarNode { - id: any;// of the form "schema.table.column" - name: string; - tableType: string; - icon: string; - algSymbol: string; - routerLink: any; - label: string; - allowRouting = true; - cssClass: string; - allowDrag = false; - allowDropFrom = false; - allowDropTo = false; - children: SidebarNode[] = []; - isSeparator = false; - dataModel: string; - action: (tree, node, $event) => any = null; - private dropAction: (tree: TreeModel, node: TreeNode, $event: any, {from, to}: { from: any; to: any }) => any = null; - private autoExpand = true; - private autoActive = true; - - constructor(id, name, icon = null, routerLink: any = null, allowDrag = false, allowDropFrom = false, allowDropTo = false) { - this.id = id; - this.name = name; - this.icon = icon; - this.routerLink = routerLink; - this.allowDrag = allowDrag; - this.allowDropFrom = allowDropFrom; - this.allowDropTo = allowDropTo; - } - - /** - * generate an instance of the SidebarNode class from a parsed JSON object - * @param obj parsed JSON object - * @param settings Settings that should be applied to all generated SidebarNodes - */ - static fromJson(obj, settings = {}) { - const sidebarNode = new SidebarNode(obj.id, obj.name); - for (const key in obj) { - if (obj.hasOwnProperty(key)) { - if (key === 'action') { - continue; - } else if (key === 'children' && sidebarNode.children) { - sidebarNode.children = []; - for (const c of obj.children) { - sidebarNode.children.push(this.fromJson(c, settings)); - } - } else { - sidebarNode[key] = obj[key]; - for (const k in settings) { - if (settings.hasOwnProperty(k)) { - sidebarNode[k] = settings[k]; + id: any;// of the form "schema.table.column" + name: string; + tableType: string; + icon: string; + algSymbol: string; + routerLink: any; + label: string; + allowRouting = true; + cssClass: string; + allowDrag = false; + allowDropFrom = false; + allowDropTo = false; + children: SidebarNode[] = []; + isSeparator = false; + dataModel: string; + action: (tree, node, $event) => any = null; + private dropAction: (tree: TreeModel, node: TreeNode, $event: any, {from, to}: { + from: any; + to: any + }) => any = null; + private autoExpand = true; + private autoActive = true; + + constructor(id, name, icon = null, routerLink: any = null, allowDrag = false, allowDropFrom = false, allowDropTo = false) { + this.id = id; + this.name = name; + this.icon = icon; + this.routerLink = routerLink; + this.allowDrag = allowDrag; + this.allowDropFrom = allowDropFrom; + this.allowDropTo = allowDropTo; + } + + /** + * generate an instance of the SidebarNode class from a parsed JSON object + * @param obj parsed JSON object + * @param settings Settings that should be applied to all generated SidebarNodes + */ + static fromJson(obj, settings = {}) { + const sidebarNode = new SidebarNode(obj.id, obj.name); + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + if (key === 'action') { + continue; + } else if (key === 'children' && sidebarNode.children) { + sidebarNode.children = []; + for (const c of obj.children) { + sidebarNode.children.push(this.fromJson(c, settings)); + } + } else { + sidebarNode[key] = obj[key]; + for (const k in settings) { + if (settings.hasOwnProperty(k)) { + sidebarNode[k] = settings[k]; + } + } + } } - } } - } + return sidebarNode; + } + + setChildren(children: SidebarNode[]) { + this.children = children; + } + + getNamespace(): string { + return this.id.split('.')[0]; + } + + getEntity(): string { + return this.id.split('.')[0] + '.' + this.id.split('.')[1]; + } + + getField(): string { + return this.id.split('.')[2]; + } + + asSeparator() { + this.isSeparator = true; + return this; + } + + setAction(action: (tree, node, $event) => any) { + this.action = action; + return this; + } + + setDropAction(action: (tree: TreeModel, node: TreeNode, $event: any, {from, to}) => any) { + this.dropAction = action; + return this; + + } + + setAutoExpand(autoExpand: boolean) { + this.autoExpand = autoExpand; + return this; + } + + setAutoActive(autoActive: boolean) { + this.autoActive = autoActive; + return this; + } + + disableRouting() { + this.allowRouting = false; + return this; + } + + isAutoExpand() { + return this.autoExpand; + } + + isAutoActive() { + return this.autoActive; + } + + setAlgSymbol(symbol: string) { + this.algSymbol = symbol; + this.icon = null; + return this; } - return sidebarNode; - } - - setChildren(children: SidebarNode[]) { - this.children = children; - } - - getNamespace(): string { - return this.id.split('.')[0]; - } - - getEntity(): string { - return this.id.split('.')[0] + '.' + this.id.split('.')[1]; - } - - getField(): string { - return this.id.split('.')[2]; - } - - asSeparator() { - this.isSeparator = true; - return this; - } - - setAction(action: (tree, node, $event) => any) { - this.action = action; - return this; - } - - setDropAction(action: (tree: TreeModel, node: TreeNode, $event: any, {from, to}) => any) { - this.dropAction = action; - return this; - - } - - setAutoExpand(autoExpand: boolean) { - this.autoExpand = autoExpand; - return this; - } - - setAutoActive(autoActive: boolean) { - this.autoActive = autoActive; - return this; - } - - disableRouting() { - this.allowRouting = false; - return this; - } - - isAutoExpand() { - return this.autoExpand; - } - - isAutoActive() { - return this.autoActive; - } - - setAlgSymbol(symbol: string) { - this.algSymbol = symbol; - this.icon = null; - return this; - } } export interface JavaPage { - id: any; - name: string; - icon: string; - label: string; + id: any; + name: string; + icon: string; + label: string; } diff --git a/src/app/models/ui-request.model.ts b/src/app/models/ui-request.model.ts index cc66f5d0..df804a57 100644 --- a/src/app/models/ui-request.model.ts +++ b/src/app/models/ui-request.model.ts @@ -4,250 +4,250 @@ import {Node} from '../views/querying/algebra/algebra.model'; import {EntityType} from './catalog.model'; export class RequestModel { - type: string; + type: string; } export abstract class UIRequest extends RequestModel { - entityId: number; - namespace: string; - currentPage: number; - data: Map; - filter: Map; - sortState: Map; - selectInterval: string; + entityId: number; + namespace: string; + currentPage: number; + data: Map; + filter: Map; + sortState: Map; + selectInterval: string; } export class EntityRequest extends UIRequest { - type = 'EntityRequest'; - - constructor(entityId: number, namespace: string, currentPage: number, filter: any = null, sortState: any = null) { - super(); - this.entityId = entityId; - this.namespace = namespace; - this.currentPage = currentPage; - this.filter = filter; - this.sortState = sortState; - return this; - } + type = 'EntityRequest'; + + constructor(entityId: number, namespace: string, currentPage: number, filter: any = null, sortState: any = null) { + super(); + this.entityId = entityId; + this.namespace = namespace; + this.currentPage = currentPage; + this.filter = filter; + this.sortState = sortState; + return this; + } } export class RelAlgRequest extends UIRequest { - type = 'RelAlgRequest'; - topNode: Node; - createView: boolean; - analyze: boolean; - tableType; - string; - viewName: string; - store: string; - freshness: string; - interval; - timeUnit: string; - useCache: boolean; - - constructor(node: Node, cache: boolean, analyzeQuery: boolean, createView?: boolean, tableType?: string, viewName?: string, store?: string, freshness?: string, interval?, timeUnit?: string) { - super(); - this.topNode = node; - this.useCache = cache; - this.analyze = analyzeQuery; - this.createView = createView || false; - this.tableType = tableType || 'table'; - this.viewName = viewName || 'viewName'; - this.store = store || null; - this.freshness = freshness || null; - this.interval = interval || null; - this.timeUnit = timeUnit || null; - } + type = 'RelAlgRequest'; + topNode: Node; + createView: boolean; + analyze: boolean; + tableType; + string; + viewName: string; + store: string; + freshness: string; + interval; + timeUnit: string; + useCache: boolean; + + constructor(node: Node, cache: boolean, analyzeQuery: boolean, createView?: boolean, tableType?: string, viewName?: string, store?: string, freshness?: string, interval?, timeUnit?: string) { + super(); + this.topNode = node; + this.useCache = cache; + this.analyze = analyzeQuery; + this.createView = createView || false; + this.tableType = tableType || 'table'; + this.viewName = viewName || 'viewName'; + this.store = store || null; + this.freshness = freshness || null; + this.interval = interval || null; + this.timeUnit = timeUnit || null; + } } export class QueryRequest extends UIRequest { - type = 'QueryRequest'; - query: string; - analyze: boolean; - language: string; - namespace: string; - cache: boolean; - - constructor(query: string, analyze: boolean, cache: boolean, lang: string, namespace: string) { - super(); - this.query = query; - this.analyze = analyze; - this.cache = cache; - this.language = lang; - this.namespace = namespace; - this.currentPage = 1; - return this; - } + type = 'QueryRequest'; + query: string; + analyze: boolean; + language: string; + namespace: string; + cache: boolean; + + constructor(query: string, analyze: boolean, cache: boolean, lang: string, namespace: string) { + super(); + this.query = query; + this.analyze = analyze; + this.cache = cache; + this.language = lang; + this.namespace = namespace; + this.currentPage = 1; + return this; + } } export class GraphRequest extends QueryRequest { - type = 'GraphRequest'; - private nodeIds: string[]; - private edgeIds: string[]; - - constructor(namespace: string, nodeIds: Set, edgeIds: Set) { - super('MATCH * RETURN *', false, false, 'CYPHER', namespace); - this.nodeIds = Array.from(nodeIds); - this.edgeIds = Array.from(edgeIds); - } + type = 'GraphRequest'; + private nodeIds: string[]; + private edgeIds: string[]; + + constructor(namespace: string, nodeIds: Set, edgeIds: Set) { + super('MATCH * RETURN *', false, false, 'CYPHER', namespace); + this.nodeIds = Array.from(nodeIds); + this.edgeIds = Array.from(edgeIds); + } } export class QueryExplorationRequest extends UIRequest { - query: string; - analyze: boolean; - cPage: number; - - constructor(query: string, analyze: boolean, cPage: number) { - super(); - this.query = query; - this.analyze = analyze; - this.cPage = cPage; - return this; - } + query: string; + analyze: boolean; + cPage: number; + + constructor(query: string, analyze: boolean, cPage: number) { + super(); + this.query = query; + this.analyze = analyze; + this.cPage = cPage; + return this; + } } /** * Request to classify data */ export class ClassifyRequest { - id: number; - header: UiColumnDefinition[]; - classified: string[][]; - cPage: number; - - constructor(id: number, header: UiColumnDefinition[], classified: string[][], cPage: number) { - this.id = id; - this.header = header; - this.classified = classified; - this.cPage = cPage; - return this; - } + id: number; + header: UiColumnDefinition[]; + classified: string[][]; + cPage: number; + + constructor(id: number, header: UiColumnDefinition[], classified: string[][], cPage: number) { + this.id = id; + this.header = header; + this.classified = classified; + this.cPage = cPage; + return this; + } } export class Exploration { - id: number; - header: UiColumnDefinition[]; - classified: string[][]; + id: number; + header: UiColumnDefinition[]; + classified: string[][]; - constructor(id: number, header: UiColumnDefinition[], classified: string[][]) { - this.id = id; - this.header = header; - this.classified = classified; + constructor(id: number, header: UiColumnDefinition[], classified: string[][]) { + this.id = id; + this.header = header; + this.classified = classified; - } + } } export class PluginEntity { - id: string; - stringPath: string; - status: boolean; - imagePath: string; - categories: string[]; - version: string; + id: string; + stringPath: string; + status: boolean; + imagePath: string; + categories: string[]; + version: string; } export enum PluginStatus { - UNLOADED = 'UNLOADED', - LOADED = 'LOADED', - ACTIVE = 'ACTIVE' + UNLOADED = 'UNLOADED', + LOADED = 'LOADED', + ACTIVE = 'ACTIVE' } export class ExploreTable extends UIRequest { - id: number; - header: UiColumnDefinition[]; - cPage: number; - - constructor(id: number, header: UiColumnDefinition[], cPage: number) { - super(); - this.id = id; - this.header = header; - this.cPage = cPage; - } + id: number; + header: UiColumnDefinition[]; + cPage: number; + + constructor(id: number, header: UiColumnDefinition[], cPage: number) { + super(); + this.id = id; + this.header = header; + this.cPage = cPage; + } } export class StatisticRequest extends UIRequest { - constructor(entityId?: number) { - super(); - this.entityId = entityId || null; - return this; - } + constructor(entityId?: number) { + super(); + this.entityId = entityId || null; + return this; + } } export class MonitoringRequest extends UIRequest { - constructor(selectInterval?: string) { - super(); - this.selectInterval = selectInterval || null; - return this; - } + constructor(selectInterval?: string) { + super(); + this.selectInterval = selectInterval || null; + return this; + } } export class DeleteRequest extends UIRequest { - constructor(entityId: number, data: any) { - super(); - this.entityId = entityId; - this.data = data; - } + constructor(entityId: number, data: any) { + super(); + this.entityId = entityId; + this.data = data; + } } export class SchemaRequest extends UIRequest { - routerLinkRoot: string; - views: boolean; - /** - * depth 1: schemas - * depth 2: schemas + tables - * depth 3: schemas + tables + columns - */ - depth: number; - /** - * if show table is false, "table" will not be shown in left sidebar - */ - showTable: boolean; - schemaEdit: boolean; - dataModels: DataModel[]; - - constructor(routerLinkRoot: string, views: boolean, depth: number, showTable: boolean, schemaEdit?: boolean, dataModels: DataModel[] = [DataModel.RELATIONAL, DataModel.DOCUMENT, DataModel.GRAPH]) { - super(); - this.routerLinkRoot = routerLinkRoot; - this.views = views; - this.depth = depth; - this.showTable = showTable; - this.schemaEdit = schemaEdit || false; - this.dataModels = dataModels; - } + routerLinkRoot: string; + views: boolean; + /** + * depth 1: schemas + * depth 2: schemas + tables + * depth 3: schemas + tables + columns + */ + depth: number; + /** + * if show table is false, "table" will not be shown in left sidebar + */ + showTable: boolean; + schemaEdit: boolean; + dataModels: DataModel[]; + + constructor(routerLinkRoot: string, views: boolean, depth: number, showTable: boolean, schemaEdit?: boolean, dataModels: DataModel[] = [DataModel.RELATIONAL, DataModel.DOCUMENT, DataModel.GRAPH]) { + super(); + this.routerLinkRoot = routerLinkRoot; + this.views = views; + this.depth = depth; + this.showTable = showTable; + this.schemaEdit = schemaEdit || false; + this.dataModels = dataModels; + } } export class ColumnRequest extends UIRequest { - oldColumn: UiColumnDefinition; - newColumn: UiColumnDefinition; - renameOnly: boolean; - tableType: string; - - constructor(entityId: number, oldColumn: UiColumnDefinition = null, newColumn: UiColumnDefinition = null, renameOnly = false, tableType: string = 'table') { - super(); - this.entityId = entityId; - this.oldColumn = oldColumn; - this.newColumn = newColumn; - this.renameOnly = renameOnly; - this.tableType = tableType; - } + oldColumn: UiColumnDefinition; + newColumn: UiColumnDefinition; + renameOnly: boolean; + tableType: string; + + constructor(entityId: number, oldColumn: UiColumnDefinition = null, newColumn: UiColumnDefinition = null, renameOnly = false, tableType: string = 'table') { + super(); + this.entityId = entityId; + this.oldColumn = oldColumn; + this.newColumn = newColumn; + this.renameOnly = renameOnly; + this.tableType = tableType; + } } export class MaterializedRequest extends UIRequest { - constructor(entityId: number) { - super(); - this.entityId = entityId; - } + constructor(entityId: number) { + super(); + this.entityId = entityId; + } } export enum DataModel { - DOCUMENT = 'DOCUMENT', - RELATIONAL = 'RELATIONAL', - GRAPH = 'GRAPH' + DOCUMENT = 'DOCUMENT', + RELATIONAL = 'RELATIONAL', + GRAPH = 'GRAPH' } @@ -257,71 +257,71 @@ export enum DataModel { * and when you want to create a new table */ export class EditTableRequest { - namespaceId: number; - entityId: number; - entityName: string; - action: string;//truncate / drop - columns: UiColumnDefinition[]; - storeId: number; - tableType: EntityType; - - constructor(namespaceId: number, entityId: number = null, entityName: string = null, action: string = null, columns: UiColumnDefinition[] = null, storeId: number = null, tableType: EntityType = EntityType.ENTITY) { - this.namespaceId = namespaceId; - this.entityId = entityId; - this.entityName = entityName; - this.action = action; - this.columns = columns; - this.storeId = storeId; - this.tableType = EntityType.ENTITY; - } - - getAction() { - return this.action; - } + namespaceId: number; + entityId: number; + entityName: string; + action: string;//truncate / drop + columns: UiColumnDefinition[]; + storeId: number; + tableType: EntityType; + + constructor(namespaceId: number, entityId: number = null, entityName: string = null, action: string = null, columns: UiColumnDefinition[] = null, storeId: number = null, tableType: EntityType = EntityType.ENTITY) { + this.namespaceId = namespaceId; + this.entityId = entityId; + this.entityName = entityName; + this.action = action; + this.columns = columns; + this.storeId = storeId; + this.tableType = EntityType.ENTITY; + } + + getAction() { + return this.action; + } } export class EditCollectionRequest { - namespaceId: number; - entityName: string; - entityId: number; - action: string; - store: string; - - constructor(namespaceId: number, entityName: string = null, entityId: number = null, action: string = null, store: string = null) { - this.namespaceId = namespaceId; - this.entityName = entityName; - this.entityId = entityId; - this.action = action; - this.store = store; - } + namespaceId: number; + entityName: string; + entityId: number; + action: string; + store: string; + + constructor(namespaceId: number, entityName: string = null, entityId: number = null, action: string = null, store: string = null) { + this.namespaceId = namespaceId; + this.entityName = entityName; + this.entityId = entityId; + this.action = action; + this.store = store; + } } /** * Request to drop or create a constraint of a table */ export class ConstraintRequest { - constructor(private entityId: number, private constraint: TableConstraint) { - } + constructor(private entityId: number, private constraint: TableConstraint) { + } } export enum Method { - ADD = 'ADD', - DROP = 'DROP', - MODIFY = 'MODIFY', - TRUNCATE = 'TRUNCATE' + ADD = 'ADD', + DROP = 'DROP', + MODIFY = 'MODIFY', + TRUNCATE = 'TRUNCATE' } export class RegisterRequest extends RequestModel { - type = 'RegisterRequest'; - source: string; - payload: string; + type = 'RegisterRequest'; + source: string; + payload: string; - constructor(source: string, payload: string) { - super(); - this.source = source; - this.payload = payload; - } + constructor(source: string, payload: string) { + super(); + this.source = source; + this.payload = payload; + } } @@ -329,43 +329,43 @@ export class RegisterRequest extends RequestModel { * Send request to either create or drop a schema */ export class Namespace { - private name: string; - private type: string;//todo enum - private store: string; - - // fields for creation - create = false; - ifNotExists = true; - authorization: string = null; - - //fields for deletion - drop = false; - ifExists = true; - cascade = false; - - constructor(name: string, type: string, store: string) { - this.name = name; - this.type = type; - this.store = store; - } - - setCreate(create: boolean) { - this.create = create; - return this; - } - - setAuthorization(auth: string) { - this.authorization = auth; - return this; - } - - setDrop(drop: boolean) { - this.drop = drop; - return this; - } - - setCascade(cascade: boolean) { - this.cascade = cascade; - return this; - } + private name: string; + private type: string;//todo enum + private store: string; + + // fields for creation + create = false; + ifNotExists = true; + authorization: string = null; + + //fields for deletion + drop = false; + ifExists = true; + cascade = false; + + constructor(name: string, type: string, store: string) { + this.name = name; + this.type = type; + this.store = store; + } + + setCreate(create: boolean) { + this.create = create; + return this; + } + + setAuthorization(auth: string) { + this.authorization = auth; + return this; + } + + setDrop(drop: boolean) { + this.drop = drop; + return this; + } + + setCascade(cascade: boolean) { + this.cascade = cascade; + return this; + } } diff --git a/src/app/pipes/pipes.ts b/src/app/pipes/pipes.ts index 5064e19e..a79ed747 100644 --- a/src/app/pipes/pipes.ts +++ b/src/app/pipes/pipes.ts @@ -2,22 +2,22 @@ import {Pipe, PipeTransform} from '@angular/core'; @Pipe({name: 'value'}) export class ValuePipe implements PipeTransform { - transform(value: any): any { - return Object.values(value); - } + transform(value: any): any { + return Object.values(value); + } } @Pipe({name: 'searchFilter'}) export class SearchFilterPipe implements PipeTransform { - transform(map: Map, searchText: string): Map { - if (!searchText) { - return map; - } - searchText = searchText.toLocaleLowerCase(); - let filteredMap = new Map(); - for (let key of Array.from(map.keys()).filter((query: string) => query.toLowerCase().indexOf(searchText) > -1)) { - filteredMap.set(key, map.get(key)); + transform(map: Map, searchText: string): Map { + if (!searchText) { + return map; + } + searchText = searchText.toLocaleLowerCase(); + let filteredMap = new Map(); + for (let key of Array.from(map.keys()).filter((query: string) => query.toLowerCase().indexOf(searchText) > -1)) { + filteredMap.set(key, map.get(key)); + } + return filteredMap; } - return filteredMap; - } } diff --git a/src/app/plugins/notebooks/components/edit-notebook/edit-notebook.component.ts b/src/app/plugins/notebooks/components/edit-notebook/edit-notebook.component.ts index a06e63e5..4443a62e 100644 --- a/src/app/plugins/notebooks/components/edit-notebook/edit-notebook.component.ts +++ b/src/app/plugins/notebooks/components/edit-notebook/edit-notebook.component.ts @@ -1,4 +1,18 @@ -import {Component, EventEmitter, HostListener, inject, Input, OnChanges, OnDestroy, OnInit, Output, QueryList, SimpleChanges, ViewChild, ViewChildren} from '@angular/core'; +import { + Component, + EventEmitter, + HostListener, + inject, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + QueryList, + SimpleChanges, + ViewChild, + ViewChildren +} from '@angular/core'; import {KernelSpec, NotebookContent, SessionResponse} from '../../models/notebooks-response.model'; import {NotebooksService} from '../../services/notebooks.service'; import {NotebooksSidebarService} from '../../services/notebooks-sidebar.service'; @@ -17,724 +31,724 @@ import {FormControl, FormGroup, Validators} from '@angular/forms'; import {ToasterService} from '../../../../components/toast-exposer/toaster.service'; @Component({ - selector: 'app-edit-notebook', - templateUrl: './edit-notebook.component.html', - styleUrls: ['./edit-notebook.component.scss'] + selector: 'app-edit-notebook', + templateUrl: './edit-notebook.component.html', + styleUrls: ['./edit-notebook.component.scss'] }) export class EditNotebookComponent implements OnInit, OnChanges, OnDestroy { - private readonly _notebooks = inject(NotebooksService); - public readonly _sidebar = inject(NotebooksSidebarService); - private readonly _content = inject(NotebooksContentService); - private readonly _toast = inject(ToasterService); - private readonly _router = inject(Router); - private readonly _route = inject(ActivatedRoute); - private readonly _loading = inject(LoadingScreenService); - - @Input() sessionId: string; - @Output() openChangeSessionModal = new EventEmitter<{ name: string, path: string }>(); - path: string; - name: string; - nb: NotebookWrapper; - session: SessionResponse; - kernelSpec: KernelSpec; - private subscriptions = new Subscription(); - selectedCell: NotebookCell; - selectedCellType: CellType = 'code'; - busyCellIds = new Set(); - mode: NbMode = 'command'; - namespaces: string[] = []; - expand = false; - private copiedCell: string; // stringified NotebookCell - @ViewChild('deleteNotebookModal') public deleteNotebookModal: ModalDirective; - @ViewChild('restartKernelModal') public restartKernelModal: ModalDirective; - private executeAllAfterRestart = false; - @ViewChild('overwriteNotebookModal') public overwriteNotebookModal: ModalDirective; - @ViewChild('renameNotebookModal') public renameNotebookModal: ModalDirective; - @ViewChild('closeNotebookModal') public closeNotebookModal: ModalDirective; - @ViewChild('terminateKernelModal') public terminateKernelModal: ModalDirective; - @ViewChild('terminateAllKernelsModal') public terminateAllKernelsModal: ModalDirective; - @ViewChildren('nbCell') cellComponents: QueryList; - renameNotebookForm: FormGroup; - closeNotebookForm: FormGroup; - deleting = false; - overwriting = false; - inserting = false; - closeNbSubject: Subject; - - constructor() { - } - - ngOnInit(): void { - this.subscriptions.add( - this._content.onSessionsChange().subscribe(sessions => this.updateSession(sessions)) - ); - this.subscriptions.add( - this._content.onNamespaceChange().subscribe(namespaces => this.namespaces = namespaces) - ); - this.initForms(); - } - - ngOnChanges(changes: SimpleChanges): void { - if (changes.sessionId) { - // Handle changes to sessionId - this._notebooks.getSession(this.sessionId).subscribe(session => { - this.session = session; - this.path = this._notebooks.getPathFromSession(session); - this.name = this._notebooks.getNameFromSession(session); - const urlPath = 'notebooks/' + this._route.snapshot.url.map(segment => decodeURIComponent(segment.toString())).join('/'); - if (this.path !== urlPath) { - this.closeEdit(true); - return; + private readonly _notebooks = inject(NotebooksService); + public readonly _sidebar = inject(NotebooksSidebarService); + private readonly _content = inject(NotebooksContentService); + private readonly _toast = inject(ToasterService); + private readonly _router = inject(Router); + private readonly _route = inject(ActivatedRoute); + private readonly _loading = inject(LoadingScreenService); + + @Input() sessionId: string; + @Output() openChangeSessionModal = new EventEmitter<{ name: string, path: string }>(); + path: string; + name: string; + nb: NotebookWrapper; + session: SessionResponse; + kernelSpec: KernelSpec; + private subscriptions = new Subscription(); + selectedCell: NotebookCell; + selectedCellType: CellType = 'code'; + busyCellIds = new Set(); + mode: NbMode = 'command'; + namespaces: string[] = []; + expand = false; + private copiedCell: string; // stringified NotebookCell + @ViewChild('deleteNotebookModal') public deleteNotebookModal: ModalDirective; + @ViewChild('restartKernelModal') public restartKernelModal: ModalDirective; + private executeAllAfterRestart = false; + @ViewChild('overwriteNotebookModal') public overwriteNotebookModal: ModalDirective; + @ViewChild('renameNotebookModal') public renameNotebookModal: ModalDirective; + @ViewChild('closeNotebookModal') public closeNotebookModal: ModalDirective; + @ViewChild('terminateKernelModal') public terminateKernelModal: ModalDirective; + @ViewChild('terminateAllKernelsModal') public terminateAllKernelsModal: ModalDirective; + @ViewChildren('nbCell') cellComponents: QueryList; + renameNotebookForm: FormGroup; + closeNotebookForm: FormGroup; + deleting = false; + overwriting = false; + inserting = false; + closeNbSubject: Subject; + + constructor() { + } + + ngOnInit(): void { + this.subscriptions.add( + this._content.onSessionsChange().subscribe(sessions => this.updateSession(sessions)) + ); + this.subscriptions.add( + this._content.onNamespaceChange().subscribe(namespaces => this.namespaces = namespaces) + ); + this.initForms(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.sessionId) { + // Handle changes to sessionId + this._notebooks.getSession(this.sessionId).subscribe(session => { + this.session = session; + this.path = this._notebooks.getPathFromSession(session); + this.name = this._notebooks.getNameFromSession(session); + const urlPath = 'notebooks/' + this._route.snapshot.url.map(segment => decodeURIComponent(segment.toString())).join('/'); + if (this.path !== urlPath) { + this.closeEdit(true); + return; + } + if (!this.renameNotebookModal.isShown) { + this.renameNotebookForm.patchValue({name: this.name}); + } + this._content.setPreferredSessionId(this.path, this.sessionId); + this.loadNotebook(); + }, () => { + this.closeEdit(); + }); } - if (!this.renameNotebookModal.isShown) { - this.renameNotebookForm.patchValue({name: this.name}); + } + + ngOnDestroy() { + this.subscriptions.unsubscribe(); + this.nb?.closeSocket(); + this._sidebar.deselect(); + } + + // https://stackoverflow.com/questions/35922071/warn-user-of-unsaved-changes-before-leaving-page + + @HostListener('window:beforeunload') + canDeactivate(): Observable | boolean { + const hasChanged = this.nb?.hasChangedSinceSave(); + return !hasChanged; + } + + initForms() { + this.renameNotebookForm = new FormGroup({ + name: new FormControl('', [ + Validators.pattern('[a-zA-Z0-9_. \-]*[a-zA-Z0-9_\-]'), // last symbol can't be '.' or ' ' + Validators.maxLength(50), + Validators.required + ]) + }); + this.closeNotebookForm = new FormGroup({ + saveChanges: new FormControl(true), + shutDown: new FormControl(true) + }); + } + + private updateSession(sessions: SessionResponse[]) { + this.session = sessions.find(session => session.id === this.sessionId); + if (!this.session) { + this._toast.warn(`Kernel was closed.`); + return; } - this._content.setPreferredSessionId(this.path, this.sessionId); - this.loadNotebook(); - }, () => { - this.closeEdit(); - }); - } - } - - ngOnDestroy() { - this.subscriptions.unsubscribe(); - this.nb?.closeSocket(); - this._sidebar.deselect(); - } - - // https://stackoverflow.com/questions/35922071/warn-user-of-unsaved-changes-before-leaving-page - - @HostListener('window:beforeunload') - canDeactivate(): Observable | boolean { - const hasChanged = this.nb?.hasChangedSinceSave(); - return !hasChanged; - } - - initForms() { - this.renameNotebookForm = new FormGroup({ - name: new FormControl('', [ - Validators.pattern('[a-zA-Z0-9_. \-]*[a-zA-Z0-9_\-]'), // last symbol can't be '.' or ' ' - Validators.maxLength(50), - Validators.required - ]) - }); - this.closeNotebookForm = new FormGroup({ - saveChanges: new FormControl(true), - shutDown: new FormControl(true) - }); - } - - private updateSession(sessions: SessionResponse[]) { - this.session = sessions.find(session => session.id === this.sessionId); - if (!this.session) { - this._toast.warn(`Kernel was closed.`); - return; - } - const path = this._notebooks.getPathFromSession(this.session); - if (this.path !== path) { - if (this.path) { - const queryParams = {session: this.sessionId, forced: true}; - this._router.navigate([this._sidebar.baseUrl].concat(path.split('/')), {queryParams}); - this._toast.warn(`The path to the notebook has changed.`, 'Info'); - } - this.path = path; - this.name = this.name = this._notebooks.getNameFromSession(this.session); - - } - } - - private loadNotebook() { - this._loading.show(); - if (this.nb) { - this.nb.closeSocket(); - this.nb = null; - } - this._content.getNotebookContent(this.path, this.nb == null).subscribe(res => { - if (res) { - this.nb = new NotebookWrapper(res, this.busyCellIds, - new NotebooksWebSocket(this.session.kernel.id), - id => this.getCellComponent(id)?.renderMd(), - (id, output) => this.getCellComponent(id)?.renderError(output), - (id, output) => this.getCellComponent(id)?.renderStream(output), - id => this.getCellComponent(id)?.renderResultSet()); - this.expand = this.nb.isExpansionAllowed(); - this.kernelSpec = this._content.getKernelspec(this.session.kernel.name); - if (this.kernelSpec) { - this.nb.setKernelSpec(this.kernelSpec); - this.uploadNotebook(false); + const path = this._notebooks.getPathFromSession(this.session); + if (this.path !== path) { + if (this.path) { + const queryParams = {session: this.sessionId, forced: true}; + this._router.navigate([this._sidebar.baseUrl].concat(path.split('/')), {queryParams}); + this._toast.warn(`The path to the notebook has changed.`, 'Info'); + } + this.path = path; + this.name = this.name = this._notebooks.getNameFromSession(this.session); + } - if (this.nb.cells.length < 1) { - this.nb.insertCellByIdx(0, false); + } + + private loadNotebook() { + this._loading.show(); + if (this.nb) { + this.nb.closeSocket(); + this.nb = null; } - this.selectCell(this.nb.cells[0].id); - document.getElementById('notebook').focus(); - } - }).add(() => { - this._loading.hide(); - if (!this.nb) { - this._toast.error(`Could not read content of ${this.path}.`); + this._content.getNotebookContent(this.path, this.nb == null).subscribe(res => { + if (res) { + this.nb = new NotebookWrapper(res, this.busyCellIds, + new NotebooksWebSocket(this.session.kernel.id), + id => this.getCellComponent(id)?.renderMd(), + (id, output) => this.getCellComponent(id)?.renderError(output), + (id, output) => this.getCellComponent(id)?.renderStream(output), + id => this.getCellComponent(id)?.renderResultSet()); + this.expand = this.nb.isExpansionAllowed(); + this.kernelSpec = this._content.getKernelspec(this.session.kernel.name); + if (this.kernelSpec) { + this.nb.setKernelSpec(this.kernelSpec); + this.uploadNotebook(false); + } + if (this.nb.cells.length < 1) { + this.nb.insertCellByIdx(0, false); + } + this.selectCell(this.nb.cells[0].id); + document.getElementById('notebook').focus(); + } + }).add(() => { + this._loading.hide(); + if (!this.nb) { + this._toast.error(`Could not read content of ${this.path}.`); + this.closeEdit(true); + } + }); + } + + /** + * Close the edit component and navigate to the parent directory of this notebook. + * @param forced if true, no confirmation dialog will appear if there are unsaved changes + */ + closeEdit(forced = false) { + this.nb?.closeSocket(); + const queryParams = forced ? {forced: true} : null; + this._router.navigate([this._sidebar.baseUrl].concat(this._content.directoryPath.split('/')), + {queryParams}); + } + + /** + * Open the close notebook dialog and return a subject to subscribe to the dialog result. + * If the dialog is already open, return false. + * @return a boolean Subject that emits true if the close operation is confirmed and false when aborted + */ + confirmClose(): Subject | boolean { + if (this.closeNotebookModal.isShown) { + return false; + } + this.closeNbSubject = new Subject(); + this.closeNotebookModal.config.keyboard = false; + this.closeNotebookModal.config.backdrop = 'static'; + this.closeNotebookModal.show(); + return this.closeNbSubject; + } + + + closeNotebookCancelled() { + this.closeNbSubject?.next(false); + this.closeNbSubject?.complete(); + this.closeNbSubject = null; + this._sidebar.deselect(); + this.closeNotebookModal.hide(); + } + + + deleteNotebook(): void { + this.deleting = true; + this.terminateSessions(); this.closeEdit(true); - } - }); - } - - /** - * Close the edit component and navigate to the parent directory of this notebook. - * @param forced if true, no confirmation dialog will appear if there are unsaved changes - */ - closeEdit(forced = false) { - this.nb?.closeSocket(); - const queryParams = forced ? {forced: true} : null; - this._router.navigate([this._sidebar.baseUrl].concat(this._content.directoryPath.split('/')), - {queryParams}); - } - - /** - * Open the close notebook dialog and return a subject to subscribe to the dialog result. - * If the dialog is already open, return false. - * @return a boolean Subject that emits true if the close operation is confirmed and false when aborted - */ - confirmClose(): Subject | boolean { - if (this.closeNotebookModal.isShown) { - return false; - } - this.closeNbSubject = new Subject(); - this.closeNotebookModal.config.keyboard = false; - this.closeNotebookModal.config.backdrop = 'static'; - this.closeNotebookModal.show(); - return this.closeNbSubject; - } - - - closeNotebookCancelled() { - this.closeNbSubject?.next(false); - this.closeNbSubject?.complete(); - this.closeNbSubject = null; - this._sidebar.deselect(); - this.closeNotebookModal.hide(); - } - - - deleteNotebook(): void { - this.deleting = true; - this.terminateSessions(); - this.closeEdit(true); - this._notebooks.deleteFile(this.path).subscribe().add(() => { - this._content.update(); - this.deleting = false; - this.deleteNotebookModal.hide(); - }); - - } - - closeNotebookSubmitted() { - if (this.closeNotebookForm.value.saveChanges) { - this.overwriteNotebook(true); - } - - if (this.session && this.closeNotebookForm.value.shutDown) { - this.terminateSession(); - } - if (this.closeNbSubject) { - this.nb?.closeSocket(); - this.closeNbSubject.next(true); - this.closeNbSubject?.complete(); - } else { - this.closeEdit(true); - } - this.closeNbSubject = null; - this.closeNotebookModal.hide(); - } - - closeAndTerminate(terminateAll: boolean) { - this.closeEdit(); - if (terminateAll) { - this.terminateSessions(); - this.terminateAllKernelsModal.hide(); - } else { - this.terminateSession(); - this.terminateKernelModal.hide(); - } - } - - terminateSession() { - this._content.deleteSession(this.sessionId).subscribe(); - } - - terminateSessions() { - this._content.deleteSessions(this.path).subscribe(); - } - - rename() { - if (!this.renameNotebookForm.valid) { - return; - } - let fileName = this.renameNotebookForm.value.name; - if (!fileName.endsWith('.ipynb')) { - fileName += '.ipynb'; - } - const dirPath = this._content.directoryPath; - this._sidebar.moveFile(this.path, dirPath + '/' + fileName); - this.renameNotebookModal.hide(); - } - - duplicateNotebook() { - this._notebooks.duplicateFile(this.path, this._content.directoryPath).subscribe(() => this._content.update(), - err => this._toast.error(err.error.message, `Could not duplicate ${this.path}.`)); - } - - downloadNotebook() { - this._content.downloadNotebook(this.nb.notebook, this.name); - } - - exportNotebook() { - if (this.nb.hasChangedSinceSave()) { - this._toast.warn('Please save your changes first before exporting the notebook.', 'Info'); - return; - } - this._notebooks.getExportedNotebook(this.path, this.kernelSpec?.name).subscribe(res => { - this._content.downloadNotebook(res.content, 'exported_' + this.name); - }, - () => { - this._toast.warn('Unable to export the notebook.'); + this._notebooks.deleteFile(this.path).subscribe().add(() => { + this._content.update(); + this.deleting = false; + this.deleteNotebookModal.hide(); }); - } - - trustNotebook() { - this.nb.trustAllCells(); - } - - - executeSelected(advanceToNext = false) { - this.selectedComponent?.updateSource(); - this.nb.executeCell(this.selectedCell); - if (advanceToNext) { - if (this.nb.getCellIndex(this.selectedCell.id) === this.nb.cells.length - 1) { - this.insertCell(this.selectedCell.id, true, true); - } else { - this.selectCellBelow(false); - this.forceCommandMode(); - } - } - } - - executeAboveSelected() { - this.nb.executeCells(this.selectedCell, true, false); - } - - executeSelectedAndBelow() { - this.selectedComponent?.updateSource(); - this.nb.executeCells(this.selectedCell, false, true); - } - - executeAll() { - this.nb.executeAll(); - } - - renderMdCells() { - this.nb.executeMdCells(); - } - - clearSelectedOutput() { - this.selectedCell.outputs = []; - this.selectedCell.execution_count = null; - } - - clearAllOutputs() { - this.nb.clearAllOutputs(); - } - - toggleExpansion() { - this.expand = !this.expand; - this.nb.setExpansionAllowed(this.expand); - this._toast.success('Variable expansion has been ' + (this.expand ? 'activated' : 'deactivated') + '.'); - } - - /** - * This first compares the modification times of when this notebook was loaded and the one on disk. - * If they are not equal, the content of the one on disk is loaded and compared. This is only done now for - * improved performance.If they differ, the overwriteNotebookModal is shown. - * Otherwise, the notebook is uploaded. - */ - uploadNotebook(showSuccessToast = true) { - this._notebooks.getContents(this.path, false).pipe( - mergeMap(res => { - if (res.last_modified === this.nb.lastModifiedWhenLoaded) { - return this._notebooks.updateNotebook(this.path, this.nb.notebook); - } else { - this.uploadNotebookWithDeepCompare(showSuccessToast); - return EMPTY; - } - }) - ).subscribe(res => { - if (showSuccessToast) { - this._toast.success('Notebook was saved.'); - } - this.nb.markAsSaved(res.last_modified); - }, () => { - this._toast.error('An error occurred while uploading the notebook.'); - - }); - } - - /** - * Checks whether the content of the notebook that is stored on disk has changed compared - * to when it was loaded. If true, the overwriteNotebookModal is shown. Otherwise, the notebook is uploaded. - */ - uploadNotebookWithDeepCompare(showSuccessToast = true) { - this._notebooks.getContents(this.path, true).pipe( - mergeMap(res => { - if (this.nb.lastSaveDiffersFrom((res).content)) { - this.overwriteNotebookModal.show(); // show confirm dialog - return EMPTY; - } else { - return this._notebooks.updateNotebook(this.path, this.nb.notebook); - } - }) - ).subscribe(res => { - if (showSuccessToast) { - this._toast.success('Notebook was saved.'); - } - this.nb.markAsSaved(res.last_modified); - }, () => { - this._toast.error('An error occurred while uploading the notebook.'); - }); - } - - /** - * Upload to notebook without confirmation. - * The previous version is overwritten. - */ - overwriteNotebook(showSuccessToast = true) { - this.overwriting = true; - this._notebooks.updateNotebook(this.path, this.nb.notebook).subscribe(res => { - if (showSuccessToast) { - this._toast.success('Notebook was saved.'); - } - this.nb?.markAsSaved(res.last_modified); - }, () => { - this._toast.error('An error occurred while uploading the notebook.'); - }).add(() => { - this.overwriteNotebookModal.hide(); - this.overwriting = false; - }); - } - - revertNotebook() { - this.loadNotebook(); - this._toast.success('Notebook was reverted.'); - this.overwriteNotebookModal.hide(); - - } - - interruptKernel() { - this._notebooks.interruptKernel(this.session.kernel.id).subscribe(() => { - }, () => { - this._toast.error('Unable to interrupt the kernel.'); - }); - } - - requestRestart(executeAll: boolean = false) { - this.executeAllAfterRestart = executeAll; - this.restartKernelModal.show(); - } - - restartKernel() { - this._notebooks.restartKernel(this.session.kernel.id).pipe( - tap(() => this.nb.setKernelStatusBusy()), - delay(2500) // time for the kernel to restart - ).subscribe(() => { - this.nb.requestExecutionState(); - if (this.executeAllAfterRestart) { + + } + + closeNotebookSubmitted() { + if (this.closeNotebookForm.value.saveChanges) { + this.overwriteNotebook(true); + } + + if (this.session && this.closeNotebookForm.value.shutDown) { + this.terminateSession(); + } + if (this.closeNbSubject) { + this.nb?.closeSocket(); + this.closeNbSubject.next(true); + this.closeNbSubject?.complete(); + } else { + this.closeEdit(true); + } + this.closeNbSubject = null; + this.closeNotebookModal.hide(); + } + + closeAndTerminate(terminateAll: boolean) { + this.closeEdit(); + if (terminateAll) { + this.terminateSessions(); + this.terminateAllKernelsModal.hide(); + } else { + this.terminateSession(); + this.terminateKernelModal.hide(); + } + } + + terminateSession() { + this._content.deleteSession(this.sessionId).subscribe(); + } + + terminateSessions() { + this._content.deleteSessions(this.path).subscribe(); + } + + rename() { + if (!this.renameNotebookForm.valid) { + return; + } + let fileName = this.renameNotebookForm.value.name; + if (!fileName.endsWith('.ipynb')) { + fileName += '.ipynb'; + } + const dirPath = this._content.directoryPath; + this._sidebar.moveFile(this.path, dirPath + '/' + fileName); + this.renameNotebookModal.hide(); + } + + duplicateNotebook() { + this._notebooks.duplicateFile(this.path, this._content.directoryPath).subscribe(() => this._content.update(), + err => this._toast.error(err.error.message, `Could not duplicate ${this.path}.`)); + } + + downloadNotebook() { + this._content.downloadNotebook(this.nb.notebook, this.name); + } + + exportNotebook() { + if (this.nb.hasChangedSinceSave()) { + this._toast.warn('Please save your changes first before exporting the notebook.', 'Info'); + return; + } + this._notebooks.getExportedNotebook(this.path, this.kernelSpec?.name).subscribe(res => { + this._content.downloadNotebook(res.content, 'exported_' + this.name); + }, + () => { + this._toast.warn('Unable to export the notebook.'); + }); + } + + trustNotebook() { + this.nb.trustAllCells(); + } + + + executeSelected(advanceToNext = false) { + this.selectedComponent?.updateSource(); + this.nb.executeCell(this.selectedCell); + if (advanceToNext) { + if (this.nb.getCellIndex(this.selectedCell.id) === this.nb.cells.length - 1) { + this.insertCell(this.selectedCell.id, true, true); + } else { + this.selectCellBelow(false); + this.forceCommandMode(); + } + } + } + + executeAboveSelected() { + this.nb.executeCells(this.selectedCell, true, false); + } + + executeSelectedAndBelow() { + this.selectedComponent?.updateSource(); + this.nb.executeCells(this.selectedCell, false, true); + } + + executeAll() { this.nb.executeAll(); - } - }, () => { - this._toast.error('Unable to restart the kernel.'); - }); - this.restartKernelModal.hide(); - } - - insertCell(id: string, below: boolean, editMode = true) { - if (this.inserting) { - return; - } - const cell = this.nb.insertCell(id, below); - this.inserting = true; - timer(50).pipe(take(1)).subscribe(() => { - // ensure enough time has passed for the cell to be added to DOM - this.selectCell(cell.id, editMode); - if (!editMode) { - this.forceCommandMode(); - } - timer(100).pipe(take(1)).subscribe(() => this.inserting = false); // prevent spam - }); - } - - moveCell(oldIdx: number, below: boolean) { - const newIdx = oldIdx + (below ? 1 : -1); - if (newIdx >= 0 && newIdx < this.nb.cells.length) { - moveItemInArray(this.nb.cells, oldIdx, newIdx); - } - } - - duplicateCell(id: string) { - this.getCellComponent(id)?.updateSource(); - const cell = this.nb.duplicateCell(id); - timer(50).pipe(take(1)).subscribe(() => { - this.selectCell(cell.id, false); - }); - } - - copySelectedCell() { - this.selectedComponent?.updateSource(); - this.copiedCell = JSON.stringify(this.selectedCell); - } - - pasteCopiedCell(below: boolean = true) { - if (this.copiedCell) { - const copyCell = this.nb.insertCopyOfCell(this.copiedCell, this.selectedCell, below); - timer(50).pipe(take(1)).subscribe(() => { - this.selectCell(copyCell.id, false); - }); - } - } - - deleteCell(id: string) { - if (this.nb.cells.length > 1) { - const cellBelow = this.nb.cellBelow(id); - this.nb.deleteCell(id); - if (cellBelow) { - this.selectCell(cellBelow.id, false); - } else { - this.selectCell(this.nb.cells[this.nb.cells.length - 1].id, false); - } - } else { - this.insertCell(id, false, false); - this.deleteCell(id); - } - } - - selectCell(id: string, editMode = false) { - const unselectId = this.selectedCell?.id; - if (id !== unselectId) { - this.selectedComponent?.editor?.blur(); - this.selectedCell = this.nb.getCell(id); - if (this.selectedCell) { - this.selectedCellType = this.nb.getCellType(this.selectedCell); - this.scrollCellIntoView(id); - if (editMode) { - this.mode = 'edit'; // if component does not yet exist - this.selectedComponent?.editMode(); - this.selectedComponent?.editor?.focus(); + } + + renderMdCells() { + this.nb.executeMdCells(); + } + + clearSelectedOutput() { + this.selectedCell.outputs = []; + this.selectedCell.execution_count = null; + } + + clearAllOutputs() { + this.nb.clearAllOutputs(); + } + + toggleExpansion() { + this.expand = !this.expand; + this.nb.setExpansionAllowed(this.expand); + this._toast.success('Variable expansion has been ' + (this.expand ? 'activated' : 'deactivated') + '.'); + } + + /** + * This first compares the modification times of when this notebook was loaded and the one on disk. + * If they are not equal, the content of the one on disk is loaded and compared. This is only done now for + * improved performance.If they differ, the overwriteNotebookModal is shown. + * Otherwise, the notebook is uploaded. + */ + uploadNotebook(showSuccessToast = true) { + this._notebooks.getContents(this.path, false).pipe( + mergeMap(res => { + if (res.last_modified === this.nb.lastModifiedWhenLoaded) { + return this._notebooks.updateNotebook(this.path, this.nb.notebook); + } else { + this.uploadNotebookWithDeepCompare(showSuccessToast); + return EMPTY; + } + }) + ).subscribe(res => { + if (showSuccessToast) { + this._toast.success('Notebook was saved.'); + } + this.nb.markAsSaved(res.last_modified); + }, () => { + this._toast.error('An error occurred while uploading the notebook.'); + + }); + } + + /** + * Checks whether the content of the notebook that is stored on disk has changed compared + * to when it was loaded. If true, the overwriteNotebookModal is shown. Otherwise, the notebook is uploaded. + */ + uploadNotebookWithDeepCompare(showSuccessToast = true) { + this._notebooks.getContents(this.path, true).pipe( + mergeMap(res => { + if (this.nb.lastSaveDiffersFrom((res).content)) { + this.overwriteNotebookModal.show(); // show confirm dialog + return EMPTY; + } else { + return this._notebooks.updateNotebook(this.path, this.nb.notebook); + } + }) + ).subscribe(res => { + if (showSuccessToast) { + this._toast.success('Notebook was saved.'); + } + this.nb.markAsSaved(res.last_modified); + }, () => { + this._toast.error('An error occurred while uploading the notebook.'); + }); + } + + /** + * Upload to notebook without confirmation. + * The previous version is overwritten. + */ + overwriteNotebook(showSuccessToast = true) { + this.overwriting = true; + this._notebooks.updateNotebook(this.path, this.nb.notebook).subscribe(res => { + if (showSuccessToast) { + this._toast.success('Notebook was saved.'); + } + this.nb?.markAsSaved(res.last_modified); + }, () => { + this._toast.error('An error occurred while uploading the notebook.'); + }).add(() => { + this.overwriteNotebookModal.hide(); + this.overwriting = false; + }); + } + + revertNotebook() { + this.loadNotebook(); + this._toast.success('Notebook was reverted.'); + this.overwriteNotebookModal.hide(); + + } + + interruptKernel() { + this._notebooks.interruptKernel(this.session.kernel.id).subscribe(() => { + }, () => { + this._toast.error('Unable to interrupt the kernel.'); + }); + } + + requestRestart(executeAll: boolean = false) { + this.executeAllAfterRestart = executeAll; + this.restartKernelModal.show(); + } + + restartKernel() { + this._notebooks.restartKernel(this.session.kernel.id).pipe( + tap(() => this.nb.setKernelStatusBusy()), + delay(2500) // time for the kernel to restart + ).subscribe(() => { + this.nb.requestExecutionState(); + if (this.executeAllAfterRestart) { + this.nb.executeAll(); + } + }, () => { + this._toast.error('Unable to restart the kernel.'); + }); + this.restartKernelModal.hide(); + } + + insertCell(id: string, below: boolean, editMode = true) { + if (this.inserting) { + return; } - } - } - } - - selectCellAbove(editMode = false) { - const cellAbove = this.nb.cellAbove(this.selectedCell.id); - if (cellAbove) { - this.selectCell(cellAbove.id, editMode); - } - } - - selectCellBelow(editMode = false) { - const cellBelow = this.nb.cellBelow(this.selectedCell.id); - if (cellBelow) { - this.selectCell(cellBelow.id, editMode); - } - } - - private scrollCellIntoView(id) { - // https://stackoverflow.com/a/37829643 - const element = document.getElementById(id); // id of the scroll to element - if (!element) { - return; - } - - if (element.getBoundingClientRect().bottom > window.innerHeight - 50) { - element.scrollIntoView({behavior: 'smooth', block: 'end', inline: 'nearest'}); - } else if (element.getBoundingClientRect().top < 100) { - element.scrollIntoView({behavior: 'smooth', block: 'start', inline: 'nearest'}); - } - } - - - keyDown(event: KeyboardEvent) { - const modifiers: number = +event.altKey + +event.ctrlKey + +event.shiftKey; - if (this.mode === 'edit') { - this.handleEditModeKey(event, modifiers); - } else if (!(event.target as HTMLElement).classList.contains('no-command-hotkey')) { - this.handleCommandModeKey(event, modifiers); - } - - } - - private handleEditModeKey(event: KeyboardEvent, modifiers: number) { - if (modifiers > 0) { - switch (event.key.toLowerCase()) { - case 'enter': - this.handleModifiedEnter(event, modifiers); - break; - case 's': - if (event.ctrlKey && modifiers === 1) { - event.preventDefault(); - this.uploadNotebook(); - } - break; - } - } else { - switch (event.key.toLowerCase()) { - case 'escape': - this.forceCommandMode(); - break; - } - } - - } - - private handleCommandModeKey(event: KeyboardEvent, modifiers: number) { - if (modifiers > 0) { - switch (event.key.toLowerCase()) { - case 'enter': - this.handleModifiedEnter(event, modifiers); - break; - case 's': - if (event.ctrlKey && modifiers === 1) { - event.preventDefault(); - this.uploadNotebook(); - } - break; - case 'v': - if (event.shiftKey && modifiers === 1) { - event.preventDefault(); - this.pasteCopiedCell(false); - break; - } - } - - } else { - switch (event.key.toLowerCase()) { - case 'enter': - event.preventDefault(); - this.selectedComponent?.editMode(); - break; - case 'a': - event.preventDefault(); - this.insertCell(this.selectedCell.id, false, false); - break; - case 'b': - event.preventDefault(); - this.insertCell(this.selectedCell.id, true, false); - break; - case 'm': - event.preventDefault(); - this.setCellType('markdown'); - break; - case 'y': - event.preventDefault(); - this.setCellType('code'); - break; - case 'p': - event.preventDefault(); - this.setCellType('poly'); - break; - case 's': - event.preventDefault(); - this.uploadNotebook(); - break; - case 'c': - event.preventDefault(); - this.copySelectedCell(); - break; - case 'v': - event.preventDefault(); - this.pasteCopiedCell(true); - break; - case 'x': - event.preventDefault(); - this.copySelectedCell(); - this.deleteCell(this.selectedCell.id); - break; - case 'arrowup': - event.preventDefault(); - this.selectCellAbove(); - break; - case 'arrowdown': - event.preventDefault(); - this.selectCellBelow(); - break; - } - } - } - - private handleModifiedEnter(event: KeyboardEvent, modifiers: number) { - if (modifiers > 1) { - return; - } - event.preventDefault(); - if (event.altKey) { - this.executeSelected(); - this.insertCell(this.selectedCell.id, true, true); - } else if (event.ctrlKey) { - this.executeSelected(); - if (this.mode === 'edit') { - this.forceCommandMode(); - } - } else if (event.shiftKey) { - this.executeSelected(true); - } - } - - private forceCommandMode() { - this.selectedComponent?.commandMode(); - document.getElementById('notebook').focus(); - } - - onTypeChange(event: Event) { - const type: CellType = (event.target as HTMLOptionElement).value; - this.setCellType(type); - } - - setCellType(type: CellType) { - const oldType = this.selectedCell.cell_type; - if (oldType === 'markdown') { - this.selectedComponent.isMdRendered = false; - } - this.nb.changeCellType(this.selectedCell, type); - this.selectedCellType = type; - this.selectedComponent.updateCellType(); - } - - getPreviewText(cell: NotebookCell) { - const source = Array.isArray(cell.source) ? cell.source[0] : cell.source.split('\n', 2)[0]; - return source?.slice(0, 50); - } - - - drop(event: CdkDragDrop) { - // https://material.angular.io/cdk/drag-drop/overview - moveItemInArray(this.nb.cells, event.previousIndex, event.currentIndex); - } - - - identify(index, item: NotebookCell) { - // https://stackoverflow.com/questions/42108217/how-to-use-trackby-with-ngfor - return item.id; - } - - private getCellComponent(id: string): NbCellComponent { - return this.cellComponents.find(c => { - return c.id === id; - }); - } - - private get selectedComponent(): NbCellComponent { - return this.cellComponents.find(c => { - return c.id === this.selectedCell.id; - }); - } + const cell = this.nb.insertCell(id, below); + this.inserting = true; + timer(50).pipe(take(1)).subscribe(() => { + // ensure enough time has passed for the cell to be added to DOM + this.selectCell(cell.id, editMode); + if (!editMode) { + this.forceCommandMode(); + } + timer(100).pipe(take(1)).subscribe(() => this.inserting = false); // prevent spam + }); + } + + moveCell(oldIdx: number, below: boolean) { + const newIdx = oldIdx + (below ? 1 : -1); + if (newIdx >= 0 && newIdx < this.nb.cells.length) { + moveItemInArray(this.nb.cells, oldIdx, newIdx); + } + } + + duplicateCell(id: string) { + this.getCellComponent(id)?.updateSource(); + const cell = this.nb.duplicateCell(id); + timer(50).pipe(take(1)).subscribe(() => { + this.selectCell(cell.id, false); + }); + } + + copySelectedCell() { + this.selectedComponent?.updateSource(); + this.copiedCell = JSON.stringify(this.selectedCell); + } + + pasteCopiedCell(below: boolean = true) { + if (this.copiedCell) { + const copyCell = this.nb.insertCopyOfCell(this.copiedCell, this.selectedCell, below); + timer(50).pipe(take(1)).subscribe(() => { + this.selectCell(copyCell.id, false); + }); + } + } + + deleteCell(id: string) { + if (this.nb.cells.length > 1) { + const cellBelow = this.nb.cellBelow(id); + this.nb.deleteCell(id); + if (cellBelow) { + this.selectCell(cellBelow.id, false); + } else { + this.selectCell(this.nb.cells[this.nb.cells.length - 1].id, false); + } + } else { + this.insertCell(id, false, false); + this.deleteCell(id); + } + } + + selectCell(id: string, editMode = false) { + const unselectId = this.selectedCell?.id; + if (id !== unselectId) { + this.selectedComponent?.editor?.blur(); + this.selectedCell = this.nb.getCell(id); + if (this.selectedCell) { + this.selectedCellType = this.nb.getCellType(this.selectedCell); + this.scrollCellIntoView(id); + if (editMode) { + this.mode = 'edit'; // if component does not yet exist + this.selectedComponent?.editMode(); + this.selectedComponent?.editor?.focus(); + } + } + } + } + + selectCellAbove(editMode = false) { + const cellAbove = this.nb.cellAbove(this.selectedCell.id); + if (cellAbove) { + this.selectCell(cellAbove.id, editMode); + } + } + + selectCellBelow(editMode = false) { + const cellBelow = this.nb.cellBelow(this.selectedCell.id); + if (cellBelow) { + this.selectCell(cellBelow.id, editMode); + } + } + + private scrollCellIntoView(id) { + // https://stackoverflow.com/a/37829643 + const element = document.getElementById(id); // id of the scroll to element + if (!element) { + return; + } + + if (element.getBoundingClientRect().bottom > window.innerHeight - 50) { + element.scrollIntoView({behavior: 'smooth', block: 'end', inline: 'nearest'}); + } else if (element.getBoundingClientRect().top < 100) { + element.scrollIntoView({behavior: 'smooth', block: 'start', inline: 'nearest'}); + } + } + + + keyDown(event: KeyboardEvent) { + const modifiers: number = +event.altKey + +event.ctrlKey + +event.shiftKey; + if (this.mode === 'edit') { + this.handleEditModeKey(event, modifiers); + } else if (!(event.target as HTMLElement).classList.contains('no-command-hotkey')) { + this.handleCommandModeKey(event, modifiers); + } + + } + + private handleEditModeKey(event: KeyboardEvent, modifiers: number) { + if (modifiers > 0) { + switch (event.key.toLowerCase()) { + case 'enter': + this.handleModifiedEnter(event, modifiers); + break; + case 's': + if (event.ctrlKey && modifiers === 1) { + event.preventDefault(); + this.uploadNotebook(); + } + break; + } + } else { + switch (event.key.toLowerCase()) { + case 'escape': + this.forceCommandMode(); + break; + } + } + + } + + private handleCommandModeKey(event: KeyboardEvent, modifiers: number) { + if (modifiers > 0) { + switch (event.key.toLowerCase()) { + case 'enter': + this.handleModifiedEnter(event, modifiers); + break; + case 's': + if (event.ctrlKey && modifiers === 1) { + event.preventDefault(); + this.uploadNotebook(); + } + break; + case 'v': + if (event.shiftKey && modifiers === 1) { + event.preventDefault(); + this.pasteCopiedCell(false); + break; + } + } + + } else { + switch (event.key.toLowerCase()) { + case 'enter': + event.preventDefault(); + this.selectedComponent?.editMode(); + break; + case 'a': + event.preventDefault(); + this.insertCell(this.selectedCell.id, false, false); + break; + case 'b': + event.preventDefault(); + this.insertCell(this.selectedCell.id, true, false); + break; + case 'm': + event.preventDefault(); + this.setCellType('markdown'); + break; + case 'y': + event.preventDefault(); + this.setCellType('code'); + break; + case 'p': + event.preventDefault(); + this.setCellType('poly'); + break; + case 's': + event.preventDefault(); + this.uploadNotebook(); + break; + case 'c': + event.preventDefault(); + this.copySelectedCell(); + break; + case 'v': + event.preventDefault(); + this.pasteCopiedCell(true); + break; + case 'x': + event.preventDefault(); + this.copySelectedCell(); + this.deleteCell(this.selectedCell.id); + break; + case 'arrowup': + event.preventDefault(); + this.selectCellAbove(); + break; + case 'arrowdown': + event.preventDefault(); + this.selectCellBelow(); + break; + } + } + } + + private handleModifiedEnter(event: KeyboardEvent, modifiers: number) { + if (modifiers > 1) { + return; + } + event.preventDefault(); + if (event.altKey) { + this.executeSelected(); + this.insertCell(this.selectedCell.id, true, true); + } else if (event.ctrlKey) { + this.executeSelected(); + if (this.mode === 'edit') { + this.forceCommandMode(); + } + } else if (event.shiftKey) { + this.executeSelected(true); + } + } + + private forceCommandMode() { + this.selectedComponent?.commandMode(); + document.getElementById('notebook').focus(); + } + + onTypeChange(event: Event) { + const type: CellType = (event.target as HTMLOptionElement).value; + this.setCellType(type); + } + + setCellType(type: CellType) { + const oldType = this.selectedCell.cell_type; + if (oldType === 'markdown') { + this.selectedComponent.isMdRendered = false; + } + this.nb.changeCellType(this.selectedCell, type); + this.selectedCellType = type; + this.selectedComponent.updateCellType(); + } + + getPreviewText(cell: NotebookCell) { + const source = Array.isArray(cell.source) ? cell.source[0] : cell.source.split('\n', 2)[0]; + return source?.slice(0, 50); + } + + + drop(event: CdkDragDrop) { + // https://material.angular.io/cdk/drag-drop/overview + moveItemInArray(this.nb.cells, event.previousIndex, event.currentIndex); + } + + + identify(index, item: NotebookCell) { + // https://stackoverflow.com/questions/42108217/how-to-use-trackby-with-ngfor + return item.id; + } + + private getCellComponent(id: string): NbCellComponent { + return this.cellComponents.find(c => { + return c.id === id; + }); + } + + private get selectedComponent(): NbCellComponent { + return this.cellComponents.find(c => { + return c.id === this.selectedCell.id; + }); + } } export type NbMode = 'edit' | 'command'; diff --git a/src/app/plugins/notebooks/components/edit-notebook/nb-cell/nb-cell.component.ts b/src/app/plugins/notebooks/components/edit-notebook/nb-cell/nb-cell.component.ts index 768dceb8..df156f3e 100644 --- a/src/app/plugins/notebooks/components/edit-notebook/nb-cell/nb-cell.component.ts +++ b/src/app/plugins/notebooks/components/edit-notebook/nb-cell/nb-cell.component.ts @@ -10,290 +10,290 @@ import {RelationalResult, Result} from '../../../../../components/data-view/mode import {FormControl, FormGroup, Validators} from '@angular/forms'; @Component({ - selector: 'app-nb-cell', - templateUrl: './nb-cell.component.html', - styleUrls: ['./nb-cell.component.scss'], + selector: 'app-nb-cell', + templateUrl: './nb-cell.component.html', + styleUrls: ['./nb-cell.component.scss'], }) export class NbCellComponent implements OnInit, AfterViewInit { - @Input() cell: NotebookCell; - @Input() isFocused: boolean; - @Input() isFirst: boolean; - @Input() isLast: boolean; - @Input() isExecuting: boolean; - @Input() mode: NbMode; - @Input() selectedCellType: CellType; // not necessarily the type of this cell - @Input() namespaces: string[]; - @Input() nbLanguage: string; - @Input() isTrusted: boolean; - @Output() modeChange = new EventEmitter(); - @Output() insert = new EventEmitter(); // true: below, false: above - @Output() move = new EventEmitter(); // true: down, false: up - @Output() duplicate = new EventEmitter(); - @Output() execute = new EventEmitter(); - @Output() changeType = new EventEmitter(); - @Output() delete = new EventEmitter(); - @Output() selected = new EventEmitter(); - @ViewChild('editor') editor: NbInputEditorComponent; - @ViewChild('cellDiv') cellDiv: ElementRef; - cellType: CellType = 'code'; - - isMouseOver = false; - resultVariable: string; - resultIsTooLong = false; - polyForm: FormGroup; - resultSet: Result; - private ansi_up = new AnsiUp(); - mdSource = ''; - errorHtml: SafeHtml; - streamHtml: SafeHtml; - isMdRendered = false; - confirmingDeletion = false; - sourceHidden = false; - outputsHidden = false; - manualExecution = false; - private readonly MAX_RESULT_SIZE = 1000; - - // https://katex.org/docs/options.html - public options: KatexOptions = { - throwOnError: false, - errorColor: '#f86c6b' - }; - - constructor(private _sanitizer: DomSanitizer, - private _markdown: MarkdownService) { - } - - ngOnInit(): void { - this.updateCellType(); - this.ansi_up.escape_html = true; // prevent xss - this.initForms(); - - if (this.cellType === 'markdown') { - this.renderMd(); - } else if (this.cellType === 'poly') { - this.renderResultSet(); - } else if (this.cellType === 'code') { - const output = this.cell.outputs.find(o => o.output_type === 'error'); - if (output) { - this.renderError(output); - } - const stream = this.cell.outputs.find(o => o.output_type === 'stream'); - if (stream) { - this.renderStream(stream); - } + @Input() cell: NotebookCell; + @Input() isFocused: boolean; + @Input() isFirst: boolean; + @Input() isLast: boolean; + @Input() isExecuting: boolean; + @Input() mode: NbMode; + @Input() selectedCellType: CellType; // not necessarily the type of this cell + @Input() namespaces: string[]; + @Input() nbLanguage: string; + @Input() isTrusted: boolean; + @Output() modeChange = new EventEmitter(); + @Output() insert = new EventEmitter(); // true: below, false: above + @Output() move = new EventEmitter(); // true: down, false: up + @Output() duplicate = new EventEmitter(); + @Output() execute = new EventEmitter(); + @Output() changeType = new EventEmitter(); + @Output() delete = new EventEmitter(); + @Output() selected = new EventEmitter(); + @ViewChild('editor') editor: NbInputEditorComponent; + @ViewChild('cellDiv') cellDiv: ElementRef; + cellType: CellType = 'code'; + + isMouseOver = false; + resultVariable: string; + resultIsTooLong = false; + polyForm: FormGroup; + resultSet: Result; + private ansi_up = new AnsiUp(); + mdSource = ''; + errorHtml: SafeHtml; + streamHtml: SafeHtml; + isMdRendered = false; + confirmingDeletion = false; + sourceHidden = false; + outputsHidden = false; + manualExecution = false; + private readonly MAX_RESULT_SIZE = 1000; + + // https://katex.org/docs/options.html + public options: KatexOptions = { + throwOnError: false, + errorColor: '#f86c6b' + }; + + constructor(private _sanitizer: DomSanitizer, + private _markdown: MarkdownService) { } - this.sourceHidden = this.cell.metadata.jupyter?.source_hidden; - this.outputsHidden = this.cell.metadata.jupyter?.outputs_hidden; - this.manualExecution = this.cell.metadata.polypheny?.manual_execution; - } - - ngAfterViewInit(): void { - this.editor.setCode(this.source); - if (this.isFocused && this.mode === 'edit') { - this.editor.focus(); + + ngOnInit(): void { + this.updateCellType(); + this.ansi_up.escape_html = true; // prevent xss + this.initForms(); + + if (this.cellType === 'markdown') { + this.renderMd(); + } else if (this.cellType === 'poly') { + this.renderResultSet(); + } else if (this.cellType === 'code') { + const output = this.cell.outputs.find(o => o.output_type === 'error'); + if (output) { + this.renderError(output); + } + const stream = this.cell.outputs.find(o => o.output_type === 'stream'); + if (stream) { + this.renderStream(stream); + } + } + this.sourceHidden = this.cell.metadata.jupyter?.source_hidden; + this.outputsHidden = this.cell.metadata.jupyter?.outputs_hidden; + this.manualExecution = this.cell.metadata.polypheny?.manual_execution; } - } - - private initForms() { - this.polyForm = new FormGroup({ - // default value should be equal to default value in NotebookWrapper when type is changed - language: new FormControl(this.cell.metadata.polypheny?.language || 'sql'), - namespace: new FormControl(this.cell.metadata.polypheny?.namespace || 'public'), - variable: new FormControl(this.cell.metadata.polypheny?.result_variable || 'result', [ - Validators.pattern('[a-zA-Z_][a-zA-Z0-9_]*'), // first symbol can't be digit - Validators.maxLength(30), - Validators.required - ]), - expand: new FormControl(this.cell.metadata.polypheny?.expand_params || true), // turn off expansion for single cells - }); - if (this.cellType === 'poly') { // cell-type was already poly when loaded - this.cell.metadata.polypheny.language = this.polyForm.value.language; - this.cell.metadata.polypheny.namespace = this.polyForm.value.namespace; - this.cell.metadata.polypheny.result_variable = this.polyForm.value.variable; - this.cell.metadata.polypheny.expand_params = this.polyForm.value.expand; + + ngAfterViewInit(): void { + this.editor.setCode(this.source); + if (this.isFocused && this.mode === 'edit') { + this.editor.focus(); + } } - } - - onFocus(event: MouseEvent) { - if (!this.isFocused) { - const target = event.target as HTMLElement; - if (!target.classList.contains('no-select-cell')) { - this.selected.emit(this.cell.id); - } + + private initForms() { + this.polyForm = new FormGroup({ + // default value should be equal to default value in NotebookWrapper when type is changed + language: new FormControl(this.cell.metadata.polypheny?.language || 'sql'), + namespace: new FormControl(this.cell.metadata.polypheny?.namespace || 'public'), + variable: new FormControl(this.cell.metadata.polypheny?.result_variable || 'result', [ + Validators.pattern('[a-zA-Z_][a-zA-Z0-9_]*'), // first symbol can't be digit + Validators.maxLength(30), + Validators.required + ]), + expand: new FormControl(this.cell.metadata.polypheny?.expand_params || true), // turn off expansion for single cells + }); + if (this.cellType === 'poly') { // cell-type was already poly when loaded + this.cell.metadata.polypheny.language = this.polyForm.value.language; + this.cell.metadata.polypheny.namespace = this.polyForm.value.namespace; + this.cell.metadata.polypheny.result_variable = this.polyForm.value.variable; + this.cell.metadata.polypheny.expand_params = this.polyForm.value.expand; + } } - } - onEditorFocus() { - this.editMode(); - } + onFocus(event: MouseEvent) { + if (!this.isFocused) { + const target = event.target as HTMLElement; + if (!target.classList.contains('no-select-cell')) { + this.selected.emit(this.cell.id); + } + } + } - onEditorBlur() { - this.commandMode(); - } + onEditorFocus() { + this.editMode(); + } - editMode() { - if (this.mode === 'edit') { - return; + onEditorBlur() { + this.commandMode(); } - if (this.isMdRendered) { - this.editor.setCode(this.source); - this.isMdRendered = false; + + editMode() { + if (this.mode === 'edit') { + return; + } + if (this.isMdRendered) { + this.editor.setCode(this.source); + this.isMdRendered = false; + } + this.editor.focus(); + this.mode = 'edit'; + this.modeChange.emit(this.mode); + this.setSourceHidden(false); } - this.editor.focus(); - this.mode = 'edit'; - this.modeChange.emit(this.mode); - this.setSourceHidden(false); - } - commandMode() { - this.editor.blur(); - this.mode = 'command'; - this.modeChange.emit(this.mode); - } + commandMode() { + this.editor.blur(); + this.mode = 'command'; + this.modeChange.emit(this.mode); + } - onMouseEnter() { - this.isMouseOver = true; + onMouseEnter() { + this.isMouseOver = true; - } + } - onMouseLeave() { - this.isMouseOver = false; - this.updateSource(); - } + onMouseLeave() { + this.isMouseOver = false; + this.updateSource(); + } - executeCell() { - this.updateSource(); - this.execute.emit(this.id); - } + executeCell() { + this.updateSource(); + this.execute.emit(this.id); + } - updateSource() { - if (!this.isMdRendered) { - this.cell.source = this.editor.getCode(); + updateSource() { + if (!this.isMdRendered) { + this.cell.source = this.editor.getCode(); + } } - } - renderMd() { - this.mdSource = this.source; - if (!this.mdSource?.trim()) { - this.mdSource = '`Empty Markdown Cell`'; + renderMd() { + this.mdSource = this.source; + if (!this.mdSource?.trim()) { + this.mdSource = '`Empty Markdown Cell`'; + } + this.isMdRendered = true; } - this.isMdRendered = true; - } - renderError(output: CellErrorOutput) { - if (output) { - this.errorHtml = this._sanitizer.bypassSecurityTrustHtml(this.ansi_up.ansi_to_html(output.traceback.join('\n'))); + renderError(output: CellErrorOutput) { + if (output) { + this.errorHtml = this._sanitizer.bypassSecurityTrustHtml(this.ansi_up.ansi_to_html(output.traceback.join('\n'))); + } } - } - renderStream(output: CellStreamOutput) { - if (output) { - const text = Array.isArray(output.text) ? output.text.join('\n') : output.text; - this.streamHtml = this._sanitizer.bypassSecurityTrustHtml(this.ansi_up.ansi_to_html(text)); + renderStream(output: CellStreamOutput) { + if (output) { + const text = Array.isArray(output.text) ? output.text.join('\n') : output.text; + this.streamHtml = this._sanitizer.bypassSecurityTrustHtml(this.ansi_up.ansi_to_html(text)); + } } - } - - renderResultSet() { - if (this.cellType === 'poly') { - const output = this.cell.outputs.find(o => o.output_type === 'display_data' - && (o).data['application/json']); - if (output) { - const jsonResult = (output.data['application/json']); - if (jsonResult.affectedTuples > this.MAX_RESULT_SIZE && jsonResult.data?.length >= this.MAX_RESULT_SIZE) { - this.resultIsTooLong = true; - jsonResult.data = jsonResult.data.slice(0, this.MAX_RESULT_SIZE); - } else { - this.resultIsTooLong = false; + + renderResultSet() { + if (this.cellType === 'poly') { + const output = this.cell.outputs.find(o => o.output_type === 'display_data' + && (o).data['application/json']); + if (output) { + const jsonResult = (output.data['application/json']); + if (jsonResult.affectedTuples > this.MAX_RESULT_SIZE && jsonResult.data?.length >= this.MAX_RESULT_SIZE) { + this.resultIsTooLong = true; + jsonResult.data = jsonResult.data.slice(0, this.MAX_RESULT_SIZE); + } else { + this.resultIsTooLong = false; + } + this.resultVariable = output.metadata.polypheny?.result_variable; + this.resultSet = jsonResult; + } else { + this.resultSet = null; + } + } + } + + deleteCell() { + if (!this.confirmingDeletion) { + this.confirmingDeletion = true; + return; + } + this.delete.emit(this.id); + } + + resetDeleteConfirm() { + this.confirmingDeletion = false; + } + + onReady() { + const images = document.querySelectorAll('.nb-markdown img'); + for (let i = 0; i < images.length; i++) { + const image = images[i] as HTMLElement; + image.style.maxWidth = '100%'; + } + + const tables = document.querySelectorAll('.nb-markdown table'); + for (let i = 0; i < tables.length; i++) { + const table = tables[i] as HTMLElement; + table.classList.add('table', 'table-hover', 'table-striped', 'table-borderless'); //'table table-hover table-striped table-borderless' } - this.resultVariable = output.metadata.polypheny?.result_variable; - this.resultSet = jsonResult; - } else { - this.resultSet = null; - } } - } - deleteCell() { - if (!this.confirmingDeletion) { - this.confirmingDeletion = true; - return; + updateCellType() { + if (this.cell.metadata.polypheny?.cell_type === 'poly') { + this.cellType = 'poly'; + } else { + this.cellType = this.cell.cell_type; + } + this.manualExecution = this.cell.metadata.polypheny?.manual_execution; + this.sourceHidden = this.cell.metadata.jupyter?.source_hidden; + this.outputsHidden = this.cell.metadata.jupyter?.outputs_hidden; } - this.delete.emit(this.id); - } - - resetDeleteConfirm() { - this.confirmingDeletion = false; - } - - onReady() { - const images = document.querySelectorAll('.nb-markdown img'); - for (let i = 0; i < images.length; i++) { - const image = images[i] as HTMLElement; - image.style.maxWidth = '100%'; + + changedNamespace() { + this.cell.metadata.polypheny.namespace = this.polyForm.value.namespace; } - const tables = document.querySelectorAll('.nb-markdown table'); - for (let i = 0; i < tables.length; i++) { - const table = tables[i] as HTMLElement; - table.classList.add('table', 'table-hover', 'table-striped', 'table-borderless'); //'table table-hover table-striped table-borderless' + changedLanguage() { + this.cell.metadata.polypheny.language = this.polyForm.value.language; } - } - updateCellType() { - if (this.cell.metadata.polypheny?.cell_type === 'poly') { - this.cellType = 'poly'; - } else { - this.cellType = this.cell.cell_type; + changedVariableName() { + this.cell.metadata.polypheny.result_variable = this.polyForm.valid ? this.polyForm.value.variable : ''; } - this.manualExecution = this.cell.metadata.polypheny?.manual_execution; - this.sourceHidden = this.cell.metadata.jupyter?.source_hidden; - this.outputsHidden = this.cell.metadata.jupyter?.outputs_hidden; - } - - changedNamespace() { - this.cell.metadata.polypheny.namespace = this.polyForm.value.namespace; - } - - changedLanguage() { - this.cell.metadata.polypheny.language = this.polyForm.value.language; - } - - changedVariableName() { - this.cell.metadata.polypheny.result_variable = this.polyForm.valid ? this.polyForm.value.variable : ''; - } - - toggledManual() { - this.manualExecution = !this.manualExecution; - if (!this.cell.metadata.polypheny) { - this.cell.metadata.polypheny = {}; + + toggledManual() { + this.manualExecution = !this.manualExecution; + if (!this.cell.metadata.polypheny) { + this.cell.metadata.polypheny = {}; + } + this.cell.metadata.polypheny.manual_execution = this.manualExecution; } - this.cell.metadata.polypheny.manual_execution = this.manualExecution; - } - setSourceHidden(hide: boolean) { - this.sourceHidden = hide; - if (!this.cell.metadata.jupyter) { - this.cell.metadata.jupyter = {}; + setSourceHidden(hide: boolean) { + this.sourceHidden = hide; + if (!this.cell.metadata.jupyter) { + this.cell.metadata.jupyter = {}; + } + this.cell.metadata.jupyter.source_hidden = hide; } - this.cell.metadata.jupyter.source_hidden = hide; - } - setOutputsHidden(hide: boolean) { - this.outputsHidden = hide; - if (!this.cell.metadata.jupyter) { - this.cell.metadata.jupyter = {}; + setOutputsHidden(hide: boolean) { + this.outputsHidden = hide; + if (!this.cell.metadata.jupyter) { + this.cell.metadata.jupyter = {}; + } + this.cell.metadata.jupyter.outputs_hidden = hide; } - this.cell.metadata.jupyter.outputs_hidden = hide; - } - get source(): string { - return (typeof this.cell.source === 'string') ? - this.cell.source : this.cell.source.join(''); - } + get source(): string { + return (typeof this.cell.source === 'string') ? + this.cell.source : this.cell.source.join(''); + } - get id(): string { - return this.cell.id; - } + get id(): string { + return this.cell.id; + } } diff --git a/src/app/plugins/notebooks/components/edit-notebook/nb-input-editor/nb-input-editor.component.ts b/src/app/plugins/notebooks/components/edit-notebook/nb-input-editor/nb-input-editor.component.ts index 9cec6c7e..5623c7fd 100644 --- a/src/app/plugins/notebooks/components/edit-notebook/nb-input-editor/nb-input-editor.component.ts +++ b/src/app/plugins/notebooks/components/edit-notebook/nb-input-editor/nb-input-editor.component.ts @@ -3,76 +3,76 @@ import {CellType} from '../notebook-wrapper'; import {EditorComponent} from '../../../../../components/editor/editor.component'; @Component({ - selector: 'app-nb-input-editor', - templateUrl: './nb-input-editor.component.html', - styleUrls: ['./nb-input-editor.component.scss'] + selector: 'app-nb-input-editor', + templateUrl: './nb-input-editor.component.html', + styleUrls: ['./nb-input-editor.component.scss'] }) export class NbInputEditorComponent implements OnInit, OnChanges, AfterViewInit { - @Input() type: CellType; - @Input() nbLanguage: string; // cannot change while a notebook is open - @Input() polyLanguage: 'cypher' | 'mql' | 'cql' | 'sql' | 'pig'; - @ViewChild('editor') editor: EditorComponent; + @Input() type: CellType; + @Input() nbLanguage: string; // cannot change while a notebook is open + @Input() polyLanguage: 'cypher' | 'mql' | 'cql' | 'sql' | 'pig'; + @ViewChild('editor') editor: EditorComponent; - editorLanguage = 'python'; + editorLanguage = 'python'; - editorOptions = { - minLines: 4, - maxLines: 60, - showLineNumbers: false, - highlightGutterLine: false, - highlightActiveLine: false, - fontSize: '0.875rem' - }; + editorOptions = { + minLines: 4, + maxLines: 60, + showLineNumbers: false, + highlightGutterLine: false, + highlightActiveLine: false, + fontSize: '0.875rem' + }; - constructor() { - } + constructor() { + } - ngOnInit(): void { - } + ngOnInit(): void { + } - ngAfterViewInit(): void { - this.editor.setScrollMargin(5, 5); - } + ngAfterViewInit(): void { + this.editor.setScrollMargin(5, 5); + } - ngOnChanges(changes: SimpleChanges): void { - if (changes.type) { - this.updateEditorLanguage(); - } else if (changes.polyLanguage) { - this.updateEditorLanguage(); + ngOnChanges(changes: SimpleChanges): void { + if (changes.type) { + this.updateEditorLanguage(); + } else if (changes.polyLanguage) { + this.updateEditorLanguage(); + } } - } - setCode(code: string) { - this.editor.setCode(code); - } + setCode(code: string) { + this.editor.setCode(code); + } - getCode(): string { - return this.editor.getCode(); - } + getCode(): string { + return this.editor.getCode(); + } - focus() { - this.editor.focus(); - } + focus() { + this.editor.focus(); + } - blur() { - this.editor.blur(); - } + blur() { + this.editor.blur(); + } - private updateEditorLanguage() { - switch (this.type) { - case 'markdown': - this.editorLanguage = 'markdown'; - break; - case 'code': - this.editorLanguage = this.nbLanguage; - break; - case 'poly': - this.editorLanguage = this.polyLanguage; - break; - case 'raw': - this.editorLanguage = 'python'; // could be anything + private updateEditorLanguage() { + switch (this.type) { + case 'markdown': + this.editorLanguage = 'markdown'; + break; + case 'code': + this.editorLanguage = this.nbLanguage; + break; + case 'poly': + this.editorLanguage = this.polyLanguage; + break; + case 'raw': + this.editorLanguage = 'python'; // could be anything + } } - } } diff --git a/src/app/plugins/notebooks/components/edit-notebook/nb-output-data/nb-output-data.component.ts b/src/app/plugins/notebooks/components/edit-notebook/nb-output-data/nb-output-data.component.ts index 185a96c6..11bb11d1 100644 --- a/src/app/plugins/notebooks/components/edit-notebook/nb-output-data/nb-output-data.component.ts +++ b/src/app/plugins/notebooks/components/edit-notebook/nb-output-data/nb-output-data.component.ts @@ -2,18 +2,18 @@ import {Component, Input, OnInit} from '@angular/core'; import {KernelData} from '../../../models/kernel-response.model'; @Component({ - selector: 'app-nb-output-data', - templateUrl: './nb-output-data.component.html', - styleUrls: ['./nb-output-data.component.scss'] + selector: 'app-nb-output-data', + templateUrl: './nb-output-data.component.html', + styleUrls: ['./nb-output-data.component.scss'] }) export class NbOutputDataComponent implements OnInit { - @Input() data: KernelData; - @Input() isTrusted = false; + @Input() data: KernelData; + @Input() isTrusted = false; - constructor() { - } + constructor() { + } - ngOnInit(): void { - } + ngOnInit(): void { + } } diff --git a/src/app/plugins/notebooks/components/edit-notebook/nb-poly-output/nb-poly-output.component.ts b/src/app/plugins/notebooks/components/edit-notebook/nb-poly-output/nb-poly-output.component.ts index a549feed..d668d755 100644 --- a/src/app/plugins/notebooks/components/edit-notebook/nb-poly-output/nb-poly-output.component.ts +++ b/src/app/plugins/notebooks/components/edit-notebook/nb-poly-output/nb-poly-output.component.ts @@ -3,29 +3,29 @@ import {EntityConfig} from '../../../../../components/data-view/data-table/entit import {Result} from '../../../../../components/data-view/models/result-set.model'; @Component({ - selector: 'app-db-poly-output', - templateUrl: './nb-poly-output.component.html', - styleUrls: ['./nb-poly-output.component.scss'] + selector: 'app-db-poly-output', + templateUrl: './nb-poly-output.component.html', + styleUrls: ['./nb-poly-output.component.scss'] }) export class NbPolyOutputComponent implements OnInit { - @Input() resultSet: Result; - @Input() resultVariable: string; - @Input() resultIsTooLong: boolean; + @Input() resultSet: Result; + @Input() resultVariable: string; + @Input() resultIsTooLong: boolean; - tableConfig: EntityConfig = { - create: false, - update: false, - delete: false, - sort: false, - search: false, - exploring: false, - hideCreateView: true - }; + tableConfig: EntityConfig = { + create: false, + update: false, + delete: false, + sort: false, + search: false, + exploring: false, + hideCreateView: true + }; - constructor() { - } + constructor() { + } - ngOnInit(): void { - } + ngOnInit(): void { + } } diff --git a/src/app/plugins/notebooks/components/edit-notebook/notebook-wrapper.ts b/src/app/plugins/notebooks/components/edit-notebook/notebook-wrapper.ts index f1bbbf28..856d5f4c 100644 --- a/src/app/plugins/notebooks/components/edit-notebook/notebook-wrapper.ts +++ b/src/app/plugins/notebooks/components/edit-notebook/notebook-wrapper.ts @@ -1,483 +1,503 @@ import {KernelSpec, NotebookContent} from '../../models/notebooks-response.model'; -import {CellDisplayDataOutput, CellErrorOutput, CellExecuteResultOutput, CellOutputType, CellStreamOutput, Notebook, NotebookCell} from '../../models/notebook.model'; +import { + CellDisplayDataOutput, + CellErrorOutput, + CellExecuteResultOutput, + CellOutputType, + CellStreamOutput, + Notebook, + NotebookCell +} from '../../models/notebook.model'; import * as uuid from 'uuid'; import {NotebooksWebSocket} from '../../services/notebooks-webSocket'; -import {KernelDisplayData, KernelErrorMsg, KernelExecuteInput, KernelExecuteReply, KernelExecuteResult, KernelInterruptReply, KernelMsg, KernelShutdownReply, KernelStatus, KernelStream, KernelUpdateDisplayData} from '../../models/kernel-response.model'; +import { + KernelDisplayData, + KernelErrorMsg, + KernelExecuteInput, + KernelExecuteReply, + KernelExecuteResult, + KernelInterruptReply, + KernelMsg, + KernelShutdownReply, + KernelStatus, + KernelStream, + KernelUpdateDisplayData +} from '../../models/kernel-response.model'; import {interval, Subscription} from 'rxjs'; export class NotebookWrapper { - private nb: Notebook; - private codeOrigin: Map = new Map(); - private copyNbJson: string; - kernelStatus: 'unknown' | 'idle' | 'busy' | 'starting' = 'unknown'; - lastModifiedWhenLoaded: string; - private keepAlive: Subscription; - trustedCellIds: Set = new Set(); // cells that were executed by the user -> trust html outputs - - constructor(private content: NotebookContent, - private busyCellIds: Set, - private socket: NotebooksWebSocket, - private onExecuteMdCell: (id: string) => void, - private onReceivedErrorOutput: (id: string, output: CellErrorOutput) => void, - private onReceivedStreamOutput: (id: string, output: CellStreamOutput) => void, - private onRenderPolyCell: (id: string) => void) { - this.nb = content.content; - this.markAsSaved(content.last_modified); - this.validateIds(); - if (!this.nb.metadata.polypheny?.expand_params) { - const metadata = this.nb.metadata.polypheny || {}; - metadata.expand_params = false; - this.nb.metadata.polypheny = metadata; - } - this.busyCellIds.clear(); - this.socket.onMessage().subscribe(msg => this.handleKernelMsg(msg), - err => { - console.warn('received error: ' + err); - }, () => { - this.socket = null; - this.kernelStatus = 'unknown'; - }); - this.socket.requestExecutionState(); - this.keepAlive = interval(60000).subscribe(() => this.socket?.requestExecutionState()); // prevent 300s timeout - } - - closeSocket() { - this.keepAlive?.unsubscribe(); - this.socket?.close(); - } - - requestExecutionState() { - this.socket?.requestExecutionState(); - } - - markAsSaved(lastModified: string) { - this.lastModifiedWhenLoaded = lastModified; - this.copyNbJson = JSON.stringify(this.nb); - } - - hasChangedSinceSave(): boolean { - return this.copyNbJson && this.copyNbJson !== JSON.stringify(this.nb); - } - - lastSaveDiffersFrom(other: Notebook): boolean { - return JSON.stringify(other) !== this.copyNbJson; - } - - private validateIds() { - const ids = new Set(); - for (const cell of this.nb.cells) { - if (cell.id == null || ids.has(cell.id)) { - cell.id = uuid.v4(); - } - ids.add(cell.id); - } - } - - insertCellByIdx(i: number, below: boolean = true): NotebookCell { - const newCell = this.getEmptyCell(); - this.nb.cells.splice(i + +below, 0, newCell); - return newCell; - } - - insertCell(id: string, below: boolean = true): NotebookCell { - const i = this.getCellIndex(id); - if (i !== -1) { - const newCell = this.getEmptyCell(); - this.nb.cells.splice(i + +below, 0, newCell); - return newCell; - } - return null; - } - - insertCopyOfCell(jsonCell: string, reference: NotebookCell, below: boolean = true): NotebookCell { - const i = this.nb.cells.indexOf(reference); - if (i !== -1) { - const copyCell = this.getCopyOfCell(jsonCell); - this.nb.cells.splice(i + +below, 0, copyCell); - return copyCell; - } - return null; - - } - - duplicateCell(id: string) { - const originalCell = this.getCell(id); - if (originalCell) { - const copyCell = this.getCopyOfCell(JSON.stringify(originalCell)); - this.cells.splice(this.getCellIndex(id) + 1, 0, copyCell); - return copyCell; - } - return null; - } - - deleteCell(id: string) { - const i = this.getCellIndex(id); - if (i !== -1) { - this.nb.cells.splice(i, 1); - } - } - - /** - * Returns the index of the cell with the given id. - * -1 if no cell was found. - */ - getCellIndex(id: string) { - return this.nb.cells.findIndex(cell => cell.id === id); - } - - getCell(id: string) { - return this.nb.cells.find(cell => cell.id === id); - } - - cellAbove(id: string) { - const idx = this.getCellIndex(id); - return idx < 1 ? null : this.cells[idx - 1]; - } - - cellBelow(id: string) { - const idx = this.getCellIndex(id); - return idx === this.cells.length - 1 ? null : this.cells[idx + 1]; - } - - executeAll() { - for (const cell of this.nb.cells) { - this.executeCell(cell, true); - } - } - - executeCells(reference: NotebookCell, above: boolean, refAndBelow: boolean) { - const idx = this.nb.cells.indexOf(reference); - if (idx < 0) { - return; - } - if (above) { - for (const cell of this.nb.cells.slice(0, idx)) { - this.executeCell(cell, true); - } - } - if (refAndBelow) { - this.executeCell(reference, false); - for (const cell of this.nb.cells.slice(idx + 1)) { - this.executeCell(cell, true); - } - } - } - - executeCell(cell: NotebookCell | string, automatic = false) { - if (typeof cell === 'string') { - cell = this.getCell(cell); - } - if (!cell || (automatic && cell.metadata.polypheny?.manual_execution)) { - return; - } - switch (this.getCellType(cell)) { - case 'code': - if (this.socket) { - cell.outputs = []; - const msgId = this.socket?.sendCode(cell.source); - this.codeOrigin.set(msgId, cell.id); - this.busyCellIds.add(cell.id); + private nb: Notebook; + private codeOrigin: Map = new Map(); + private copyNbJson: string; + kernelStatus: 'unknown' | 'idle' | 'busy' | 'starting' = 'unknown'; + lastModifiedWhenLoaded: string; + private keepAlive: Subscription; + trustedCellIds: Set = new Set(); // cells that were executed by the user -> trust html outputs + + constructor(private content: NotebookContent, + private busyCellIds: Set, + private socket: NotebooksWebSocket, + private onExecuteMdCell: (id: string) => void, + private onReceivedErrorOutput: (id: string, output: CellErrorOutput) => void, + private onReceivedStreamOutput: (id: string, output: CellStreamOutput) => void, + private onRenderPolyCell: (id: string) => void) { + this.nb = content.content; + this.markAsSaved(content.last_modified); + this.validateIds(); + if (!this.nb.metadata.polypheny?.expand_params) { + const metadata = this.nb.metadata.polypheny || {}; + metadata.expand_params = false; + this.nb.metadata.polypheny = metadata; } - break; - case 'markdown': - this.onExecuteMdCell(cell.id); - break; - case 'poly': - if (this.socket) { - cell.outputs = []; - const poly = cell.metadata.polypheny; - const id = this.socket.sendQuery(cell.source, poly.language, poly.namespace, - poly.result_variable || '_', this.isExpansionAllowed() && poly.expand_params); - this.codeOrigin.set(id, cell.id); - this.busyCellIds.add(cell.id); + this.busyCellIds.clear(); + this.socket.onMessage().subscribe(msg => this.handleKernelMsg(msg), + err => { + console.warn('received error: ' + err); + }, () => { + this.socket = null; + this.kernelStatus = 'unknown'; + }); + this.socket.requestExecutionState(); + this.keepAlive = interval(60000).subscribe(() => this.socket?.requestExecutionState()); // prevent 300s timeout + } + + closeSocket() { + this.keepAlive?.unsubscribe(); + this.socket?.close(); + } + + requestExecutionState() { + this.socket?.requestExecutionState(); + } + + markAsSaved(lastModified: string) { + this.lastModifiedWhenLoaded = lastModified; + this.copyNbJson = JSON.stringify(this.nb); + } + + hasChangedSinceSave(): boolean { + return this.copyNbJson && this.copyNbJson !== JSON.stringify(this.nb); + } + + lastSaveDiffersFrom(other: Notebook): boolean { + return JSON.stringify(other) !== this.copyNbJson; + } + + private validateIds() { + const ids = new Set(); + for (const cell of this.nb.cells) { + if (cell.id == null || ids.has(cell.id)) { + cell.id = uuid.v4(); + } + ids.add(cell.id); + } + } + + insertCellByIdx(i: number, below: boolean = true): NotebookCell { + const newCell = this.getEmptyCell(); + this.nb.cells.splice(i + +below, 0, newCell); + return newCell; + } + + insertCell(id: string, below: boolean = true): NotebookCell { + const i = this.getCellIndex(id); + if (i !== -1) { + const newCell = this.getEmptyCell(); + this.nb.cells.splice(i + +below, 0, newCell); + return newCell; + } + return null; + } + + insertCopyOfCell(jsonCell: string, reference: NotebookCell, below: boolean = true): NotebookCell { + const i = this.nb.cells.indexOf(reference); + if (i !== -1) { + const copyCell = this.getCopyOfCell(jsonCell); + this.nb.cells.splice(i + +below, 0, copyCell); + return copyCell; } - break; - default: + return null; + } - this.trustedCellIds.add(cell.id); - } - executeMdCells() { - for (const cell of this.nb.cells) { - if (cell.cell_type === 'markdown') { - this.executeCell(cell); - } + duplicateCell(id: string) { + const originalCell = this.getCell(id); + if (originalCell) { + const copyCell = this.getCopyOfCell(JSON.stringify(originalCell)); + this.cells.splice(this.getCellIndex(id) + 1, 0, copyCell); + return copyCell; + } + return null; } - } - trustAllCells() { - for (const cell of this.nb.cells) { - if (cell.cell_type !== 'markdown') { + deleteCell(id: string) { + const i = this.getCellIndex(id); + if (i !== -1) { + this.nb.cells.splice(i, 1); + } + } + + /** + * Returns the index of the cell with the given id. + * -1 if no cell was found. + */ + getCellIndex(id: string) { + return this.nb.cells.findIndex(cell => cell.id === id); + } + + getCell(id: string) { + return this.nb.cells.find(cell => cell.id === id); + } + + cellAbove(id: string) { + const idx = this.getCellIndex(id); + return idx < 1 ? null : this.cells[idx - 1]; + } + + cellBelow(id: string) { + const idx = this.getCellIndex(id); + return idx === this.cells.length - 1 ? null : this.cells[idx + 1]; + } + + executeAll() { + for (const cell of this.nb.cells) { + this.executeCell(cell, true); + } + } + + executeCells(reference: NotebookCell, above: boolean, refAndBelow: boolean) { + const idx = this.nb.cells.indexOf(reference); + if (idx < 0) { + return; + } + if (above) { + for (const cell of this.nb.cells.slice(0, idx)) { + this.executeCell(cell, true); + } + } + if (refAndBelow) { + this.executeCell(reference, false); + for (const cell of this.nb.cells.slice(idx + 1)) { + this.executeCell(cell, true); + } + } + } + + executeCell(cell: NotebookCell | string, automatic = false) { + if (typeof cell === 'string') { + cell = this.getCell(cell); + } + if (!cell || (automatic && cell.metadata.polypheny?.manual_execution)) { + return; + } + switch (this.getCellType(cell)) { + case 'code': + if (this.socket) { + cell.outputs = []; + const msgId = this.socket?.sendCode(cell.source); + this.codeOrigin.set(msgId, cell.id); + this.busyCellIds.add(cell.id); + } + break; + case 'markdown': + this.onExecuteMdCell(cell.id); + break; + case 'poly': + if (this.socket) { + cell.outputs = []; + const poly = cell.metadata.polypheny; + const id = this.socket.sendQuery(cell.source, poly.language, poly.namespace, + poly.result_variable || '_', this.isExpansionAllowed() && poly.expand_params); + this.codeOrigin.set(id, cell.id); + this.busyCellIds.add(cell.id); + } + break; + default: + } this.trustedCellIds.add(cell.id); - } - } - } - - setKernelSpec(kernel: KernelSpec) { - this.nb.metadata.kernelspec = { - display_name: kernel.spec.display_name, - language: kernel.spec.language, - name: kernel.name - }; - } - - changeCellType(cell: NotebookCell, type: CellType) { - const oldType = this.getCellType(cell); - if (oldType === type) { - return; - } - switch (oldType) { - case 'code': - delete cell.outputs; - delete cell.execution_count; - break; - case 'markdown': - break; - case 'raw': - break; - case 'poly': - delete cell.outputs; - delete cell.execution_count; - delete cell.metadata.polypheny?.cell_type; - break; - } - const oldMeta = cell.metadata.polypheny; - switch (type) { - case 'code': - cell.outputs = []; - cell.execution_count = null; - cell.metadata = {polypheny: oldMeta}; - break; - case 'markdown': - cell.metadata = {}; - break; - case 'raw': - cell.metadata = {}; - break; - case 'poly': - cell.outputs = []; - cell.execution_count = null; - cell.metadata = { - polypheny: { - cell_type: 'poly', language: oldMeta?.language || 'sql', - namespace: oldMeta?.namespace || 'public', - result_variable: oldMeta?.result_variable || 'result', - expand_params: oldMeta?.expand_params || true, // nb-lvl expand_params also needs to be true to work - manual_execution: oldMeta?.manual_execution || false - } + } + + executeMdCells() { + for (const cell of this.nb.cells) { + if (cell.cell_type === 'markdown') { + this.executeCell(cell); + } + } + } + + trustAllCells() { + for (const cell of this.nb.cells) { + if (cell.cell_type !== 'markdown') { + this.trustedCellIds.add(cell.id); + } + } + } + + setKernelSpec(kernel: KernelSpec) { + this.nb.metadata.kernelspec = { + display_name: kernel.spec.display_name, + language: kernel.spec.language, + name: kernel.name + }; + } + + changeCellType(cell: NotebookCell, type: CellType) { + const oldType = this.getCellType(cell); + if (oldType === type) { + return; + } + switch (oldType) { + case 'code': + delete cell.outputs; + delete cell.execution_count; + break; + case 'markdown': + break; + case 'raw': + break; + case 'poly': + delete cell.outputs; + delete cell.execution_count; + delete cell.metadata.polypheny?.cell_type; + break; + } + const oldMeta = cell.metadata.polypheny; + switch (type) { + case 'code': + cell.outputs = []; + cell.execution_count = null; + cell.metadata = {polypheny: oldMeta}; + break; + case 'markdown': + cell.metadata = {}; + break; + case 'raw': + cell.metadata = {}; + break; + case 'poly': + cell.outputs = []; + cell.execution_count = null; + cell.metadata = { + polypheny: { + cell_type: 'poly', language: oldMeta?.language || 'sql', + namespace: oldMeta?.namespace || 'public', + result_variable: oldMeta?.result_variable || 'result', + expand_params: oldMeta?.expand_params || true, // nb-lvl expand_params also needs to be true to work + manual_execution: oldMeta?.manual_execution || false + } + }; + break; + default: + return; + } + cell.cell_type = type === 'poly' ? 'code' : type; + } + + clearAllOutputs() { + for (const cell of this.cells) { + if (cell.cell_type === 'code') { + cell.outputs = []; + cell.execution_count = null; + } + } + } + + private handleKernelMsg(msg: KernelMsg) { + const cellId = this.codeOrigin.get(msg?.parent_header?.msg_id); + if (!cellId && msg.msg_type !== 'status') { + return; // we ignore messages from other users in the same session, except for status updates + } + if (msg.msg_type === 'status') { + this.handleStatusMsg(msg); + return; + } + const cell = this.getCell(cellId); + if (!cell) { + return; + } + + switch (msg.msg_type) { + case 'execute_input': + this.handleInputMsg(msg, cell); + break; + case 'execute_reply': + this.handleReplyMsg(msg, cell); + break; + case 'stream': + this.handleStreamMsg(msg, cell); + break; + case 'execute_result': + this.handleResultMsg(msg, cell); + break; + case 'error': + this.handleErrorMsg(msg, cell); + break; + case 'interrupt_reply': + this.handleInterruptMsg(msg, cell); + break; + case 'shutdown_reply': + this.handleShutdownMsg(msg, cell); + break; + case 'display_data': + this.handleDisplayMsg(msg, cell); + break; + case 'update_display_data': + this.handleUpdateDisplayMsg(msg, cell); + break; + } + } + + private handleStatusMsg(msg: KernelStatus) { + this.kernelStatus = msg.content.execution_state; + } + + private handleInputMsg(msg: KernelExecuteInput, cell: NotebookCell) { + this.busyCellIds.add(cell.id); + cell.execution_count = msg.content.execution_count; + } + + private handleReplyMsg(msg: KernelExecuteReply, cell: NotebookCell) { + this.busyCellIds.delete(cell.id); + if (msg.content.status === 'ok') { + cell.execution_count = msg.content.execution_count; + } + } + + private handleStreamMsg(msg: KernelStream, cell: NotebookCell) { + if (this.getCellType(cell) === 'poly') { + return; + } + let lastOutput = cell.outputs[cell.outputs.length - 1]; + if (lastOutput && lastOutput.output_type === 'stream') { + lastOutput = lastOutput; + if (lastOutput.name === msg.content.name) { + this.addTextToOutputStream(lastOutput, msg.content.text); + this.onReceivedStreamOutput(cell.id, lastOutput); + return; + } + + } + const output: CellStreamOutput = { + output_type: 'stream', + name: msg.content.name, + text: [msg.content.text] }; - break; - default: - return; - } - cell.cell_type = type === 'poly' ? 'code' : type; - } - - clearAllOutputs() { - for (const cell of this.cells) { - if (cell.cell_type === 'code') { - cell.outputs = []; - cell.execution_count = null; - } - } - } - - private handleKernelMsg(msg: KernelMsg) { - const cellId = this.codeOrigin.get(msg?.parent_header?.msg_id); - if (!cellId && msg.msg_type !== 'status') { - return; // we ignore messages from other users in the same session, except for status updates - } - if (msg.msg_type === 'status') { - this.handleStatusMsg(msg); - return; - } - const cell = this.getCell(cellId); - if (!cell) { - return; - } - - switch (msg.msg_type) { - case 'execute_input': - this.handleInputMsg(msg, cell); - break; - case 'execute_reply': - this.handleReplyMsg(msg, cell); - break; - case 'stream': - this.handleStreamMsg(msg, cell); - break; - case 'execute_result': - this.handleResultMsg(msg, cell); - break; - case 'error': - this.handleErrorMsg(msg, cell); - break; - case 'interrupt_reply': - this.handleInterruptMsg(msg, cell); - break; - case 'shutdown_reply': - this.handleShutdownMsg(msg, cell); - break; - case 'display_data': - this.handleDisplayMsg(msg, cell); - break; - case 'update_display_data': - this.handleUpdateDisplayMsg(msg, cell); - break; - } - } - - private handleStatusMsg(msg: KernelStatus) { - this.kernelStatus = msg.content.execution_state; - } - - private handleInputMsg(msg: KernelExecuteInput, cell: NotebookCell) { - this.busyCellIds.add(cell.id); - cell.execution_count = msg.content.execution_count; - } - - private handleReplyMsg(msg: KernelExecuteReply, cell: NotebookCell) { - this.busyCellIds.delete(cell.id); - if (msg.content.status === 'ok') { - cell.execution_count = msg.content.execution_count; - } - } - - private handleStreamMsg(msg: KernelStream, cell: NotebookCell) { - if (this.getCellType(cell) === 'poly') { - return; - } - let lastOutput = cell.outputs[cell.outputs.length - 1]; - if (lastOutput && lastOutput.output_type === 'stream') { - lastOutput = lastOutput; - if (lastOutput.name === msg.content.name) { - this.addTextToOutputStream(lastOutput, msg.content.text); - this.onReceivedStreamOutput(cell.id, lastOutput); - return; - } - - } - const output: CellStreamOutput = { - output_type: 'stream', - name: msg.content.name, - text: [msg.content.text] - }; - cell.outputs.push(output); - this.onReceivedStreamOutput(cell.id, output); - } - - private handleResultMsg(msg: KernelExecuteResult, cell: NotebookCell) { - this.busyCellIds.delete(cell.id); - if (this.getCellType(cell) === 'poly') { - return; - } - const output = msg.content as CellExecuteResultOutput; - output.output_type = msg.msg_type as CellOutputType; - cell.outputs.push(output); - } - - private handleErrorMsg(msg: KernelErrorMsg, cell: NotebookCell) { - const output = msg.content as CellErrorOutput; - output.output_type = msg.msg_type as CellOutputType; - cell.outputs.push(output); - this.busyCellIds.delete(cell.id); - this.onReceivedErrorOutput(cell.id, output); - } - - private handleInterruptMsg(msg: KernelInterruptReply, cell: NotebookCell) { - this.busyCellIds.clear(); - } - - private handleShutdownMsg(msg: KernelShutdownReply, cell: NotebookCell) { - this.busyCellIds.clear(); - } - - private handleDisplayMsg(msg: KernelDisplayData, cell: NotebookCell) { - const output = msg.content as CellDisplayDataOutput; - output.output_type = msg.msg_type as CellOutputType; - delete output['transient']; // not supported yet - cell.outputs.push(output); - if (output.data['application/json'] && this.getCellType(cell) === 'poly') { - output.metadata.polypheny = {result_variable: cell.metadata.polypheny.result_variable || '_'}; - this.onRenderPolyCell(cell.id); - } - } - - private handleUpdateDisplayMsg(msg: KernelUpdateDisplayData, cell: NotebookCell) { - this.handleDisplayMsg(msg, cell); // not correctly supported yet - } - - getCellType(cell: NotebookCell): CellType { - if (cell.cell_type === 'code') { - if (cell.metadata?.polypheny?.cell_type === 'poly') { - return 'poly'; - } - } - return cell.cell_type; - } - - private addTextToOutputStream(lastOutput: CellStreamOutput, newText: string) { - let text = lastOutput.text; - if (Array.isArray(text)) { - text = text.join(''); - } else if (!text) { - text = ''; - } - - // Carriage return should 'overwrite' the last line -> enables dynamic progress bars - const splitText = newText.split('\r'); - text += splitText[0]; - for (let i = 1; i < splitText.length; i++) { - const line = splitText[i]; - const lastNewlineIdx = text.lastIndexOf('\n'); - if (line.charAt(0) === '\n' || lastNewlineIdx === -1) { - text += line; // \r\n -> normal newline - continue; - } - text = text.slice(0, lastNewlineIdx + 1) + line; - } - lastOutput.text = text; - } - - private getEmptyCell(): NotebookCell { - return { - cell_type: 'code', - id: uuid.v4(), - metadata: {}, - source: [], - execution_count: null, - outputs: [] - }; - } - - private getCopyOfCell(jsonCell: string): NotebookCell { - const copyCell = JSON.parse(jsonCell); - copyCell.id = uuid.v4(); - return copyCell; - } - - /** - * Useful to set to signal to the user the UI is waiting for the kernel to restart. - */ - setKernelStatusBusy() { - this.kernelStatus = 'busy'; - } - - get cells() { - return this.nb.cells; - } - - get notebook() { - return this.nb; - } - - isExpansionAllowed(): boolean { - return this.nb.metadata.polypheny?.expand_params; - } - - setExpansionAllowed(allowed: boolean) { - this.nb.metadata.polypheny.expand_params = allowed; - } + cell.outputs.push(output); + this.onReceivedStreamOutput(cell.id, output); + } + + private handleResultMsg(msg: KernelExecuteResult, cell: NotebookCell) { + this.busyCellIds.delete(cell.id); + if (this.getCellType(cell) === 'poly') { + return; + } + const output = msg.content as CellExecuteResultOutput; + output.output_type = msg.msg_type as CellOutputType; + cell.outputs.push(output); + } + + private handleErrorMsg(msg: KernelErrorMsg, cell: NotebookCell) { + const output = msg.content as CellErrorOutput; + output.output_type = msg.msg_type as CellOutputType; + cell.outputs.push(output); + this.busyCellIds.delete(cell.id); + this.onReceivedErrorOutput(cell.id, output); + } + + private handleInterruptMsg(msg: KernelInterruptReply, cell: NotebookCell) { + this.busyCellIds.clear(); + } + + private handleShutdownMsg(msg: KernelShutdownReply, cell: NotebookCell) { + this.busyCellIds.clear(); + } + + private handleDisplayMsg(msg: KernelDisplayData, cell: NotebookCell) { + const output = msg.content as CellDisplayDataOutput; + output.output_type = msg.msg_type as CellOutputType; + delete output['transient']; // not supported yet + cell.outputs.push(output); + if (output.data['application/json'] && this.getCellType(cell) === 'poly') { + output.metadata.polypheny = {result_variable: cell.metadata.polypheny.result_variable || '_'}; + this.onRenderPolyCell(cell.id); + } + } + + private handleUpdateDisplayMsg(msg: KernelUpdateDisplayData, cell: NotebookCell) { + this.handleDisplayMsg(msg, cell); // not correctly supported yet + } + + getCellType(cell: NotebookCell): CellType { + if (cell.cell_type === 'code') { + if (cell.metadata?.polypheny?.cell_type === 'poly') { + return 'poly'; + } + } + return cell.cell_type; + } + + private addTextToOutputStream(lastOutput: CellStreamOutput, newText: string) { + let text = lastOutput.text; + if (Array.isArray(text)) { + text = text.join(''); + } else if (!text) { + text = ''; + } + + // Carriage return should 'overwrite' the last line -> enables dynamic progress bars + const splitText = newText.split('\r'); + text += splitText[0]; + for (let i = 1; i < splitText.length; i++) { + const line = splitText[i]; + const lastNewlineIdx = text.lastIndexOf('\n'); + if (line.charAt(0) === '\n' || lastNewlineIdx === -1) { + text += line; // \r\n -> normal newline + continue; + } + text = text.slice(0, lastNewlineIdx + 1) + line; + } + lastOutput.text = text; + } + + private getEmptyCell(): NotebookCell { + return { + cell_type: 'code', + id: uuid.v4(), + metadata: {}, + source: [], + execution_count: null, + outputs: [] + }; + } + + private getCopyOfCell(jsonCell: string): NotebookCell { + const copyCell = JSON.parse(jsonCell); + copyCell.id = uuid.v4(); + return copyCell; + } + + /** + * Useful to set to signal to the user the UI is waiting for the kernel to restart. + */ + setKernelStatusBusy() { + this.kernelStatus = 'busy'; + } + + get cells() { + return this.nb.cells; + } + + get notebook() { + return this.nb; + } + + isExpansionAllowed(): boolean { + return this.nb.metadata.polypheny?.expand_params; + } + + setExpansionAllowed(allowed: boolean) { + this.nb.metadata.polypheny.expand_params = allowed; + } } export type CellType = 'code' | 'markdown' | 'raw' | 'poly'; diff --git a/src/app/plugins/notebooks/components/manage-notebook/manage-notebook.component.ts b/src/app/plugins/notebooks/components/manage-notebook/manage-notebook.component.ts index ebe2b0f0..42e108b7 100644 --- a/src/app/plugins/notebooks/components/manage-notebook/manage-notebook.component.ts +++ b/src/app/plugins/notebooks/components/manage-notebook/manage-notebook.component.ts @@ -11,205 +11,205 @@ import {UtilService} from '../../../../services/util.service'; import {ToasterService} from '../../../../components/toast-exposer/toaster.service'; @Component({ - selector: 'app-manage-notebook', - templateUrl: './manage-notebook.component.html', - styleUrls: ['./manage-notebook.component.scss'] + selector: 'app-manage-notebook', + templateUrl: './manage-notebook.component.html', + styleUrls: ['./manage-notebook.component.scss'] }) export class ManageNotebookComponent implements OnInit, OnDestroy { - private readonly _router = inject(Router); - private readonly _sidebar = inject(NotebooksSidebarService); - private readonly _content = inject(NotebooksContentService); - private readonly _notebooks = inject(NotebooksService); - private readonly _toast = inject(ToasterService); - public readonly _util = inject(UtilService); - - metadata: Content; - private parentPath: string; - private directoryPath: string; - sessions: SessionResponse[]; - availableKernels: KernelSpec[] = []; - selectedSession: SessionResponse; - statusText = 'No Kernel running'; - confirmingDeletion = false; - creating = false; - deleting = false; - renameFileForm: FormGroup; - terminateSessionForm: FormGroup; - connectSessionForm: FormGroup; - createKernelForm: FormGroup; - @ViewChild('renameFileModal', {static: false}) public renameFileModal: ModalDirective; - @ViewChild('terminateSessionModal', {static: false}) public terminateSessionModal: ModalDirective; - @ViewChild('connectSessionModal', {static: false}) public connectSessionModal: ModalDirective; - @ViewChild('createKernelModal', {static: false}) public createKernelModal: ModalDirective; - private subscriptions = new Subscription(); - - constructor() { - } - - ngOnInit(): void { - this.initForms(); - - this._content.onContentChange().subscribe(() => { - this.metadata = this._content.metadata; - this.parentPath = this._content.parentPath; - this.directoryPath = this._content.directoryPath; - if (!this.renameFileModal.isShown) { + private readonly _router = inject(Router); + private readonly _sidebar = inject(NotebooksSidebarService); + private readonly _content = inject(NotebooksContentService); + private readonly _notebooks = inject(NotebooksService); + private readonly _toast = inject(ToasterService); + public readonly _util = inject(UtilService); + + metadata: Content; + private parentPath: string; + private directoryPath: string; + sessions: SessionResponse[]; + availableKernels: KernelSpec[] = []; + selectedSession: SessionResponse; + statusText = 'No Kernel running'; + confirmingDeletion = false; + creating = false; + deleting = false; + renameFileForm: FormGroup; + terminateSessionForm: FormGroup; + connectSessionForm: FormGroup; + createKernelForm: FormGroup; + @ViewChild('renameFileModal', {static: false}) public renameFileModal: ModalDirective; + @ViewChild('terminateSessionModal', {static: false}) public terminateSessionModal: ModalDirective; + @ViewChild('connectSessionModal', {static: false}) public connectSessionModal: ModalDirective; + @ViewChild('createKernelModal', {static: false}) public createKernelModal: ModalDirective; + private subscriptions = new Subscription(); + + constructor() { + } + + ngOnInit(): void { + this.initForms(); + + this._content.onContentChange().subscribe(() => { + this.metadata = this._content.metadata; + this.parentPath = this._content.parentPath; + this.directoryPath = this._content.directoryPath; + if (!this.renameFileModal.isShown) { + this.renameFileForm.patchValue({name: this.metadata.name}); + } + this.updateSessions(this._content.onSessionsChange().getValue()); + }); + + this.metadata = this._content.metadata; + this.parentPath = this._content.parentPath; + this.directoryPath = this._content.directoryPath; this.renameFileForm.patchValue({name: this.metadata.name}); - } - this.updateSessions(this._content.onSessionsChange().getValue()); - }); - - this.metadata = this._content.metadata; - this.parentPath = this._content.parentPath; - this.directoryPath = this._content.directoryPath; - this.renameFileForm.patchValue({name: this.metadata.name}); - - const sub1 = this._content.onSessionsChange().subscribe(sessions => this.updateSessions(sessions)); - const sub2 = this._content.onKernelSpecsChange().subscribe(specs => this.updateAvailableKernels(specs)); - this.subscriptions.add(sub1); - this.subscriptions.add(sub2); - } - - ngOnDestroy() { - this.subscriptions.unsubscribe(); - } - - private initForms() { - this.renameFileForm = new FormGroup({ - name: new FormControl('', [ - Validators.pattern('[a-zA-Z0-9_. \-]*[a-zA-Z0-9_\-]'), // last symbol can't be '.' or ' ' - Validators.maxLength(50), - Validators.required - ]) - }); - this.terminateSessionForm = new FormGroup({ - session: new FormControl('', Validators.required), - }); - this.connectSessionForm = new FormGroup({ - session: new FormControl('', Validators.required), - }); - this.createKernelForm = new FormGroup({ - kernel: new FormControl('', Validators.required), - }); - this.terminateSessionForm.get('session').valueChanges.subscribe(val => this.updateSelectedSession(val)); - this.connectSessionForm.get('session').valueChanges.subscribe(val => this.updateSelectedSession(val)); - } - - deleteFile() { - if (!this.confirmingDeletion) { - this.confirmingDeletion = true; - return; - } - if (this.metadata.type === 'notebook' && this.sessions.length > 0) { - this.terminateAllSessions(); - } - this._notebooks.deleteFile(this.metadata.path).subscribe(res => { - const path = this.metadata.type === 'directory' ? this.parentPath : this.directoryPath; - this._router.navigate([this._sidebar.baseUrl].concat(path.split('/'))); - }, - err => { - this._toast.error(`Could not delete ${this.metadata.path}.`); + + const sub1 = this._content.onSessionsChange().subscribe(sessions => this.updateSessions(sessions)); + const sub2 = this._content.onKernelSpecsChange().subscribe(specs => this.updateAvailableKernels(specs)); + this.subscriptions.add(sub1); + this.subscriptions.add(sub2); + } + + ngOnDestroy() { + this.subscriptions.unsubscribe(); + } + + private initForms() { + this.renameFileForm = new FormGroup({ + name: new FormControl('', [ + Validators.pattern('[a-zA-Z0-9_. \-]*[a-zA-Z0-9_\-]'), // last symbol can't be '.' or ' ' + Validators.maxLength(50), + Validators.required + ]) + }); + this.terminateSessionForm = new FormGroup({ + session: new FormControl('', Validators.required), + }); + this.connectSessionForm = new FormGroup({ + session: new FormControl('', Validators.required), + }); + this.createKernelForm = new FormGroup({ + kernel: new FormControl('', Validators.required), + }); + this.terminateSessionForm.get('session').valueChanges.subscribe(val => this.updateSelectedSession(val)); + this.connectSessionForm.get('session').valueChanges.subscribe(val => this.updateSelectedSession(val)); + } + + deleteFile() { + if (!this.confirmingDeletion) { + this.confirmingDeletion = true; + return; } - ); - } - - resetDeleteConfirm() { - this.confirmingDeletion = false; - } - - duplicateFile() { - this._notebooks.duplicateFile(this.metadata.path, this.directoryPath).subscribe(res => this._content.update(), - err => this._toast.error(`Could not duplicate ${this.metadata.path}.`)); - } - - renameFile() { - if (!this.renameFileForm.valid) { - return; - } - let fileName = this.renameFileForm.value.name; - if (this.metadata.type === 'notebook' && !fileName.endsWith('.ipynb')) { - fileName += '.ipynb'; - } - const path = this.metadata.type === 'directory' ? this.parentPath : this.directoryPath; - this._sidebar.moveFile(this.metadata.path, path + '/' + fileName); - this.renameFileModal.hide(); - } - - downloadFile() { - this._content.downloadFile(); - } - - connect() { - this.connectSessionModal.hide(); - this.openSession(this.connectSessionForm.value.session); - } - - create() { - this.creating = true; - this._notebooks.createSession(this.metadata.name, this.metadata.path, - this.createKernelForm.value.kernel, true).subscribe(res => { - this._content.addSession(res); - this.openSession(res.id); - }, - err => this._toast.error('Could not create session.') - ).add(() => { - this.creating = false; - this.createKernelModal.hide(); - }); - } - - terminateSession() { - const id = this.terminateSessionForm.value.session; - this.deleting = true; - this._content.deleteSession(id).subscribe( - res => { - this.deleting = false; - this.terminateSessionModal.hide(); + if (this.metadata.type === 'notebook' && this.sessions.length > 0) { + this.terminateAllSessions(); } - ); - } - - terminateAllSessions() { - this.deleting = true; - this._content.deleteSessions(this.metadata.path).subscribe( - res => { - this.deleting = false; - this.terminateSessionModal.hide(); + this._notebooks.deleteFile(this.metadata.path).subscribe(res => { + const path = this.metadata.type === 'directory' ? this.parentPath : this.directoryPath; + this._router.navigate([this._sidebar.baseUrl].concat(path.split('/'))); + }, + err => { + this._toast.error(`Could not delete ${this.metadata.path}.`); + } + ); + } + + resetDeleteConfirm() { + this.confirmingDeletion = false; + } + + duplicateFile() { + this._notebooks.duplicateFile(this.metadata.path, this.directoryPath).subscribe(res => this._content.update(), + err => this._toast.error(`Could not duplicate ${this.metadata.path}.`)); + } + + renameFile() { + if (!this.renameFileForm.valid) { + return; } - ); - } - - private updateSessions(sessions: SessionResponse[]) { - if (this.metadata.type === 'notebook') { - this.sessions = sessions.filter(session => session.path.startsWith(this.metadata.path)); - const s = this.sessions.length > 1 ? 's' : ''; - if (this.sessions.length > 0) { - this.statusText = this.sessions.length + ' Kernel' + s + ' running'; - this.connectSessionForm.patchValue({session: this.sessions[0].id}); - this.terminateSessionForm.patchValue({session: this.sessions[0].id}); - } else { - this.statusText = 'No Kernel running'; - } - } - } - - private openSession(sessionId: string) { - const queryParams = {session: sessionId}; - this._router.navigate([this._sidebar.baseUrl].concat(this.metadata.path.split('/')), {queryParams}); - } - - private updateAvailableKernels(kernelSpecs: KernelSpecs) { - if (kernelSpecs == null) { - this.availableKernels = []; - } else { - this.availableKernels = Object.values(kernelSpecs.kernelspecs); - this.createKernelForm.patchValue({kernel: kernelSpecs.default}); - } - } - - private updateSelectedSession(sessionId: string) { - this.selectedSession = this.sessions.find(session => session.id === sessionId); - } + let fileName = this.renameFileForm.value.name; + if (this.metadata.type === 'notebook' && !fileName.endsWith('.ipynb')) { + fileName += '.ipynb'; + } + const path = this.metadata.type === 'directory' ? this.parentPath : this.directoryPath; + this._sidebar.moveFile(this.metadata.path, path + '/' + fileName); + this.renameFileModal.hide(); + } + + downloadFile() { + this._content.downloadFile(); + } + + connect() { + this.connectSessionModal.hide(); + this.openSession(this.connectSessionForm.value.session); + } + + create() { + this.creating = true; + this._notebooks.createSession(this.metadata.name, this.metadata.path, + this.createKernelForm.value.kernel, true).subscribe(res => { + this._content.addSession(res); + this.openSession(res.id); + }, + err => this._toast.error('Could not create session.') + ).add(() => { + this.creating = false; + this.createKernelModal.hide(); + }); + } + + terminateSession() { + const id = this.terminateSessionForm.value.session; + this.deleting = true; + this._content.deleteSession(id).subscribe( + res => { + this.deleting = false; + this.terminateSessionModal.hide(); + } + ); + } + + terminateAllSessions() { + this.deleting = true; + this._content.deleteSessions(this.metadata.path).subscribe( + res => { + this.deleting = false; + this.terminateSessionModal.hide(); + } + ); + } + + private updateSessions(sessions: SessionResponse[]) { + if (this.metadata.type === 'notebook') { + this.sessions = sessions.filter(session => session.path.startsWith(this.metadata.path)); + const s = this.sessions.length > 1 ? 's' : ''; + if (this.sessions.length > 0) { + this.statusText = this.sessions.length + ' Kernel' + s + ' running'; + this.connectSessionForm.patchValue({session: this.sessions[0].id}); + this.terminateSessionForm.patchValue({session: this.sessions[0].id}); + } else { + this.statusText = 'No Kernel running'; + } + } + } + + private openSession(sessionId: string) { + const queryParams = {session: sessionId}; + this._router.navigate([this._sidebar.baseUrl].concat(this.metadata.path.split('/')), {queryParams}); + } + + private updateAvailableKernels(kernelSpecs: KernelSpecs) { + if (kernelSpecs == null) { + this.availableKernels = []; + } else { + this.availableKernels = Object.values(kernelSpecs.kernelspecs); + this.createKernelForm.patchValue({kernel: kernelSpecs.default}); + } + } + + private updateSelectedSession(sessionId: string) { + this.selectedSession = this.sessions.find(session => session.id === sessionId); + } } diff --git a/src/app/plugins/notebooks/components/notebooks-dashboard/notebooks-dashboard.component.ts b/src/app/plugins/notebooks/components/notebooks-dashboard/notebooks-dashboard.component.ts index ea3c61d7..0af77ecd 100644 --- a/src/app/plugins/notebooks/components/notebooks-dashboard/notebooks-dashboard.component.ts +++ b/src/app/plugins/notebooks/components/notebooks-dashboard/notebooks-dashboard.component.ts @@ -8,175 +8,175 @@ import {ToasterService} from '../../../../components/toast-exposer/toaster.servi import {cilMediaPlay, cilReload, cilTrash} from '@coreui/icons'; @Component({ - selector: 'app-notebooks-dashboard', - templateUrl: './notebooks-dashboard.component.html', - styleUrls: ['./notebooks-dashboard.component.scss'] + selector: 'app-notebooks-dashboard', + templateUrl: './notebooks-dashboard.component.html', + styleUrls: ['./notebooks-dashboard.component.scss'] }) export class NotebooksDashboardComponent implements OnInit, OnDestroy { - @ViewChild('terminateSessionsModal') public terminateSessionsModal: ModalDirective; - @ViewChild('terminateUnusedSessionsModal') public terminateUnusedSessionsModal: ModalDirective; - @ViewChild('restartContainerModal') public restartContainerModal: ModalDirective; - @ViewChild('destroyContainerModal') public destroyContainerModal: ModalDirective; - @ViewChild('startContainerModal') public startContainerModal: ModalDirective; - - private readonly _notebooks = inject(NotebooksService); - private readonly _content = inject(NotebooksContentService); - private readonly _toast = inject(ToasterService); - - icons = {cilReload, cilTrash, cilMediaPlay}; - private subscriptions = new Subscription(); - sessions: SessionResponse[] = []; - hasUnusedSessions = false; - notebookPaths: string[] = []; - isPreferredSession: boolean[] = []; - creating = false; - deleting = false; - serverStatus: StatusResponse; - pluginLoaded = false; - sessionSubscription = null; - instances = []; - - constructor() { - } - - ngOnInit(): void { - this.getServerStatus(); - this._notebooks.getDockerInstances().subscribe({ - next: res => { - this.instances = <[]>res; - }, - error: err => { - console.log(err); - } - }); - this.getPluginStatus(); - - const sub = interval(10000).subscribe(() => { - this.getServerStatus(); - }); - this.subscriptions.add(sub); - } - - ngOnDestroy(): void { - this.subscriptions.unsubscribe(); - } - - subscribeToSessionChanges() { - if (this.sessionSubscription !== null) { - return; + @ViewChild('terminateSessionsModal') public terminateSessionsModal: ModalDirective; + @ViewChild('terminateUnusedSessionsModal') public terminateUnusedSessionsModal: ModalDirective; + @ViewChild('restartContainerModal') public restartContainerModal: ModalDirective; + @ViewChild('destroyContainerModal') public destroyContainerModal: ModalDirective; + @ViewChild('startContainerModal') public startContainerModal: ModalDirective; + + private readonly _notebooks = inject(NotebooksService); + private readonly _content = inject(NotebooksContentService); + private readonly _toast = inject(ToasterService); + + icons = {cilReload, cilTrash, cilMediaPlay}; + private subscriptions = new Subscription(); + sessions: SessionResponse[] = []; + hasUnusedSessions = false; + notebookPaths: string[] = []; + isPreferredSession: boolean[] = []; + creating = false; + deleting = false; + serverStatus: StatusResponse; + pluginLoaded = false; + sessionSubscription = null; + instances = []; + + constructor() { } - this.sessionSubscription = this._content.onSessionsChange().subscribe(res => { - this.updateSessions(res); - this.hasUnusedSessions = res.some(session => session.kernel?.connections === 0); - }); - } - - unsubscribeFromSessionChanges() { - if (this.sessionSubscription !== null) { - this.sessionSubscription.unsubscribe(); - this.sessionSubscription = null; + + ngOnInit(): void { + this.getServerStatus(); + this._notebooks.getDockerInstances().subscribe({ + next: res => { + this.instances = <[]>res; + }, + error: err => { + console.log(err); + } + }); + this.getPluginStatus(); + + const sub = interval(10000).subscribe(() => { + this.getServerStatus(); + }); + this.subscriptions.add(sub); } - } - - terminateSessions(onlyUnused = false): void { - this.deleting = true; - this._content.deleteAllSessions(onlyUnused).subscribe().add(() => { - this.deleting = false; - this.terminateSessionsModal.hide(); - this.terminateUnusedSessionsModal.hide(); - }); - } - - updateSessions(sessions: SessionResponse[]) { - this.sessions = sessions.slice().sort((a, b) => { - if (a.path < b.path) { - return -1; - } else { - return a.path > b.path ? 1 : 0; - } - }); - const paths = this.sessions.map(s => this._notebooks.getPathFromSession(s)); - this.notebookPaths = paths.map(p => p.replace('notebooks/', '') - .replace(/\//g, '/\u200B')); // zero-width space => allow soft line breaks after '/' - this.isPreferredSession = this.sessions.map((s, i) => - this._content.getPreferredSessionId(paths[i]) === s.id - ); - } - - showStartContainerModal() { - this._notebooks.getDockerInstances().subscribe({ - next: res => { - this.instances = <[]>res; - }, - error: err => { - console.log(err); - } - }); - } - - createContainer(id: number) { - this.creating = true; - this._notebooks.createContainer(id).subscribe({ - next: res => { - this.startContainerModal.hide(); - this.creating = false; - }, - error: err => { - this.creating = false; - console.log(err); - } - }).add(() => this.getServerStatus()); - } - - destroyContainer() { - this._notebooks.destroyContainer().subscribe({ - next: res => this.destroyContainerModal.hide(), - error: err => console.log(err), - }).add(() => this.getServerStatus()); - } - - restartContainer() { - this.serverStatus = null; - this.restartContainerModal.hide(); - this._notebooks.restartContainer().subscribe({ - next: res => { - this._toast.success('Successfully restarted the container.'); - this._content.updateSessions(); - this._content.update(); - }, - error: err => { - this._toast.error('An error occurred while restarting the container!'); - } - }).add(() => this.getServerStatus()); - } - - getPluginStatus() { - this._notebooks.getPluginStatus().subscribe({ - next: res => { - this.pluginLoaded = true; - }, error: () => { - this.pluginLoaded = false; - this._content.setAutoUpdate(false); + + ngOnDestroy(): void { this.subscriptions.unsubscribe(); - } - }); - } - - getServerStatus() { - this._notebooks.getStatus().subscribe({ - next: res => { - this.serverStatus = res; - this._content.updateAvailableKernels(); - this._content.updateSessions(); - this.subscribeToSessionChanges(); - }, - error: () => { + } + + subscribeToSessionChanges() { + if (this.sessionSubscription !== null) { + return; + } + this.sessionSubscription = this._content.onSessionsChange().subscribe(res => { + this.updateSessions(res); + this.hasUnusedSessions = res.some(session => session.kernel?.connections === 0); + }); + } + + unsubscribeFromSessionChanges() { + if (this.sessionSubscription !== null) { + this.sessionSubscription.unsubscribe(); + this.sessionSubscription = null; + } + } + + terminateSessions(onlyUnused = false): void { + this.deleting = true; + this._content.deleteAllSessions(onlyUnused).subscribe().add(() => { + this.deleting = false; + this.terminateSessionsModal.hide(); + this.terminateUnusedSessionsModal.hide(); + }); + } + + updateSessions(sessions: SessionResponse[]) { + this.sessions = sessions.slice().sort((a, b) => { + if (a.path < b.path) { + return -1; + } else { + return a.path > b.path ? 1 : 0; + } + }); + const paths = this.sessions.map(s => this._notebooks.getPathFromSession(s)); + this.notebookPaths = paths.map(p => p.replace('notebooks/', '') + .replace(/\//g, '/\u200B')); // zero-width space => allow soft line breaks after '/' + this.isPreferredSession = this.sessions.map((s, i) => + this._content.getPreferredSessionId(paths[i]) === s.id + ); + } + + showStartContainerModal() { + this._notebooks.getDockerInstances().subscribe({ + next: res => { + this.instances = <[]>res; + }, + error: err => { + console.log(err); + } + }); + } + + createContainer(id: number) { + this.creating = true; + this._notebooks.createContainer(id).subscribe({ + next: res => { + this.startContainerModal.hide(); + this.creating = false; + }, + error: err => { + this.creating = false; + console.log(err); + } + }).add(() => this.getServerStatus()); + } + + destroyContainer() { + this._notebooks.destroyContainer().subscribe({ + next: res => this.destroyContainerModal.hide(), + error: err => console.log(err), + }).add(() => this.getServerStatus()); + } + + restartContainer() { this.serverStatus = null; - this.unsubscribeFromSessionChanges(); - this._content.setAutoUpdate(false); - this.getPluginStatus(); - } - }); - } + this.restartContainerModal.hide(); + this._notebooks.restartContainer().subscribe({ + next: res => { + this._toast.success('Successfully restarted the container.'); + this._content.updateSessions(); + this._content.update(); + }, + error: err => { + this._toast.error('An error occurred while restarting the container!'); + } + }).add(() => this.getServerStatus()); + } + + getPluginStatus() { + this._notebooks.getPluginStatus().subscribe({ + next: res => { + this.pluginLoaded = true; + }, error: () => { + this.pluginLoaded = false; + this._content.setAutoUpdate(false); + this.subscriptions.unsubscribe(); + } + }); + } + + getServerStatus() { + this._notebooks.getStatus().subscribe({ + next: res => { + this.serverStatus = res; + this._content.updateAvailableKernels(); + this._content.updateSessions(); + this.subscribeToSessionChanges(); + }, + error: () => { + this.serverStatus = null; + this.unsubscribeFromSessionChanges(); + this._content.setAutoUpdate(false); + this.getPluginStatus(); + } + }); + } } diff --git a/src/app/plugins/notebooks/components/notebooks.component.ts b/src/app/plugins/notebooks/components/notebooks.component.ts index d9705009..ae313496 100644 --- a/src/app/plugins/notebooks/components/notebooks.component.ts +++ b/src/app/plugins/notebooks/components/notebooks.component.ts @@ -1,6 +1,13 @@ import {Component, ElementRef, inject, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {NotebooksService} from '../services/notebooks.service'; -import {ActivatedRoute, ActivatedRouteSnapshot, CanDeactivate, Router, RouterStateSnapshot, UrlSegment} from '@angular/router'; +import { + ActivatedRoute, + ActivatedRouteSnapshot, + CanDeactivate, + Router, + RouterStateSnapshot, + UrlSegment +} from '@angular/router'; import {ModalDirective} from 'ngx-bootstrap/modal'; import {FormControl, FormGroup, Validators} from '@angular/forms'; import {mergeMap, tap} from 'rxjs/operators'; @@ -14,367 +21,367 @@ import {EditNotebookComponent} from './edit-notebook/edit-notebook.component'; import {ToasterService} from '../../../components/toast-exposer/toaster.service'; @Component({ - selector: 'app-notebooks', - templateUrl: './notebooks.component.html', - styleUrls: ['./notebooks.component.scss'] + selector: 'app-notebooks', + templateUrl: './notebooks.component.html', + styleUrls: ['./notebooks.component.scss'] }) export class NotebooksComponent implements OnInit, OnDestroy, CanDeactivate { - @ViewChild('addNotebookModal') public addNotebookModal: ModalDirective; - @ViewChild('uploadNotebookModal') public uploadNotebookModal: ModalDirective; - @ViewChild('createSessionModal') public createSessionModal: ModalDirective; - @ViewChild('editNotebook') public editNotebook: EditNotebookComponent; - @ViewChild('fileInput') fileInput: ElementRef; - - private readonly _router = inject(Router); - private readonly _route = inject(ActivatedRoute); - private readonly _notebooks = inject(NotebooksService); - private readonly _toast = inject(ToasterService); - private readonly _loading = inject(LoadingScreenService); - public readonly _sidebar = inject(NotebooksSidebarService); - public readonly _content = inject(NotebooksContentService); - - createFileForm: FormGroup; - uploadFileForm: FormGroup; - createSessionForm: FormGroup; - inputFileName = 'Choose file(s)'; - editNotebookSession = ''; - availableKernels: KernelSpec[] = []; - selectedSession: SessionResponse; // session selected in the createSessionModal - private subscriptions = new Subscription(); - creating = false; - loading = false; - - constructor() { - } - - ngOnInit(): void { - this._route.url.subscribe(url => { - this.update(url); - }); - this._route.queryParams.subscribe(params => - this.editNotebookSession = (params['session']?.length === 36) ? params['session'] : ''); - - this.initForms(); - - const sub1 = this._sidebar.onCurrentFileMoved().subscribe( - to => { - if (this.editNotebookSession === '') { - this._router.navigate([this._sidebar.baseUrl].concat(to.split('/')),); - } - } - ); - const sub2 = this._sidebar.onAddButtonClicked().subscribe(() => this.addNotebookModal.show()); - const sub3 = this._sidebar.onUploadButtonClicked().subscribe(() => this.uploadNotebookModal.show()); - const sub4 = this._sidebar.onNotebookClicked().subscribe(([t, n, $e]) => this.notebookClicked(n)); - const sub5 = this._content.onInvalidLocation().subscribe(() => this.onPageNotFound()); - const sub6 = this._content.onKernelSpecsChange().subscribe(specs => this.updateAvailableKernels(specs)); - const sub7 = this._content.onServerUnreachable().subscribe(() => this.onServerUnreachable()); - - this.subscriptions.add(sub1); - this.subscriptions.add(sub2); - this.subscriptions.add(sub3); - this.subscriptions.add(sub4); - this.subscriptions.add(sub5); - this.subscriptions.add(sub6); - this.subscriptions.add(sub7); - - this._content.updateAvailableKernels(); - this._content.updateSessions(); - this._content.setAutoUpdate(true); - - this._sidebar.open(); - } - - ngOnDestroy() { - this._content.setAutoUpdate(false); - this._sidebar.close(); - this.subscriptions.unsubscribe(); - } - - canDeactivate(component: ComponentCanDeactivate, currentRoute: ActivatedRouteSnapshot, - currentState: RouterStateSnapshot, nextState: RouterStateSnapshot): Observable | boolean { - const forced = nextState.root.queryParams?.forced; - if (!forced && this.editNotebookSession && this.editNotebook && !this.editNotebook.canDeactivate()) { - return this.editNotebook.confirmClose(); + @ViewChild('addNotebookModal') public addNotebookModal: ModalDirective; + @ViewChild('uploadNotebookModal') public uploadNotebookModal: ModalDirective; + @ViewChild('createSessionModal') public createSessionModal: ModalDirective; + @ViewChild('editNotebook') public editNotebook: EditNotebookComponent; + @ViewChild('fileInput') fileInput: ElementRef; + + private readonly _router = inject(Router); + private readonly _route = inject(ActivatedRoute); + private readonly _notebooks = inject(NotebooksService); + private readonly _toast = inject(ToasterService); + private readonly _loading = inject(LoadingScreenService); + public readonly _sidebar = inject(NotebooksSidebarService); + public readonly _content = inject(NotebooksContentService); + + createFileForm: FormGroup; + uploadFileForm: FormGroup; + createSessionForm: FormGroup; + inputFileName = 'Choose file(s)'; + editNotebookSession = ''; + availableKernels: KernelSpec[] = []; + selectedSession: SessionResponse; // session selected in the createSessionModal + private subscriptions = new Subscription(); + creating = false; + loading = false; + + constructor() { } - return true; - } - - - private initForms() { - this.createFileForm = new FormGroup({ - name: new FormControl('', [ - Validators.pattern('([a-zA-Z0-9_. \-]*[a-zA-Z0-9_\-])?'), // last symbol can't be '.' or ' ' - Validators.maxLength(50)]), - type: new FormControl('notebook'), - ext: new FormControl('.txt'), - kernel: new FormControl('', [Validators.required]), - }); - - this.uploadFileForm = new FormGroup({ - fileList: new FormControl('', [Validators.required]) - }); - - this.createSessionForm = new FormGroup({ - isNew: new FormControl(false), - session: new FormControl(''), - kernel: new FormControl('', [Validators.required]), - sessions: new FormControl([]), - // Variables that cannot be set directly by the user: - name: new FormControl('', [Validators.required]), - path: new FormControl('', [Validators.required]), - canConnect: new FormControl(true), - canManage: new FormControl(true) - }); - this.createSessionForm.get('isNew').valueChanges.subscribe((isNew: boolean) => { - if (isNew) { - this.createSessionForm.patchValue({kernel: this.availableKernels.length > 0 ? this.availableKernels[0].name : ''}); - } else { - this.createSessionForm.patchValue({session: this.createSessionForm.value.sessions[0].id}); - } - }); - this.createSessionForm.get('session').valueChanges.subscribe(sessionId => - this.selectedSession = this.createSessionForm.value.sessions.find(session => session.id === sessionId)); - } - - private update(url: UrlSegment[]) { - this._content.updateLocation(url); - } - - /** - * A notebook in the sidebar was clicked - */ - private notebookClicked(node) { - const path = node.data.id; - const name = node.data.name; - const sessions = this._content.getSessionsForNotebook(path); - - if (sessions.length > 0) { - this._content.clearNbCache(); // cache is only useful if the specified kernel is not known - const preferredId = this._content.getPreferredSessionId(path); - if (sessions.find(s => s.id === preferredId)) { - // open last used session - this.openSession(preferredId, path); - return; - } - this.openConnectOrCreateSessionModal(name, path, sessions); - } else { - this._loading.show(); - - this._content.getSpecifiedKernel(path).pipe( - mergeMap(res => { - if (res) { - return this.startAndOpenNotebook(name, path, res.name); + + ngOnInit(): void { + this._route.url.subscribe(url => { + this.update(url); + }); + this._route.queryParams.subscribe(params => + this.editNotebookSession = (params['session']?.length === 36) ? params['session'] : ''); + + this.initForms(); + + const sub1 = this._sidebar.onCurrentFileMoved().subscribe( + to => { + if (this.editNotebookSession === '') { + this._router.navigate([this._sidebar.baseUrl].concat(to.split('/')),); } + } + ); + const sub2 = this._sidebar.onAddButtonClicked().subscribe(() => this.addNotebookModal.show()); + const sub3 = this._sidebar.onUploadButtonClicked().subscribe(() => this.uploadNotebookModal.show()); + const sub4 = this._sidebar.onNotebookClicked().subscribe(([t, n, $e]) => this.notebookClicked(n)); + const sub5 = this._content.onInvalidLocation().subscribe(() => this.onPageNotFound()); + const sub6 = this._content.onKernelSpecsChange().subscribe(specs => this.updateAvailableKernels(specs)); + const sub7 = this._content.onServerUnreachable().subscribe(() => this.onServerUnreachable()); + + this.subscriptions.add(sub1); + this.subscriptions.add(sub2); + this.subscriptions.add(sub3); + this.subscriptions.add(sub4); + this.subscriptions.add(sub5); + this.subscriptions.add(sub6); + this.subscriptions.add(sub7); + + this._content.updateAvailableKernels(); + this._content.updateSessions(); + this._content.setAutoUpdate(true); + + this._sidebar.open(); + } - this._loading.hide(); - this.openCreateSessionModal(name, path); - return EMPTY; - } - ) - ).subscribe( - res => { - }, - err => { - console.log(err); - this._toast.error('Failed to open Notebook. The file might be corrupted or does no longer exist.'); - this.openManagePage(path); - } - ).add(() => this._loading.hide()); + ngOnDestroy() { + this._content.setAutoUpdate(false); + this._sidebar.close(); + this.subscriptions.unsubscribe(); } - } - - private openConnectOrCreateSessionModal(name: string, path: string, sessions: SessionResponse[], canManage = true) { - this.createSessionForm.patchValue({ - name: name, - path: path, - sessions: sessions, - session: sessions[0].id, - canConnect: true, - canManage: canManage - }); - this.selectedSession = sessions[0]; - this.createSessionModal.show(); - } - - private openCreateSessionModal(name: string, path: string, canManage = true) { - this.createSessionForm.patchValue({ - name: name, - path: path, - kernel: this.availableKernels.length > 0 ? this.availableKernels[0].name : '', - isNew: true, - canConnect: false, - canManage: canManage - }); - this.createSessionModal.show(); - - } - - /** - * createSessionModal 'open' button was clicked - */ - openNotebookClicked() { - const val = this.createSessionForm.value; - if (val.isNew) { - this.creating = true; - this.startAndOpenNotebook(val.name, val.path, val.kernel).subscribe().add(() => { - this.createSessionModal.hide(); - this.creating = false; - }); - } else { - this.openSession(val.session, val.path); - this.createSessionModal.hide(); + + canDeactivate(component: ComponentCanDeactivate, currentRoute: ActivatedRouteSnapshot, + currentState: RouterStateSnapshot, nextState: RouterStateSnapshot): Observable | boolean { + const forced = nextState.root.queryParams?.forced; + if (!forced && this.editNotebookSession && this.editNotebook && !this.editNotebook.canDeactivate()) { + return this.editNotebook.confirmClose(); + } + return true; } - } - openManagePage(path: string) { - this._router.navigate([this._sidebar.baseUrl].concat(path.split('/'))); - this.createSessionModal.hide(); - } + private initForms() { + this.createFileForm = new FormGroup({ + name: new FormControl('', [ + Validators.pattern('([a-zA-Z0-9_. \-]*[a-zA-Z0-9_\-])?'), // last symbol can't be '.' or ' ' + Validators.maxLength(50)]), + type: new FormControl('notebook'), + ext: new FormControl('.txt'), + kernel: new FormControl('', [Validators.required]), + }); + + this.uploadFileForm = new FormGroup({ + fileList: new FormControl('', [Validators.required]) + }); + + this.createSessionForm = new FormGroup({ + isNew: new FormControl(false), + session: new FormControl(''), + kernel: new FormControl('', [Validators.required]), + sessions: new FormControl([]), + // Variables that cannot be set directly by the user: + name: new FormControl('', [Validators.required]), + path: new FormControl('', [Validators.required]), + canConnect: new FormControl(true), + canManage: new FormControl(true) + }); + this.createSessionForm.get('isNew').valueChanges.subscribe((isNew: boolean) => { + if (isNew) { + this.createSessionForm.patchValue({kernel: this.availableKernels.length > 0 ? this.availableKernels[0].name : ''}); + } else { + this.createSessionForm.patchValue({session: this.createSessionForm.value.sessions[0].id}); + } + }); + this.createSessionForm.get('session').valueChanges.subscribe(sessionId => + this.selectedSession = this.createSessionForm.value.sessions.find(session => session.id === sessionId)); + } + + private update(url: UrlSegment[]) { + this._content.updateLocation(url); + } - openChangeSessionModal(name: string, path: string) { - const sessions = this._content.getSessionsForNotebook(path); + /** + * A notebook in the sidebar was clicked + */ + private notebookClicked(node) { + const path = node.data.id; + const name = node.data.name; + const sessions = this._content.getSessionsForNotebook(path); + + if (sessions.length > 0) { + this._content.clearNbCache(); // cache is only useful if the specified kernel is not known + const preferredId = this._content.getPreferredSessionId(path); + if (sessions.find(s => s.id === preferredId)) { + // open last used session + this.openSession(preferredId, path); + return; + } + this.openConnectOrCreateSessionModal(name, path, sessions); + } else { + this._loading.show(); + + this._content.getSpecifiedKernel(path).pipe( + mergeMap(res => { + if (res) { + return this.startAndOpenNotebook(name, path, res.name); + } + + this._loading.hide(); + this.openCreateSessionModal(name, path); + return EMPTY; + } + ) + ).subscribe( + res => { + }, + err => { + console.log(err); + this._toast.error('Failed to open Notebook. The file might be corrupted or does no longer exist.'); + this.openManagePage(path); + } + ).add(() => this._loading.hide()); + } + } - if (sessions.length > 0) { - this.openConnectOrCreateSessionModal(name, path, sessions, false); - } else { - this.openCreateSessionModal(name, path, false); + private openConnectOrCreateSessionModal(name: string, path: string, sessions: SessionResponse[], canManage = true) { + this.createSessionForm.patchValue({ + name: name, + path: path, + sessions: sessions, + session: sessions[0].id, + canConnect: true, + canManage: canManage + }); + this.selectedSession = sessions[0]; + this.createSessionModal.show(); } - } + private openCreateSessionModal(name: string, path: string, canManage = true) { + this.createSessionForm.patchValue({ + name: name, + path: path, + kernel: this.availableKernels.length > 0 ? this.availableKernels[0].name : '', + isNew: true, + canConnect: false, + canManage: canManage + }); + this.createSessionModal.show(); - createFile() { - if (!this.createFileForm.valid) { - return; } - this.creating = true; - const val = this.createFileForm.value; - let fileName = val.name; - let ext = '.ipynb'; - if (val.type === 'notebook' && !fileName.endsWith(ext)) { - fileName += ext; - } else if (val.type === 'file') { - if (!val.ext.startsWith('.')) { - this.createFileForm.patchValue({ext: '.' + val.ext}); - val.ext = '.' + val.ext; - } - ext = val.ext === '.' ? '' : val.ext; - fileName += ext; + + /** + * createSessionModal 'open' button was clicked + */ + openNotebookClicked() { + const val = this.createSessionForm.value; + if (val.isNew) { + this.creating = true; + this.startAndOpenNotebook(val.name, val.path, val.kernel).subscribe().add(() => { + this.createSessionModal.hide(); + this.creating = false; + }); + } else { + this.openSession(val.session, val.path); + this.createSessionModal.hide(); + } + } - let path = this._content.directoryPath + '/' + fileName; - let name = fileName; - this._notebooks.createFileWithExtension(this._content.directoryPath, val.type, ext).pipe( - mergeMap(res => { - if (val.name === '') { - name = res.name; - path = res.path; - return of({}); - } - return this._notebooks.moveFile(res.path, path); - } - ), mergeMap(res => { - if (val.type !== 'notebook') { - return EMPTY; - } - return this.startAndOpenNotebook(name, path, val.kernel); - }) - ).subscribe(res => { - }, err => { - this._toast.error(err.error.message, `Creation of '${fileName}' failed`); - }).add(() => { - if (val.type !== 'notebook') { - this._content.update(); // update for notebook happens automatically when navigating - } - this.addNotebookModal.hide(); - this.createFileForm.patchValue({name: '', type: 'notebook', ext: '.txt'}); - this.creating = false; - }); - } - - /** - * File(s) in upload file input changed - */ - onFileChange(files) { - if (files == null || files.length < 1) { - if (this.fileInput) { - this.fileInput.nativeElement.value = ''; - } - this.inputFileName = 'Choose file(s)'; - } else if (files.length === 1) { - this.inputFileName = files[0].name; - } else { - this.inputFileName = files.length + ' files'; + + openManagePage(path: string) { + this._router.navigate([this._sidebar.baseUrl].concat(path.split('/'))); + this.createSessionModal.hide(); } - this.uploadFileForm.patchValue({fileList: files}); - } + openChangeSessionModal(name: string, path: string) { + const sessions = this._content.getSessionsForNotebook(path); - uploadFile() { - if (!this.uploadFileForm.valid) { - return; + if (sessions.length > 0) { + this.openConnectOrCreateSessionModal(name, path, sessions, false); + } else { + this.openCreateSessionModal(name, path, false); + } } - for (const file of this.uploadFileForm.value.fileList) { - const reader = new FileReader(); - reader.addEventListener( - 'load', - (event) => { - const base64 = event.target.result.toString().split('base64,', 2)[1]; - this._notebooks.updateFile(this._content.directoryPath + '/' + file.name, base64, 'base64', 'file') - .subscribe(res => { - this._content.update(); - }, err => { - this._toast.error(`An error occurred while uploading ${file.name}:\n${err.error}`, 'File could not be uploaded'); - }); - }, - false - ); - reader.readAsDataURL(file); + + + createFile() { + if (!this.createFileForm.valid) { + return; + } + this.creating = true; + const val = this.createFileForm.value; + let fileName = val.name; + let ext = '.ipynb'; + if (val.type === 'notebook' && !fileName.endsWith(ext)) { + fileName += ext; + } else if (val.type === 'file') { + if (!val.ext.startsWith('.')) { + this.createFileForm.patchValue({ext: '.' + val.ext}); + val.ext = '.' + val.ext; + } + ext = val.ext === '.' ? '' : val.ext; + fileName += ext; + } + let path = this._content.directoryPath + '/' + fileName; + let name = fileName; + this._notebooks.createFileWithExtension(this._content.directoryPath, val.type, ext).pipe( + mergeMap(res => { + if (val.name === '') { + name = res.name; + path = res.path; + return of({}); + } + return this._notebooks.moveFile(res.path, path); + } + ), mergeMap(res => { + if (val.type !== 'notebook') { + return EMPTY; + } + return this.startAndOpenNotebook(name, path, val.kernel); + }) + ).subscribe(res => { + }, err => { + this._toast.error(err.error.message, `Creation of '${fileName}' failed`); + }).add(() => { + if (val.type !== 'notebook') { + this._content.update(); // update for notebook happens automatically when navigating + } + this.addNotebookModal.hide(); + this.createFileForm.patchValue({name: '', type: 'notebook', ext: '.txt'}); + this.creating = false; + }); } - this.onFileChange(null); - this.uploadNotebookModal.hide(); - } - - /** - * Create a new session with the specified kernel and navigate to its edit page - */ - private startAndOpenNotebook(name: string, path: string, kernel: string): Observable { - return this._notebooks.createSession(name, path, kernel, true).pipe( - tap(res => { - this._content.addSession(res); - this.openSession(res.id, path); + + /** + * File(s) in upload file input changed + */ + onFileChange(files) { + if (files == null || files.length < 1) { + if (this.fileInput) { + this.fileInput.nativeElement.value = ''; } - ) - ); - } - - private openSession(sessionId: string, path: string) { - const queryParams = {session: sessionId}; - this._router.navigate([this._sidebar.baseUrl].concat(path.split('/')), {queryParams}); - } - - private updateAvailableKernels(kernelSpecs: KernelSpecs) { - if (kernelSpecs == null) { - this.availableKernels = []; - } else { - this.availableKernels = Object.values(kernelSpecs.kernelspecs); - this.createFileForm.patchValue({kernel: kernelSpecs.default}); + this.inputFileName = 'Choose file(s)'; + } else if (files.length === 1) { + this.inputFileName = files[0].name; + } else { + this.inputFileName = files.length + ' files'; + } + this.uploadFileForm.patchValue({fileList: files}); } - } - private onPageNotFound() { - let queryParams = {}; - if (!this.editNotebook) { - queryParams = {forced: true}; + + uploadFile() { + if (!this.uploadFileForm.valid) { + return; + } + for (const file of this.uploadFileForm.value.fileList) { + const reader = new FileReader(); + reader.addEventListener( + 'load', + (event) => { + const base64 = event.target.result.toString().split('base64,', 2)[1]; + this._notebooks.updateFile(this._content.directoryPath + '/' + file.name, base64, 'base64', 'file') + .subscribe(res => { + this._content.update(); + }, err => { + this._toast.error(`An error occurred while uploading ${file.name}:\n${err.error}`, 'File could not be uploaded'); + }); + }, + false + ); + reader.readAsDataURL(file); + } + this.onFileChange(null); + this.uploadNotebookModal.hide(); } - this._router.navigate([this._sidebar.baseUrl, 'notebooks'], {queryParams}); - } - private onServerUnreachable() { - if (!this._content.isRoot) { - this._toast.error('Jupyter Server seems to be offline.'); - this._router.navigate([this._sidebar.baseUrl, 'notebooks']); + /** + * Create a new session with the specified kernel and navigate to its edit page + */ + private startAndOpenNotebook(name: string, path: string, kernel: string): Observable { + return this._notebooks.createSession(name, path, kernel, true).pipe( + tap(res => { + this._content.addSession(res); + this.openSession(res.id, path); + } + ) + ); + } + + private openSession(sessionId: string, path: string) { + const queryParams = {session: sessionId}; + this._router.navigate([this._sidebar.baseUrl].concat(path.split('/')), {queryParams}); + } + + private updateAvailableKernels(kernelSpecs: KernelSpecs) { + if (kernelSpecs == null) { + this.availableKernels = []; + } else { + this.availableKernels = Object.values(kernelSpecs.kernelspecs); + this.createFileForm.patchValue({kernel: kernelSpecs.default}); + } + } + + private onPageNotFound() { + let queryParams = {}; + if (!this.editNotebook) { + queryParams = {forced: true}; + } + this._router.navigate([this._sidebar.baseUrl, 'notebooks'], {queryParams}); + } + + private onServerUnreachable() { + if (!this._content.isRoot) { + this._toast.error('Jupyter Server seems to be offline.'); + this._router.navigate([this._sidebar.baseUrl, 'notebooks']); + } } - } } diff --git a/src/app/plugins/notebooks/models/kernel-response.model.ts b/src/app/plugins/notebooks/models/kernel-response.model.ts index cf4de65f..6b55dcc3 100644 --- a/src/app/plugins/notebooks/models/kernel-response.model.ts +++ b/src/app/plugins/notebooks/models/kernel-response.model.ts @@ -3,42 +3,42 @@ import {CellDisplayDataOutput, CellErrorOutput, CellExecuteResultOutput} from './notebook.model'; export interface KernelMsg { - header: MsgHeader; - msg_id: string; - msg_type: string; - parent_header: MsgParentHeader; - metadata: {}; - buffers: any[]; - channel: string; + header: MsgHeader; + msg_id: string; + msg_type: string; + parent_header: MsgParentHeader; + metadata: {}; + buffers: any[]; + channel: string; } export interface MsgHeader { - msg_id: string; - msg_type: string; - username: string; - session: string; - date: string; - version: string; + msg_id: string; + msg_type: string; + username: string; + session: string; + date: string; + version: string; } export interface MsgParentHeader { - msg_id: string; - msg_type: string; - version: string; - date: string; + msg_id: string; + msg_type: string; + version: string; + date: string; } export interface KernelStatus extends KernelMsg { - content: { - execution_state: 'busy' | 'idle' | 'starting' - }; + content: { + execution_state: 'busy' | 'idle' | 'starting' + }; } export interface KernelExecuteInput extends KernelMsg { - content: { - code: string, - execution_count: number - }; + content: { + code: string, + execution_count: number + }; } /** @@ -46,20 +46,20 @@ export interface KernelExecuteInput extends KernelMsg { * */ export interface KernelExecuteReply extends KernelMsg { - content: { - status: 'ok' | 'error' | 'aborted', - execution_count: number, - user_expressions: {}, - payload: any[] + content: { + status: 'ok' | 'error' | 'aborted', + execution_count: number, + user_expressions: {}, + payload: any[] - }; + }; } export interface KernelStream extends KernelMsg { - content: { - name: 'stdout' | 'stderr', - text: string // corresponding CellStreamOutput.text can also be string[], but not here - }; + content: { + name: 'stdout' | 'stderr', + text: string // corresponding CellStreamOutput.text can also be string[], but not here + }; } /** @@ -68,50 +68,50 @@ export interface KernelStream extends KernelMsg { * Frontends should ignore mime-types they do not understand. */ export interface KernelExecuteResult extends KernelMsg { - content: Omit; + content: Omit; } export interface KernelErrorMsg extends KernelMsg { - content: Omit; + content: Omit; } export interface KernelInterruptReply extends KernelMsg { - content: { - status: 'ok' | 'error' | 'aborted' - }; + content: { + status: 'ok' | 'error' | 'aborted' + }; } export interface KernelShutdownReply extends KernelMsg { - content: { - status: 'ok' | 'error' | 'aborted', - restart: boolean - }; + content: { + status: 'ok' | 'error' | 'aborted', + restart: boolean + }; } export interface KernelDisplayData extends KernelMsg { - content: Omit; + content: Omit; } export interface KernelUpdateDisplayData extends KernelMsg { - content: Omit; + content: Omit; } export interface KernelData { - 'text/plain'?: string[] | string; - 'text/html'?: string[] | string; - 'image/png'?: string[] | string; - 'application/json'?: {}; + 'text/plain'?: string[] | string; + 'text/html'?: string[] | string; + 'image/png'?: string[] | string; + 'application/json'?: {}; } export interface KernelDisplayMetadata { - 'image/png'?: { - width: number, - height: number - }; - 'application/json'?: { - 'expanded': boolean - }; - polypheny?: { - result_variable: string; - }; + 'image/png'?: { + width: number, + height: number + }; + 'application/json'?: { + 'expanded': boolean + }; + polypheny?: { + result_variable: string; + }; } diff --git a/src/app/plugins/notebooks/models/notebook.model.ts b/src/app/plugins/notebooks/models/notebook.model.ts index 77c89cb2..88934ac9 100644 --- a/src/app/plugins/notebooks/models/notebook.model.ts +++ b/src/app/plugins/notebooks/models/notebook.model.ts @@ -2,101 +2,101 @@ import {KernelData, KernelDisplayMetadata} from './kernel-response.model'; import {CellType} from '../components/edit-notebook/notebook-wrapper'; export interface Notebook { - cells: NotebookCell[]; - metadata: NotebookMetadata; - nbformat: number; - nbformat_minor: number; + cells: NotebookCell[]; + metadata: NotebookMetadata; + nbformat: number; + nbformat_minor: number; } export interface NotebookMetadata { - kernelspec?: { - display_name: string; - language: string; - name: string; - }; - language_info?: { - codemirror_mode: { - name: string; - version: number; + kernelspec?: { + display_name: string; + language: string; + name: string; }; - file_extension: string; - mimetype: string; - name: string; - nbconvert_exporter: string; - pygments_lexer: string; - version: string; - }; - polypheny?: PolyphenyNbMetadata; + language_info?: { + codemirror_mode: { + name: string; + version: number; + }; + file_extension: string; + mimetype: string; + name: string; + nbconvert_exporter: string; + pygments_lexer: string; + version: string; + }; + polypheny?: PolyphenyNbMetadata; } export interface NotebookCell { - cell_type: 'code' | 'markdown' | 'raw'; - id: string; - metadata: CellMetadata; - source: string[] | string; - execution_count: number; - outputs?: (CellStreamOutput | CellDisplayDataOutput | CellExecuteResultOutput | CellErrorOutput)[]; + cell_type: 'code' | 'markdown' | 'raw'; + id: string; + metadata: CellMetadata; + source: string[] | string; + execution_count: number; + outputs?: (CellStreamOutput | CellDisplayDataOutput | CellExecuteResultOutput | CellErrorOutput)[]; } export interface CellOutput { - output_type: CellOutputType; + output_type: CellOutputType; } export interface CellStreamOutput extends CellOutput { - name: 'stdout' | 'stderr'; - text: string[] | string; + name: 'stdout' | 'stderr'; + text: string[] | string; } export interface CellDisplayDataOutput extends CellOutput { - data: KernelData; - metadata: KernelDisplayMetadata; + data: KernelData; + metadata: KernelDisplayMetadata; } export interface CellExecuteResultOutput extends CellOutput { - execution_count: number; - data: KernelData; - metadata: KernelDisplayMetadata; + execution_count: number; + data: KernelData; + metadata: KernelDisplayMetadata; } export interface CellErrorOutput extends CellOutput { - ename: string; - evalue: string; - traceback: string[]; + ename: string; + evalue: string; + traceback: string[]; } // https://nbformat.readthedocs.io/en/latest/format_description.html#cell-metadata export interface CellMetadata { - collapsed?: boolean; - scrolled?: boolean | 'auto'; - deletable?: boolean; - editable?: boolean; - format?: string; // for raw cells - name?: string; - tags?: string[] | string; - jupyter?: { - source_hidden?: boolean; - outputs_hidden?: boolean; - }; - execution?: {}; - polypheny?: PolyphenyMetadata; + collapsed?: boolean; + scrolled?: boolean | 'auto'; + deletable?: boolean; + editable?: boolean; + format?: string; // for raw cells + name?: string; + tags?: string[] | string; + jupyter?: { + source_hidden?: boolean; + outputs_hidden?: boolean; + }; + execution?: {}; + polypheny?: PolyphenyMetadata; } // Cell-level metadata export interface PolyphenyMetadata { - cell_type?: CellType; - namespace?: string; - language?: string; - result_variable?: string; - manual_execution?: boolean; - expand_params?: boolean; // default: true (only active if nb-lvl expand_params is true as well) + cell_type?: CellType; + namespace?: string; + language?: string; + result_variable?: string; + manual_execution?: boolean; + expand_params?: boolean; // default: true (only active if nb-lvl expand_params is true as well) } // Notebook-level metadata export interface PolyphenyNbMetadata { - expand_params?: boolean; // used to toggle expansion for entire notebook, default: false + expand_params?: boolean; // used to toggle expansion for entire notebook, default: false } export type CellOutputType = 'execute_result' | 'stream' | 'display_data' | 'error'; diff --git a/src/app/plugins/notebooks/notebooks.module.ts b/src/app/plugins/notebooks/notebooks.module.ts index c7eedd1c..4d0e74eb 100644 --- a/src/app/plugins/notebooks/notebooks.module.ts +++ b/src/app/plugins/notebooks/notebooks.module.ts @@ -22,68 +22,94 @@ import {UnsavedChangesGuard} from './services/unsaved-changes.guard'; import {NbPolyOutputComponent} from './components/edit-notebook/nb-poly-output/nb-poly-output.component'; import {SafeHtmlPipe} from './services/safe-html.pipe'; import {TreeModule} from '@ali-hm/angular-tree-component'; -import {BadgeComponent, BgColorDirective, ButtonCloseDirective, ButtonDirective, ButtonGroupComponent, ButtonToolbarComponent, CardBodyComponent, CardComponent, CardFooterComponent, CardHeaderComponent, ColComponent, ContainerComponent, FormControlDirective, FormSelectDirective, InputGroupComponent, InputGroupTextDirective, ModalBodyComponent, ModalComponent, ModalContentComponent, ModalDialogComponent, ModalFooterComponent, ModalHeaderComponent, ModalTitleDirective, RowComponent, TooltipDirective} from "@coreui/angular"; +import { + BadgeComponent, + BgColorDirective, + ButtonCloseDirective, + ButtonDirective, + ButtonGroupComponent, + ButtonToolbarComponent, + CardBodyComponent, + CardComponent, + CardFooterComponent, + CardHeaderComponent, + ColComponent, + ContainerComponent, + FormControlDirective, + FormSelectDirective, + InputGroupComponent, + InputGroupTextDirective, + ModalBodyComponent, + ModalComponent, + ModalContentComponent, + ModalDialogComponent, + ModalFooterComponent, + ModalHeaderComponent, + ModalTitleDirective, + RowComponent, + TooltipDirective +} from '@coreui/angular'; import {IconDirective} from '@coreui/icons-angular'; @NgModule({ - imports: [ - CommonModule, - FormsModule, ReactiveFormsModule, - ComponentsModule, - DragDropModule, - ModalModule.forRoot(), - BsDropdownModule, - TooltipModule, - MarkdownModule.forRoot({ - markedOptions: { - provide: MARKED_OPTIONS, - useFactory: markedOptionsFactory, - deps: [WebuiSettingsService] - } - }), - TreeModule, - NgxJsonViewerModule, ModalHeaderComponent, ModalContentComponent, ModalDialogComponent, ModalComponent, InputGroupComponent, CardBodyComponent, ModalFooterComponent, ButtonDirective, InputGroupTextDirective, FormSelectDirective, FormControlDirective, ModalTitleDirective, ButtonCloseDirective, ModalBodyComponent, CardFooterComponent, CardHeaderComponent, RowComponent, CardComponent, IconDirective, ButtonGroupComponent, ColComponent, BadgeComponent, ContainerComponent, BgColorDirective, ButtonToolbarComponent, TooltipDirective - ], - declarations: [ - NotebooksComponent, - ManageNotebookComponent, - EditNotebookComponent, - NotebooksDashboardComponent, - NbCellComponent, - NbInputEditorComponent, - NbOutputDataComponent, - NbPolyOutputComponent, - SafeHtmlPipe - ], - exports: [ - NotebooksComponent - ], - providers: [ - NotebooksSidebarService, - NotebooksContentService, - UnsavedChangesGuard - ] + imports: [ + CommonModule, + FormsModule, ReactiveFormsModule, + ComponentsModule, + DragDropModule, + ModalModule.forRoot(), + BsDropdownModule, + TooltipModule, + MarkdownModule.forRoot({ + markedOptions: { + provide: MARKED_OPTIONS, + useFactory: markedOptionsFactory, + deps: [WebuiSettingsService] + } + }), + TreeModule, + NgxJsonViewerModule, ModalHeaderComponent, ModalContentComponent, ModalDialogComponent, ModalComponent, InputGroupComponent, CardBodyComponent, ModalFooterComponent, ButtonDirective, InputGroupTextDirective, FormSelectDirective, FormControlDirective, ModalTitleDirective, ButtonCloseDirective, ModalBodyComponent, CardFooterComponent, CardHeaderComponent, RowComponent, CardComponent, IconDirective, ButtonGroupComponent, ColComponent, BadgeComponent, ContainerComponent, BgColorDirective, ButtonToolbarComponent, TooltipDirective + ], + declarations: [ + NotebooksComponent, + ManageNotebookComponent, + EditNotebookComponent, + NotebooksDashboardComponent, + NbCellComponent, + NbInputEditorComponent, + NbOutputDataComponent, + NbPolyOutputComponent, + SafeHtmlPipe + ], + exports: [ + NotebooksComponent + ], + providers: [ + NotebooksSidebarService, + NotebooksContentService, + UnsavedChangesGuard + ] }) export class NotebooksModule { } // https://stackoverflow.com/questions/69218645/dynamic-configuration-for-angular-module-imports export function markedOptionsFactory(_settings: WebuiSettingsService) { - const renderer = new MarkedRenderer(); + const renderer = new MarkedRenderer(); - renderer.blockquote = (text: string) => { - return '

' + text + '

'; - }; + renderer.blockquote = (text: string) => { + return '

' + text + '

'; + }; - const defaultLinkRenderer = renderer.link.bind(renderer); - renderer.link = (href, title, text) => { - const link = defaultLinkRenderer(href, title, text); - return link.startsWith(' { + const link = defaultLinkRenderer(href, title, text); + return link.startsWith('(); - - private _directory: DirectoryContent; - private _metadata: Content; - private cachedNb: NotebookContent = null; - private sessions = new BehaviorSubject([]); - private kernelSpecs = new BehaviorSubject(null); - private invalidLocationSubject = new Subject(); - private serverUnreachableSubject = new Subject(); - private updateInterval$: Subscription; - - private preferredSessions: Map = new Map(); - private readonly LOCAL_STORAGE_PREFERRED_SESSIONS_KEY = 'notebooks-preferred-sessions'; - private namespaces = new BehaviorSubject([]); - - constructor() { - this.updateExistingNamespaces(); - this.updateAvailableKernels(); - this.updateSessions(); - this.loadPreferredSessions(); - } - - updateLocation(url: UrlSegment[]) { - this._pathSegments = url.map((segment) => decodeURIComponent(segment.toString())); - this._pathSegments.unshift('notebooks'); - this._path = this._pathSegments.join('/'); - this._directoryPath = this._path; - this._parentPath = null; - if (this._pathSegments.length === 1) { - this._isRoot = true; - } - - this.update(); - } - - update() { - this._notebooks.getContents(this._path, false).pipe( - mergeMap(res => { - this.updateMetadata(res); - return this._notebooks.getContents(this._directoryPath, true); - }) - ).subscribe( - res => { - this.updateDirectory(res); - }, - () => this.invalidLocationSubject.next(null) - ).add(() => this.contentChange.next(null)); - } - - private updateMetadata(res: Content) { - if (res == null) { - this.invalidLocationSubject.next(null); - return; - } - this._type = res.type; - this._metadata = res; - - this._isRoot = false; - if (this._type === 'directory') { - if (this._pathSegments.length === 1) { - this._isRoot = true; - } else { - this._parentPath = this._pathSegments.slice(0, -1).join('/'); - } - - } else { - this._directoryPath = this._pathSegments.slice(0, -1).join('/'); - if (this._pathSegments.length > 2) { - this._parentPath = this._pathSegments.slice(0, -2).join('/'); - } - } - } - - private updateDirectory(res: DirectoryContent) { - this._directory = res; - } - - updateAvailableKernels() { - this._notebooks.getKernelspecs().subscribe(res => this.kernelSpecs.next(res), - () => this.serverUnreachableSubject.next(null)); - } - - updateSessions() { - forkJoin([this._notebooks.getSessions(), this._notebooks.getOpenConnections()]).subscribe( - ([sessions, openConnections]) => { - const modifiedSessions = sessions.map(session => { // insert correct connections number - if (session.kernel) { - session.kernel.connections = openConnections[session.kernel.id] || 0; - } - return session; - }); - this.updatePreferredSessions(modifiedSessions); - this.sessions.next(modifiedSessions); - - }, - () => { - this.sessions.next([]); - this.serverUnreachableSubject.next(null); - } - ); - } - - hasRunningKernel(path: string) { - const session = this.sessions.getValue().find(s => s.path.startsWith(path)); - return session !== undefined; - } - - isCurrentPath(path: string) { - return path === this._path; - } - - setAutoUpdate(active: boolean) { - this.updateInterval$?.unsubscribe(); - if (active) { - this.updateInterval$ = interval(10000).subscribe(() => { + private readonly _notebooks = inject(NotebooksService); + + private _path: string; + private _pathSegments: string[]; + private _type = 'directory'; + + private _parentPath: string; // Files: root/parent/current/file.txt + private _directoryPath: string; // Directories (path = directoryPath): root/parent/current/ + private _isRoot = true; + + private contentChange = new Subject(); + + private _directory: DirectoryContent; + private _metadata: Content; + private cachedNb: NotebookContent = null; + private sessions = new BehaviorSubject([]); + private kernelSpecs = new BehaviorSubject(null); + private invalidLocationSubject = new Subject(); + private serverUnreachableSubject = new Subject(); + private updateInterval$: Subscription; + + private preferredSessions: Map = new Map(); + private readonly LOCAL_STORAGE_PREFERRED_SESSIONS_KEY = 'notebooks-preferred-sessions'; + private namespaces = new BehaviorSubject([]); + + constructor() { this.updateExistingNamespaces(); this.updateAvailableKernels(); this.updateSessions(); + this.loadPreferredSessions(); + } + + updateLocation(url: UrlSegment[]) { + this._pathSegments = url.map((segment) => decodeURIComponent(segment.toString())); + this._pathSegments.unshift('notebooks'); + this._path = this._pathSegments.join('/'); + this._directoryPath = this._path; + this._parentPath = null; + if (this._pathSegments.length === 1) { + this._isRoot = true; + } + this.update(); - }); - this.updateExistingNamespaces(); - } - } - - addSession(session: SessionResponse) { - this.sessions.next([...this.sessions.getValue(), session]); - } - - deleteSession(sessionId: string): Observable { - return this._notebooks.deleteSession(sessionId).pipe( - tap(() => { - const newSessions = this.sessions.getValue().filter(s => s.id !== sessionId); - this.sessions.next(newSessions); - }) - ); - - } - - deleteSessions(notebookPath: string): Observable { - const sessionIds = this.getSessionsForNotebook(notebookPath).map(session => session.id); - return this._notebooks.deleteSessions(sessionIds).pipe( - tap(() => { - const newSessions = this.sessions.getValue().filter(s => !sessionIds.includes(s.id)); - this.sessions.next(newSessions); - }) - ); - } - - deleteAllSessions(onlyUnused = false): Observable { - let sessionIds = []; - if (onlyUnused) { - sessionIds = this.sessions.getValue() - .filter(session => session.kernel?.connections === 0) - .map(session => session.id); - } else { - sessionIds = this.sessions.getValue().map(session => session.id); - } - return this._notebooks.deleteSessions(sessionIds).pipe( - tap(() => { - this.sessions.next(this.sessions.getValue().filter(session => !sessionIds.includes(session.id))); - }) - ); - } - - moveSessionsWithFile(fromPath: string, toName: string, toPath: string) { - const sessionsToMove = this.sessions.getValue().filter(s => s.path.startsWith(fromPath)); - const preferredId = this.preferredSessions.get(fromPath); - this.deletePreferredSessionId(fromPath); - forkJoin( - sessionsToMove.map(s => { - const uid = this._notebooks.getUniqueIdFromSession(s); - if (preferredId === s.id) { - this.setPreferredSessionId(toPath, s.id); - } - return this._notebooks.moveSession(s.id, toName + uid, toPath + uid); - }) - ).subscribe().add(() => this.updateSessions()); - } - - /** - * Observable that can be used to find out which kernel is specified inside a notebook file. - * If not yet done, the notebook content must first be retrieved. - */ - getSpecifiedKernel(path: string): Observable { - if (path === this.cachedNb?.path && this.cachedNbIsFresh()) { - return of(this.getKernelspec(this.cachedNb.content.metadata?.kernelspec?.name)); - } - return this._notebooks.getContents(path, true).pipe( - tap((res: Content) => { - this.cacheNb(res); - }), - switchMap(() => of(this.getKernelspec(this.cachedNb.content.metadata?.kernelspec?.name))) - ); - } - - getNotebookContent(path: string, allowCache = true): Observable { - if (allowCache && path === this.cachedNb?.path && this.cachedNbIsFresh()) { - const cached = of(this.cachedNb); - this.cachedNb = null; // only return content once, since it could get modified - return cached; - } - return this._notebooks.getContents(path, true).pipe( - tap((res: Content) => { - this.cacheNb(res); - }), - switchMap(() => of(this.cachedNb)) - ); - } - - /** - * To determine the specified kernel of a notebook, its content must be loaded. - * To prevent the notebook from being loaded twice, when it is actually opened, we cache it. - * It is guaranteed that the cached version is only used if it has the same modification date as the one on disk. - */ - private cacheNb(nb: NotebookContent) { - if (nb.type !== 'notebook') { - this.cachedNb = null; - } - this.cachedNb = nb; - } - - clearNbCache() { - this.cachedNb = null; - } - - private cachedNbIsFresh(): boolean { - return this._directory.content.some(file => { - return file.path === this.cachedNb?.path && - file.last_modified === this.cachedNb.last_modified; - }); - } - - downloadFile() { - if (this.type !== 'directory') { - this._notebooks.getContents(this.metadata.path).subscribe(res => { - const file = res; - this.download(file.content, file.name, file.format); - }); - } - } - - downloadNotebook(notebook: Notebook, name: string) { - this.download(notebook, name, 'json'); - } - - private download(content: string | Notebook, name: string, format: string) { - let blob: Blob; - if (format === 'json' || typeof content !== 'string') { - blob = new Blob([JSON.stringify(content, null, 1)], {type: 'application/json'}); - } else if (format === 'base64') { - // https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript - const byteCharacters = atob(content); - const byteNumbers = new Array(byteCharacters.length); - - for (let i = 0; i < byteCharacters.length; i++) { - byteNumbers[i] = byteCharacters.charCodeAt(i); - } - blob = new Blob([new Uint8Array(byteNumbers)], {type: 'application/octet-stream'}); - } else { - blob = new Blob([content], {type: 'text/plain'}); - } - - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = name; - a.style.display = 'none'; - - a.click(); - URL.revokeObjectURL(url); - } - - getKernelspec(kernelName: string): KernelSpec { - return this.kernelSpecs.getValue().kernelspecs[kernelName]; - } - - getSessionsForNotebook(path: string = this.metadata.path): SessionResponse[] { - return this.sessions.getValue().filter(session => session.path.startsWith(path)); - } - - /** - * Returns the sessionId for the given notebook that was last used. - * It is not guaranteed that the session still exists. - */ - getPreferredSessionId(path: string): string { - return this.preferredSessions.get(path); - } - - setPreferredSessionId(path: string, sessionId: string) { - this.preferredSessions.set(path, sessionId); - this.storePreferredSessions(); - this.updateSessions(); - } - - deletePreferredSessionId(path: string) { - this.preferredSessions.delete(path); - this.storePreferredSessions(); - } - - private updatePreferredSessions(validSessions: SessionResponse[]) { - const ids = validSessions.map(s => s.id); - let changed = false; - for (const [path, id] of this.preferredSessions.entries()) { - if (!ids.includes(id)) { - changed = true; - this.preferredSessions.delete(path); - } } - if (changed) { - this.storePreferredSessions(); + + update() { + this._notebooks.getContents(this._path, false).pipe( + mergeMap(res => { + this.updateMetadata(res); + return this._notebooks.getContents(this._directoryPath, true); + }) + ).subscribe( + res => { + this.updateDirectory(res); + }, + () => this.invalidLocationSubject.next(null) + ).add(() => this.contentChange.next(null)); + } + + private updateMetadata(res: Content) { + if (res == null) { + this.invalidLocationSubject.next(null); + return; + } + this._type = res.type; + this._metadata = res; + + this._isRoot = false; + if (this._type === 'directory') { + if (this._pathSegments.length === 1) { + this._isRoot = true; + } else { + this._parentPath = this._pathSegments.slice(0, -1).join('/'); + } + + } else { + this._directoryPath = this._pathSegments.slice(0, -1).join('/'); + if (this._pathSegments.length > 2) { + this._parentPath = this._pathSegments.slice(0, -2).join('/'); + } + } + } + + private updateDirectory(res: DirectoryContent) { + this._directory = res; + } + + updateAvailableKernels() { + this._notebooks.getKernelspecs().subscribe(res => this.kernelSpecs.next(res), + () => this.serverUnreachableSubject.next(null)); + } + + updateSessions() { + forkJoin([this._notebooks.getSessions(), this._notebooks.getOpenConnections()]).subscribe( + ([sessions, openConnections]) => { + const modifiedSessions = sessions.map(session => { // insert correct connections number + if (session.kernel) { + session.kernel.connections = openConnections[session.kernel.id] || 0; + } + return session; + }); + this.updatePreferredSessions(modifiedSessions); + this.sessions.next(modifiedSessions); + + }, + () => { + this.sessions.next([]); + this.serverUnreachableSubject.next(null); + } + ); + } + + hasRunningKernel(path: string) { + const session = this.sessions.getValue().find(s => s.path.startsWith(path)); + return session !== undefined; + } + + isCurrentPath(path: string) { + return path === this._path; + } + + setAutoUpdate(active: boolean) { + this.updateInterval$?.unsubscribe(); + if (active) { + this.updateInterval$ = interval(10000).subscribe(() => { + this.updateExistingNamespaces(); + this.updateAvailableKernels(); + this.updateSessions(); + this.update(); + }); + this.updateExistingNamespaces(); + } + } + + addSession(session: SessionResponse) { + this.sessions.next([...this.sessions.getValue(), session]); + } + + deleteSession(sessionId: string): Observable { + return this._notebooks.deleteSession(sessionId).pipe( + tap(() => { + const newSessions = this.sessions.getValue().filter(s => s.id !== sessionId); + this.sessions.next(newSessions); + }) + ); + + } + + deleteSessions(notebookPath: string): Observable { + const sessionIds = this.getSessionsForNotebook(notebookPath).map(session => session.id); + return this._notebooks.deleteSessions(sessionIds).pipe( + tap(() => { + const newSessions = this.sessions.getValue().filter(s => !sessionIds.includes(s.id)); + this.sessions.next(newSessions); + }) + ); } - } - private storePreferredSessions() { - localStorage.setItem(this.LOCAL_STORAGE_PREFERRED_SESSIONS_KEY, - JSON.stringify(Array.from(this.preferredSessions.entries()))); - } + deleteAllSessions(onlyUnused = false): Observable { + let sessionIds = []; + if (onlyUnused) { + sessionIds = this.sessions.getValue() + .filter(session => session.kernel?.connections === 0) + .map(session => session.id); + } else { + sessionIds = this.sessions.getValue().map(session => session.id); + } + return this._notebooks.deleteSessions(sessionIds).pipe( + tap(() => { + this.sessions.next(this.sessions.getValue().filter(session => !sessionIds.includes(session.id))); + }) + ); + } - private loadPreferredSessions() { - this.preferredSessions = new Map( - JSON.parse(localStorage.getItem(this.LOCAL_STORAGE_PREFERRED_SESSIONS_KEY))); - } + moveSessionsWithFile(fromPath: string, toName: string, toPath: string) { + const sessionsToMove = this.sessions.getValue().filter(s => s.path.startsWith(fromPath)); + const preferredId = this.preferredSessions.get(fromPath); + this.deletePreferredSessionId(fromPath); + forkJoin( + sessionsToMove.map(s => { + const uid = this._notebooks.getUniqueIdFromSession(s); + if (preferredId === s.id) { + this.setPreferredSessionId(toPath, s.id); + } + return this._notebooks.moveSession(s.id, toName + uid, toPath + uid); + }) + ).subscribe().add(() => this.updateSessions()); + } + /** + * Observable that can be used to find out which kernel is specified inside a notebook file. + * If not yet done, the notebook content must first be retrieved. + */ + getSpecifiedKernel(path: string): Observable { + if (path === this.cachedNb?.path && this.cachedNbIsFresh()) { + return of(this.getKernelspec(this.cachedNb.content.metadata?.kernelspec?.name)); + } + return this._notebooks.getContents(path, true).pipe( + tap((res: Content) => { + this.cacheNb(res); + }), + switchMap(() => of(this.getKernelspec(this.cachedNb.content.metadata?.kernelspec?.name))) + ); + } + + getNotebookContent(path: string, allowCache = true): Observable { + if (allowCache && path === this.cachedNb?.path && this.cachedNbIsFresh()) { + const cached = of(this.cachedNb); + this.cachedNb = null; // only return content once, since it could get modified + return cached; + } + return this._notebooks.getContents(path, true).pipe( + tap((res: Content) => { + this.cacheNb(res); + }), + switchMap(() => of(this.cachedNb)) + ); + } + + /** + * To determine the specified kernel of a notebook, its content must be loaded. + * To prevent the notebook from being loaded twice, when it is actually opened, we cache it. + * It is guaranteed that the cached version is only used if it has the same modification date as the one on disk. + */ + private cacheNb(nb: NotebookContent) { + if (nb.type !== 'notebook') { + this.cachedNb = null; + } + this.cachedNb = nb; + } + + clearNbCache() { + this.cachedNb = null; + } + + private cachedNbIsFresh(): boolean { + return this._directory.content.some(file => { + return file.path === this.cachedNb?.path && + file.last_modified === this.cachedNb.last_modified; + }); + } + + downloadFile() { + if (this.type !== 'directory') { + this._notebooks.getContents(this.metadata.path).subscribe(res => { + const file = res; + this.download(file.content, file.name, file.format); + }); + } + } + + downloadNotebook(notebook: Notebook, name: string) { + this.download(notebook, name, 'json'); + } - private updateExistingNamespaces() { - /*this._crud.get(new SchemaRequest('views/notebooks/', false, 1, false)).subscribe( - res => { - const names = []; - for (const namespace of res) { - names.push(namespace?.name); + private download(content: string | Notebook, name: string, format: string) { + let blob: Blob; + if (format === 'json' || typeof content !== 'string') { + blob = new Blob([JSON.stringify(content, null, 1)], {type: 'application/json'}); + } else if (format === 'base64') { + // https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript + const byteCharacters = atob(content); + const byteNumbers = new Array(byteCharacters.length); + + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); } - this.namespaces.next(names); - }todo dl - );*/ - } - - /** - * Returns true if there exists a directory file inside the current directory with the specified path. - * Also returns true if the specified path is the current directory. - */ - isDirectory(path: string) { - return path === this._directoryPath || this._directory.content.some(file => - file.type === 'directory' && file.path === path); - } - - onContentChange() { - return this.contentChange; - } - - onSessionsChange() { - return this.sessions; - } - - onKernelSpecsChange() { - return this.kernelSpecs; - } - - onNamespaceChange() { - return this.namespaces; - } - - onDirectoryChange() { - return this._directory; - } - - onInvalidLocation() { - return this.invalidLocationSubject; - } - - onServerUnreachable() { - return this.serverUnreachableSubject; - } - - // Getters - - get path(): string { - return this._path; - } - - get type(): string { - return this._type; - } - - get parentPath(): string { - return this._parentPath; - } - - get directoryPath(): string { - return this._directoryPath; - } - - get isRoot(): boolean { - return this._isRoot; - } - - get pathSegments(): string[] { - return this._pathSegments; - } - - get directory(): DirectoryContent { - return this._directory; - } - - get metadata(): Content { - return this._metadata; - } + blob = new Blob([new Uint8Array(byteNumbers)], {type: 'application/octet-stream'}); + } else { + blob = new Blob([content], {type: 'text/plain'}); + } + + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = name; + a.style.display = 'none'; + + a.click(); + URL.revokeObjectURL(url); + } + + getKernelspec(kernelName: string): KernelSpec { + return this.kernelSpecs.getValue().kernelspecs[kernelName]; + } + + getSessionsForNotebook(path: string = this.metadata.path): SessionResponse[] { + return this.sessions.getValue().filter(session => session.path.startsWith(path)); + } + + /** + * Returns the sessionId for the given notebook that was last used. + * It is not guaranteed that the session still exists. + */ + getPreferredSessionId(path: string): string { + return this.preferredSessions.get(path); + } + + setPreferredSessionId(path: string, sessionId: string) { + this.preferredSessions.set(path, sessionId); + this.storePreferredSessions(); + this.updateSessions(); + } + + deletePreferredSessionId(path: string) { + this.preferredSessions.delete(path); + this.storePreferredSessions(); + } + + private updatePreferredSessions(validSessions: SessionResponse[]) { + const ids = validSessions.map(s => s.id); + let changed = false; + for (const [path, id] of this.preferredSessions.entries()) { + if (!ids.includes(id)) { + changed = true; + this.preferredSessions.delete(path); + } + } + if (changed) { + this.storePreferredSessions(); + } + } + + private storePreferredSessions() { + localStorage.setItem(this.LOCAL_STORAGE_PREFERRED_SESSIONS_KEY, + JSON.stringify(Array.from(this.preferredSessions.entries()))); + } + + private loadPreferredSessions() { + this.preferredSessions = new Map( + JSON.parse(localStorage.getItem(this.LOCAL_STORAGE_PREFERRED_SESSIONS_KEY))); + } + + + private updateExistingNamespaces() { + /*this._crud.get(new SchemaRequest('views/notebooks/', false, 1, false)).subscribe( + res => { + const names = []; + for (const namespace of res) { + names.push(namespace?.name); + } + this.namespaces.next(names); + }todo dl + );*/ + } + + /** + * Returns true if there exists a directory file inside the current directory with the specified path. + * Also returns true if the specified path is the current directory. + */ + isDirectory(path: string) { + return path === this._directoryPath || this._directory.content.some(file => + file.type === 'directory' && file.path === path); + } + + onContentChange() { + return this.contentChange; + } + + onSessionsChange() { + return this.sessions; + } + + onKernelSpecsChange() { + return this.kernelSpecs; + } + + onNamespaceChange() { + return this.namespaces; + } + + onDirectoryChange() { + return this._directory; + } + + onInvalidLocation() { + return this.invalidLocationSubject; + } + + onServerUnreachable() { + return this.serverUnreachableSubject; + } + + // Getters + + get path(): string { + return this._path; + } + + get type(): string { + return this._type; + } + + get parentPath(): string { + return this._parentPath; + } + + get directoryPath(): string { + return this._directoryPath; + } + + get isRoot(): boolean { + return this._isRoot; + } + + get pathSegments(): string[] { + return this._pathSegments; + } + + get directory(): DirectoryContent { + return this._directory; + } + + get metadata(): Content { + return this._metadata; + } } diff --git a/src/app/plugins/notebooks/services/notebooks-sidebar.service.ts b/src/app/plugins/notebooks/services/notebooks-sidebar.service.ts index 615caccb..90867f47 100644 --- a/src/app/plugins/notebooks/services/notebooks-sidebar.service.ts +++ b/src/app/plugins/notebooks/services/notebooks-sidebar.service.ts @@ -9,187 +9,187 @@ import {Subject, Subscription} from 'rxjs'; import {inject, Injectable} from '@angular/core'; import {NotebooksContentService} from './notebooks-content.service'; import {LoadingScreenService} from '../../../components/loading-screen/loading-screen.service'; -import {ToasterService} from "../../../components/toast-exposer/toaster.service"; +import {ToasterService} from '../../../components/toast-exposer/toaster.service'; @Injectable() export class NotebooksSidebarService { - private readonly _notebooks = inject(NotebooksService); - private readonly _content = inject(NotebooksContentService); - private readonly _leftSidebar = inject(LeftSidebarService); - private readonly _breadcrumb = inject(BreadcrumbService); - private readonly _toast = inject(ToasterService); - private readonly _loading = inject(LoadingScreenService); - - private _baseUrl = 'views/'; - - private pathSegments: string[]; - private parentPath: string; - private directoryPath: string; - private directory: DirectoryContent; - - private movedSubject = new Subject(); - private addButtonSubject = new Subject(); - private uploadButtonSubject = new Subject(); - private notebookClickedSubject = new Subject<[any, any, any]>(); // tree, node, $event - private subscriptions = new Subscription(); - - constructor() { - } - - private update() { - this.pathSegments = this._content.pathSegments; - this.parentPath = this._content.parentPath; - this.directoryPath = this._content.directoryPath; - this.directory = this._content.directory; - this.updateBreadcrumbs(); - this.updateSidebar(); - } - - private updateBreadcrumbs() { - const renamedSegments = ['Dashboard', ...this.pathSegments.slice(1)]; - const breadcrumbs = renamedSegments.slice(0, -1).map((segment, i) => new BreadcrumbItem( - segment, - this._baseUrl + '/' + this.pathSegments.slice(0, i + 1).join('/') - )); - breadcrumbs.push(new BreadcrumbItem(renamedSegments[renamedSegments.length - 1])); - this._breadcrumb.setBreadcrumbs(breadcrumbs); - this._breadcrumb.hideZoom(); - } - - /** - * Update the sidebar and breadcrumb based on the current state of the NotebooksContentService. - */ - private updateSidebar() { - if (!this.directoryPath) { - return; + private readonly _notebooks = inject(NotebooksService); + private readonly _content = inject(NotebooksContentService); + private readonly _leftSidebar = inject(LeftSidebarService); + private readonly _breadcrumb = inject(BreadcrumbService); + private readonly _toast = inject(ToasterService); + private readonly _loading = inject(LoadingScreenService); + + private _baseUrl = 'views/'; + + private pathSegments: string[]; + private parentPath: string; + private directoryPath: string; + private directory: DirectoryContent; + + private movedSubject = new Subject(); + private addButtonSubject = new Subject(); + private uploadButtonSubject = new Subject(); + private notebookClickedSubject = new Subject<[any, any, any]>(); // tree, node, $event + private subscriptions = new Subscription(); + + constructor() { + } + + private update() { + this.pathSegments = this._content.pathSegments; + this.parentPath = this._content.parentPath; + this.directoryPath = this._content.directoryPath; + this.directory = this._content.directory; + this.updateBreadcrumbs(); + this.updateSidebar(); } - const nodes: SidebarNode[] = [ - new SidebarNode('$breadcrumbs', this.directoryPath.split('/').slice(1).join(' > ') || '/').asSeparator() - ]; - if (this.parentPath) { //check if not in root - const parent = new SidebarNode( - this.parentPath, '..', 'fa fa-arrow-left', this._baseUrl + '/' + this.parentPath, - false, false, true - ); - parent.setDropAction((action, treeNode, $event, {from}) => { - this.moveFile(from.data.id, this.parentPath + '/' + from.data.name); - }); - nodes.push(parent); + private updateBreadcrumbs() { + const renamedSegments = ['Dashboard', ...this.pathSegments.slice(1)]; + const breadcrumbs = renamedSegments.slice(0, -1).map((segment, i) => new BreadcrumbItem( + segment, + this._baseUrl + '/' + this.pathSegments.slice(0, i + 1).join('/') + )); + breadcrumbs.push(new BreadcrumbItem(renamedSegments[renamedSegments.length - 1])); + this._breadcrumb.setBreadcrumbs(breadcrumbs); + this._breadcrumb.hideZoom(); } - const fileNodes = []; // files should be below all directories - for (const file of this.directory?.content || []) { - const routerLink = file.type === 'notebook' ? null : this._baseUrl + '/' + file.path; - const node = new SidebarNode( - file.path, - file.name, - this.getIcon(file.type, - file.type === 'notebook' && this._content.hasRunningKernel(file.path), - this._content.getPreferredSessionId(file.path) != null), - routerLink, - true, - true, - file.type === 'directory' - ); - if (file.type === 'directory') { - node.setDropAction((action, treeNode, $event, {from, to}) => { - this.moveFile(from.data.id, to.parent.data.id + '/' + from.data.name); + + /** + * Update the sidebar and breadcrumb based on the current state of the NotebooksContentService. + */ + private updateSidebar() { + if (!this.directoryPath) { + return; + } + const nodes: SidebarNode[] = [ + new SidebarNode('$breadcrumbs', this.directoryPath.split('/').slice(1).join(' > ') || '/').asSeparator() + ]; + if (this.parentPath) { //check if not in root + const parent = new SidebarNode( + this.parentPath, '..', 'fa fa-arrow-left', this._baseUrl + '/' + this.parentPath, + false, false, true + ); + parent.setDropAction((action, treeNode, $event, {from}) => { + this.moveFile(from.data.id, this.parentPath + '/' + from.data.name); + }); + nodes.push(parent); + + } + const fileNodes = []; // files should be below all directories + for (const file of this.directory?.content || []) { + const routerLink = file.type === 'notebook' ? null : this._baseUrl + '/' + file.path; + const node = new SidebarNode( + file.path, + file.name, + this.getIcon(file.type, + file.type === 'notebook' && this._content.hasRunningKernel(file.path), + this._content.getPreferredSessionId(file.path) != null), + routerLink, + true, + true, + file.type === 'directory' + ); + if (file.type === 'directory') { + node.setDropAction((action, treeNode, $event, {from, to}) => { + this.moveFile(from.data.id, to.parent.data.id + '/' + from.data.name); + }); + nodes.push(node); + continue; + } else if (file.type === 'notebook') { + node.setAction((tree, actionNode, $event) => + this.notebookClickedSubject.next([tree, actionNode, $event])); + } + fileNodes.push(node); + } + + this._leftSidebar.setNodes(nodes.concat(fileNodes)); + + const buttons = [ + new SidebarButton('+', () => this.addButtonSubject.next(), false), + new SidebarButton('Upload', () => this.uploadButtonSubject.next(), true), + ]; + this._leftSidebar.setTopButtons(buttons); + + if (this._content.isRoot && !this._loading.isShown() && this._leftSidebar.selectedNodeId) { + this.deselect(); + } + } + + private getIcon(type: string, isActive = false, hasPreferred = false) { + switch (type) { + case 'file': + return 'fa fa-file'; + case 'directory': + return 'fa fa-folder'; + case 'notebook': + return isActive ? + (hasPreferred ? 'fa fa-book text-success' : 'fa fa-book text-danger') : + 'fa fa-book'; + } + } + + moveFile(from: string, to: string) { + if (this._content.hasRunningKernel(from) && this._content.isDirectory(from)) { + this._toast.warn('Cannot move a folder that contains notebooks with running kernels.'); + return; + } + this._notebooks.moveFile(from, to).subscribe(res => { + if (this._content.isCurrentPath(from)) { + this.movedSubject.next(to); + } else { + this._content.update(); + } + if (this._content.hasRunningKernel(from)) { + this._content.moveSessionsWithFile(from, res.name, to); + } + + }, err => { + this._toast.error(err.error.message, `Failed to move '${from}'`); }); - nodes.push(node); - continue; - } else if (file.type === 'notebook') { - node.setAction((tree, actionNode, $event) => - this.notebookClickedSubject.next([tree, actionNode, $event])); - } - fileNodes.push(node); + + } + + open() { + this.subscriptions.unsubscribe(); + this.subscriptions = new Subscription(); + this.subscriptions.add(this._content.onContentChange().subscribe(() => this.update())); + this.subscriptions.add(this._content.onSessionsChange().subscribe(() => this.updateSidebar())); + this._leftSidebar.open(); + } + + close() { + this.subscriptions.unsubscribe(); + this._breadcrumb.hide(); + this._leftSidebar.setTopButtons([]); + this._leftSidebar.close(); } - this._leftSidebar.setNodes(nodes.concat(fileNodes)); + deselect() { + this._leftSidebar.reset(true); + this._leftSidebar.selectedNodeId = null; + } - const buttons = [ - new SidebarButton('+', () => this.addButtonSubject.next(), false), - new SidebarButton('Upload', () => this.uploadButtonSubject.next(), true), - ]; - this._leftSidebar.setTopButtons(buttons); + onAddButtonClicked() { + return this.addButtonSubject; + } - if (this._content.isRoot && !this._loading.isShown() && this._leftSidebar.selectedNodeId) { - this.deselect(); + onUploadButtonClicked() { + return this.uploadButtonSubject; } - } - - private getIcon(type: string, isActive = false, hasPreferred = false) { - switch (type) { - case 'file': - return 'fa fa-file'; - case 'directory': - return 'fa fa-folder'; - case 'notebook': - return isActive ? - (hasPreferred ? 'fa fa-book text-success' : 'fa fa-book text-danger') : - 'fa fa-book'; + + onCurrentFileMoved() { + return this.movedSubject; + } + + onNotebookClicked() { + return this.notebookClickedSubject; } - } - moveFile(from: string, to: string) { - if (this._content.hasRunningKernel(from) && this._content.isDirectory(from)) { - this._toast.warn('Cannot move a folder that contains notebooks with running kernels.'); - return; + // Getters + get baseUrl(): string { + return this._baseUrl; } - this._notebooks.moveFile(from, to).subscribe(res => { - if (this._content.isCurrentPath(from)) { - this.movedSubject.next(to); - } else { - this._content.update(); - } - if (this._content.hasRunningKernel(from)) { - this._content.moveSessionsWithFile(from, res.name, to); - } - - }, err => { - this._toast.error(err.error.message, `Failed to move '${from}'`); - }); - - } - - open() { - this.subscriptions.unsubscribe(); - this.subscriptions = new Subscription(); - this.subscriptions.add(this._content.onContentChange().subscribe(() => this.update())); - this.subscriptions.add(this._content.onSessionsChange().subscribe(() => this.updateSidebar())); - this._leftSidebar.open(); - } - - close() { - this.subscriptions.unsubscribe(); - this._breadcrumb.hide(); - this._leftSidebar.setTopButtons([]); - this._leftSidebar.close(); - } - - deselect() { - this._leftSidebar.reset(true); - this._leftSidebar.selectedNodeId = null; - } - - onAddButtonClicked() { - return this.addButtonSubject; - } - - onUploadButtonClicked() { - return this.uploadButtonSubject; - } - - onCurrentFileMoved() { - return this.movedSubject; - } - - onNotebookClicked() { - return this.notebookClickedSubject; - } - - // Getters - get baseUrl(): string { - return this._baseUrl; - } } diff --git a/src/app/plugins/notebooks/services/notebooks-webSocket.ts b/src/app/plugins/notebooks/services/notebooks-webSocket.ts index 61520668..bb84d511 100644 --- a/src/app/plugins/notebooks/services/notebooks-webSocket.ts +++ b/src/app/plugins/notebooks/services/notebooks-webSocket.ts @@ -3,95 +3,95 @@ import {webSocket} from 'rxjs/webSocket'; import {WebuiSettingsService} from '../../../services/webui-settings.service'; import * as uuid from 'uuid'; import {KernelMsg} from '../models/kernel-response.model'; -import {inject} from "@angular/core"; +import {inject} from '@angular/core'; export class NotebooksWebSocket { - public readonly _settings = inject(WebuiSettingsService); + public readonly _settings = inject(WebuiSettingsService); - private socket; - connected = false; - private msgSubject = new Subject(); + private socket; + connected = false; + private msgSubject = new Subject(); - constructor(kernelId: string) { - this.initWebSocket(kernelId); - } + constructor(kernelId: string) { + this.initWebSocket(kernelId); + } - private initWebSocket(kernelId: string) { - this.socket = webSocket({ - url: this._settings.getConnection('notebooks.socket') + `/${kernelId}`, - openObserver: { - next: (n) => { - this.connected = true; - } - } - }); - this.socket.subscribe( - msg => { - this.msgSubject.next(msg); - }, - err => { - this.connected = false; - console.error('websocket error:', err); - }, - () => { - this.connected = false; - this.msgSubject.complete(); - } - ); - } + private initWebSocket(kernelId: string) { + this.socket = webSocket({ + url: this._settings.getConnection('notebooks.socket') + `/${kernelId}`, + openObserver: { + next: (n) => { + this.connected = true; + } + } + }); + this.socket.subscribe( + msg => { + this.msgSubject.next(msg); + }, + err => { + this.connected = false; + console.error('websocket error:', err); + }, + () => { + this.connected = false; + this.msgSubject.complete(); + } + ); + } - sendMessage(obj: any): boolean { - this.socket.next(obj); - return this.connected; - } + sendMessage(obj: any): boolean { + this.socket.next(obj); + return this.connected; + } - onMessage() { - return this.msgSubject.pipe(); - } + onMessage() { + return this.msgSubject.pipe(); + } - close() { - this.socket.complete(); - } + close() { + this.socket.complete(); + } - requestExecutionState() { - const msg = { - uuid: uuid.v4(), - type: 'status', - content: null - }; - this.sendMessage(msg); - } + requestExecutionState() { + const msg = { + uuid: uuid.v4(), + type: 'status', + content: null + }; + this.sendMessage(msg); + } - sendCode(code: string[] | string): string { - const codeStr = (typeof code === 'string') ? - code : code.join('/n'); + sendCode(code: string[] | string): string { + const codeStr = (typeof code === 'string') ? + code : code.join('/n'); - const id = uuid.v4(); - const msg = { - uuid: id, - type: 'code', - content: codeStr - }; - this.sendMessage(msg); - return id; - } + const id = uuid.v4(); + const msg = { + uuid: id, + type: 'code', + content: codeStr + }; + this.sendMessage(msg); + return id; + } - sendQuery(query: string[] | string, language: string, namespace: string, variable: string, expand: boolean): string { - const queryStr = (typeof query === 'string') ? - query : query.join('/n'); + sendQuery(query: string[] | string, language: string, namespace: string, variable: string, expand: boolean): string { + const queryStr = (typeof query === 'string') ? + query : query.join('/n'); - const id = uuid.v4(); - const msg = { - uuid: id, - type: 'poly', - content: queryStr, - language: language, - namespace: namespace, - variable: variable, - expand: expand - }; - this.sendMessage(msg); - return id; - } + const id = uuid.v4(); + const msg = { + uuid: id, + type: 'poly', + content: queryStr, + language: language, + namespace: namespace, + variable: variable, + expand: expand + }; + this.sendMessage(msg); + return id; + } } diff --git a/src/app/plugins/notebooks/services/notebooks.service.ts b/src/app/plugins/notebooks/services/notebooks.service.ts index 6dbe309b..ec4c9c7b 100644 --- a/src/app/plugins/notebooks/services/notebooks.service.ts +++ b/src/app/plugins/notebooks/services/notebooks.service.ts @@ -1,200 +1,207 @@ import {Injectable} from '@angular/core'; import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http'; import {WebuiSettingsService} from '../../../services/webui-settings.service'; -import {Content, KernelResponse, KernelSpecs, NotebookContent, SessionResponse, StatusResponse} from '../models/notebooks-response.model'; +import { + Content, + KernelResponse, + KernelSpecs, + NotebookContent, + SessionResponse, + StatusResponse +} from '../models/notebooks-response.model'; import {Notebook} from '../models/notebook.model'; import * as uuid from 'uuid'; import {forkJoin} from 'rxjs'; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class NotebooksService { - constructor(private _http: HttpClient, private _settings: WebuiSettingsService) { - } - - private httpUrl = this._settings.getConnection('notebooks.rest'); - private httpOptions = {headers: new HttpHeaders({'Content-Type': 'application/json'})}; - - getStatus() { - return this._http.get(`${this.httpUrl}/status`, this.httpOptions); - } - - getPluginStatus() { - return this._http.get(`${this.httpUrl}/plugin/status`, this.httpOptions); - } - - getKernelspecs() { - return this._http.get(`${this.httpUrl}/kernelspecs`, this.httpOptions); - } - - getContents(path: string, includeInnerContent = true) { - const params = new HttpParams().append('content', includeInnerContent ? '1' : '0'); - return this._http.get(`${this.httpUrl}/contents/` + path, - {...this.httpOptions, ...{params: params}}); - } - - getExportedNotebook(path: string, language: string) { - const params = new HttpParams().append('language', language); - return this._http.get(`${this.httpUrl}/export/` + path, - {...this.httpOptions, ...{params: params}}); - } - - getContentsBase64(path: string) { - const params = new HttpParams().append('format', 'base64'); - return this._http.get(`${this.httpUrl}/contents/` + path, - {...this.httpOptions, ...{params: params}}); - } - - getSession(sessionId: string) { - return this._http.get(`${this.httpUrl}/sessions/` + sessionId, this.httpOptions); - } - - getSessions() { - return this._http.get(`${this.httpUrl}/sessions`, this.httpOptions); - } - - getKernels() { - return this._http.get(`${this.httpUrl}/kernels`, this.httpOptions); - } - - /** - * Returns a map that shows for each kernel id how many websockets are connected to it. - * This is much more useful than the connections number in the KernelResponse - */ - getOpenConnections() { - return this._http.get<{ [key: string]: number }>(`${this.httpUrl}/connections`, this.httpOptions); - } - - /** - * Creates a new session and returns it. - * If a session with the same path already exists and unique == false, then the existing session is returned. - */ - createSession(name: string, path: string, kernel: string, unique = false) { - const id = unique ? '$' + uuid.v4() : ''; - const json = { - kernel: { - name: kernel - }, - name: name + id, - path: path + id, - type: 'notebook' - }; - return this._http.post(`${this.httpUrl}/sessions`, json, this.httpOptions); - } - - getPathFromSession(session: SessionResponse) { - return session.path.split('$', 1)[0]; - } - - getNameFromSession(session: SessionResponse) { - return session.name.split('$', 1)[0]; - } - - getUniqueIdFromSession(session: SessionResponse): string { - const split = session.name.split('$', 2); - if (split.length === 2) { - return '$' + split[1]; - } - return ''; - } - - createFile(location: string, type: string) { - const json = { - type: type - }; - return this._http.post(`${this.httpUrl}/contents/${location}`, json, this.httpOptions); - } - - createFileWithExtension(location: string, type: string, ext: string) { - if (type !== 'file') { - return this.createFile(location, type); - } - const json = { - type: type, - ext: ext - }; - return this._http.post(`${this.httpUrl}/contents/${location}`, json, this.httpOptions); - } - - interruptKernel(kernelId: string) { - return this._http.post(`${this.httpUrl}/kernels/${kernelId}/interrupt`, '', this.httpOptions); - } - - restartKernel(kernelId: string) { - return this._http.post(`${this.httpUrl}/kernels/${kernelId}/restart`, '', this.httpOptions); - } - - deleteSession(sessionId: string) { - return this._http.delete(`${this.httpUrl}/sessions/${sessionId}`, this.httpOptions); - } - - deleteSessions(sessionIds: string[]) { - return forkJoin( - sessionIds.map(id => this.deleteSession(id)) - ); - } - - moveSession(sessionId: string, name: string, path: string) { - const json = { - id: sessionId, - name: name, - path: path, - }; - return this._http.patch(`${this.httpUrl}/sessions/${sessionId}`, json, this.httpOptions); - } - - moveFile(srcFilePath: string, destFilePath: string) { - const json = { - path: destFilePath, - }; - return this._http.patch(`${this.httpUrl}/contents/${srcFilePath}`, json, this.httpOptions); - } - - updateFile(filePath: string, content, format: string, type: string) { - const json = { - content: content, - format: format, - type: type - }; - return this._http.put(`${this.httpUrl}/contents/${filePath}`, json, this.httpOptions); - } - - updateNotebook(filePath: string, content: Notebook) { - const json = { - content: content, - format: 'json', - type: 'notebook' - }; - return this._http.put(`${this.httpUrl}/contents/${filePath}`, json, this.httpOptions); - } - - deleteFile(filePath: string) { - return this._http.delete(`${this.httpUrl}/contents/${filePath}`, this.httpOptions); - } - - duplicateFile(srcFilePath: string, destFilePath: string) { - const json = { - copy_from: srcFilePath - }; - return this._http.post(`${this.httpUrl}/contents/${destFilePath}`, json, this.httpOptions); - } - - restartContainer() { - return this._http.post(`${this.httpUrl}/container/restart`, '', this.httpOptions); - } - - getDockerInstances() { - return this._http.get(`${this.httpUrl}/container/getDockerInstances`); - } - - createContainer(id: number) { - return this._http.post(`${this.httpUrl}/container/create?dockerInstance=${id}`, '', this.httpOptions); - } - - destroyContainer() { - return this._http.post(`${this.httpUrl}/container/destroy`, '', this.httpOptions); - } + constructor(private _http: HttpClient, private _settings: WebuiSettingsService) { + } + + private httpUrl = this._settings.getConnection('notebooks.rest'); + private httpOptions = {headers: new HttpHeaders({'Content-Type': 'application/json'})}; + + getStatus() { + return this._http.get(`${this.httpUrl}/status`, this.httpOptions); + } + + getPluginStatus() { + return this._http.get(`${this.httpUrl}/plugin/status`, this.httpOptions); + } + + getKernelspecs() { + return this._http.get(`${this.httpUrl}/kernelspecs`, this.httpOptions); + } + + getContents(path: string, includeInnerContent = true) { + const params = new HttpParams().append('content', includeInnerContent ? '1' : '0'); + return this._http.get(`${this.httpUrl}/contents/` + path, + {...this.httpOptions, ...{params: params}}); + } + + getExportedNotebook(path: string, language: string) { + const params = new HttpParams().append('language', language); + return this._http.get(`${this.httpUrl}/export/` + path, + {...this.httpOptions, ...{params: params}}); + } + + getContentsBase64(path: string) { + const params = new HttpParams().append('format', 'base64'); + return this._http.get(`${this.httpUrl}/contents/` + path, + {...this.httpOptions, ...{params: params}}); + } + + getSession(sessionId: string) { + return this._http.get(`${this.httpUrl}/sessions/` + sessionId, this.httpOptions); + } + + getSessions() { + return this._http.get(`${this.httpUrl}/sessions`, this.httpOptions); + } + + getKernels() { + return this._http.get(`${this.httpUrl}/kernels`, this.httpOptions); + } + + /** + * Returns a map that shows for each kernel id how many websockets are connected to it. + * This is much more useful than the connections number in the KernelResponse + */ + getOpenConnections() { + return this._http.get<{ [key: string]: number }>(`${this.httpUrl}/connections`, this.httpOptions); + } + + /** + * Creates a new session and returns it. + * If a session with the same path already exists and unique == false, then the existing session is returned. + */ + createSession(name: string, path: string, kernel: string, unique = false) { + const id = unique ? '$' + uuid.v4() : ''; + const json = { + kernel: { + name: kernel + }, + name: name + id, + path: path + id, + type: 'notebook' + }; + return this._http.post(`${this.httpUrl}/sessions`, json, this.httpOptions); + } + + getPathFromSession(session: SessionResponse) { + return session.path.split('$', 1)[0]; + } + + getNameFromSession(session: SessionResponse) { + return session.name.split('$', 1)[0]; + } + + getUniqueIdFromSession(session: SessionResponse): string { + const split = session.name.split('$', 2); + if (split.length === 2) { + return '$' + split[1]; + } + return ''; + } + + createFile(location: string, type: string) { + const json = { + type: type + }; + return this._http.post(`${this.httpUrl}/contents/${location}`, json, this.httpOptions); + } + + createFileWithExtension(location: string, type: string, ext: string) { + if (type !== 'file') { + return this.createFile(location, type); + } + const json = { + type: type, + ext: ext + }; + return this._http.post(`${this.httpUrl}/contents/${location}`, json, this.httpOptions); + } + + interruptKernel(kernelId: string) { + return this._http.post(`${this.httpUrl}/kernels/${kernelId}/interrupt`, '', this.httpOptions); + } + + restartKernel(kernelId: string) { + return this._http.post(`${this.httpUrl}/kernels/${kernelId}/restart`, '', this.httpOptions); + } + + deleteSession(sessionId: string) { + return this._http.delete(`${this.httpUrl}/sessions/${sessionId}`, this.httpOptions); + } + + deleteSessions(sessionIds: string[]) { + return forkJoin( + sessionIds.map(id => this.deleteSession(id)) + ); + } + + moveSession(sessionId: string, name: string, path: string) { + const json = { + id: sessionId, + name: name, + path: path, + }; + return this._http.patch(`${this.httpUrl}/sessions/${sessionId}`, json, this.httpOptions); + } + + moveFile(srcFilePath: string, destFilePath: string) { + const json = { + path: destFilePath, + }; + return this._http.patch(`${this.httpUrl}/contents/${srcFilePath}`, json, this.httpOptions); + } + + updateFile(filePath: string, content, format: string, type: string) { + const json = { + content: content, + format: format, + type: type + }; + return this._http.put(`${this.httpUrl}/contents/${filePath}`, json, this.httpOptions); + } + + updateNotebook(filePath: string, content: Notebook) { + const json = { + content: content, + format: 'json', + type: 'notebook' + }; + return this._http.put(`${this.httpUrl}/contents/${filePath}`, json, this.httpOptions); + } + + deleteFile(filePath: string) { + return this._http.delete(`${this.httpUrl}/contents/${filePath}`, this.httpOptions); + } + + duplicateFile(srcFilePath: string, destFilePath: string) { + const json = { + copy_from: srcFilePath + }; + return this._http.post(`${this.httpUrl}/contents/${destFilePath}`, json, this.httpOptions); + } + + restartContainer() { + return this._http.post(`${this.httpUrl}/container/restart`, '', this.httpOptions); + } + + getDockerInstances() { + return this._http.get(`${this.httpUrl}/container/getDockerInstances`); + } + + createContainer(id: number) { + return this._http.post(`${this.httpUrl}/container/create?dockerInstance=${id}`, '', this.httpOptions); + } + + destroyContainer() { + return this._http.post(`${this.httpUrl}/container/destroy`, '', this.httpOptions); + } } diff --git a/src/app/plugins/notebooks/services/safe-html.pipe.ts b/src/app/plugins/notebooks/services/safe-html.pipe.ts index 73553675..0ee8cffc 100644 --- a/src/app/plugins/notebooks/services/safe-html.pipe.ts +++ b/src/app/plugins/notebooks/services/safe-html.pipe.ts @@ -2,15 +2,15 @@ import {Pipe, PipeTransform} from '@angular/core'; import {DomSanitizer} from '@angular/platform-browser'; @Pipe({ - name: 'safeHtml' + name: 'safeHtml' }) // https://stackblitz.com/edit/angular-safehtml-pipe export class SafeHtmlPipe implements PipeTransform { - constructor(private sanitized: DomSanitizer) { - } + constructor(private sanitized: DomSanitizer) { + } - transform(value) { - return this.sanitized.bypassSecurityTrustHtml(value); - } + transform(value) { + return this.sanitized.bypassSecurityTrustHtml(value); + } } diff --git a/src/app/plugins/notebooks/services/unsaved-changes.guard.ts b/src/app/plugins/notebooks/services/unsaved-changes.guard.ts index 1650f681..08356e55 100644 --- a/src/app/plugins/notebooks/services/unsaved-changes.guard.ts +++ b/src/app/plugins/notebooks/services/unsaved-changes.guard.ts @@ -4,17 +4,17 @@ import {Observable} from 'rxjs'; // https://stackoverflow.com/questions/35922071/warn-user-of-unsaved-changes-before-leaving-page export interface ComponentCanDeactivate { - canDeactivate: (component: ComponentCanDeactivate, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState: RouterStateSnapshot) => boolean | Observable; + canDeactivate: (component: ComponentCanDeactivate, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState: RouterStateSnapshot) => boolean | Observable; } @Injectable() export class UnsavedChangesGuard implements CanDeactivate { - canDeactivate( - component: ComponentCanDeactivate, - currentRoute: ActivatedRouteSnapshot, - currentState: RouterStateSnapshot, - nextState?: RouterStateSnapshot): Observable | Promise | boolean | UrlTree { - return component.canDeactivate(component, currentRoute, currentState, nextState); - } + canDeactivate( + component: ComponentCanDeactivate, + currentRoute: ActivatedRouteSnapshot, + currentState: RouterStateSnapshot, + nextState?: RouterStateSnapshot): Observable | Promise | boolean | UrlTree { + return component.canDeactivate(component, currentRoute, currentState, nextState); + } } diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts index 54fda941..c0e6486a 100644 --- a/src/app/services/auth.service.ts +++ b/src/app/services/auth.service.ts @@ -5,58 +5,58 @@ import {RegisterRequest, RequestModel} from '../models/ui-request.model'; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class AuthService { - private readonly _settings = inject(WebuiSettingsService); - - public readonly id: WritableSignal = signal(null); - private readonly status: WritableSignal = signal(ConnectionStatus.INITIAL); - public websocket: WebSocket; - - constructor() { - this.websocket = new WebSocket(); - this.initWebsocket(); - } - - private readonly _key = 'auth/id'; - - private initWebsocket() { - console.log('init AUTH'); - const id = localStorage.getItem(this._key); - effect(() => { - const currentId = this.id(); - if (!currentId) { - return; - } - localStorage.setItem(this._key, currentId); - }); - - this.websocket.onMessage().subscribe((res: RequestModel) => { - switch (res.type) { - case 'RegisterRequest': - const register = res as RegisterRequest; - - this.id.set(register.source); - this.status.set(ConnectionStatus.CONNECTED); - } - - }); - - this.websocket.reconnecting.subscribe(con => { - const req = new RegisterRequest(id, null); - this.websocket.sendMessage(req); - }); - - const msg = new RegisterRequest(id, null); - this.websocket.sendMessage(msg); - - } + private readonly _settings = inject(WebuiSettingsService); + + public readonly id: WritableSignal = signal(null); + private readonly status: WritableSignal = signal(ConnectionStatus.INITIAL); + public websocket: WebSocket; + + constructor() { + this.websocket = new WebSocket(); + this.initWebsocket(); + } + + private readonly _key = 'auth/id'; + + private initWebsocket() { + console.log('init AUTH'); + const id = localStorage.getItem(this._key); + effect(() => { + const currentId = this.id(); + if (!currentId) { + return; + } + localStorage.setItem(this._key, currentId); + }); + + this.websocket.onMessage().subscribe((res: RequestModel) => { + switch (res.type) { + case 'RegisterRequest': + const register = res as RegisterRequest; + + this.id.set(register.source); + this.status.set(ConnectionStatus.CONNECTED); + } + + }); + + this.websocket.reconnecting.subscribe(con => { + const req = new RegisterRequest(id, null); + this.websocket.sendMessage(req); + }); + + const msg = new RegisterRequest(id, null); + this.websocket.sendMessage(msg); + + } } enum ConnectionStatus { - DISCONNECTED, - CONNECTED, - INITIAL + DISCONNECTED, + CONNECTED, + INITIAL } diff --git a/src/app/services/catalog.service.ts b/src/app/services/catalog.service.ts index 5926dfc8..bc5e514f 100644 --- a/src/app/services/catalog.service.ts +++ b/src/app/services/catalog.service.ts @@ -1,7 +1,24 @@ import {effect, inject, Injectable, signal, untracked, WritableSignal} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {WebuiSettingsService} from './webui-settings.service'; -import {AdapterTemplateModel, AllocationColumnModel, AllocationEntityModel, AllocationPartitionModel, AllocationPlacementModel, AssetsModel, CatalogState, ColumnModel, ConstraintModel, EntityModel, EntityType, FieldModel, IdEntity, KeyModel, LogicalSnapshotModel, NamespaceModel} from '../models/catalog.model'; +import { + AdapterTemplateModel, + AllocationColumnModel, + AllocationEntityModel, + AllocationPartitionModel, + AllocationPlacementModel, + AssetsModel, + CatalogState, + ColumnModel, + ConstraintModel, + EntityModel, + EntityType, + FieldModel, + IdEntity, + KeyModel, + LogicalSnapshotModel, + NamespaceModel +} from '../models/catalog.model'; import {DataModel} from '../models/ui-request.model'; import {SidebarNode} from '../models/sidebar-node.model'; import {combineLatestWith, Observable, Subject} from 'rxjs'; @@ -12,359 +29,359 @@ import {AuthService} from './auth.service'; import {WebSocket} from './webSocket'; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class CatalogService { - private readonly _http = inject(HttpClient); - private readonly _settings = inject(WebuiSettingsService); - private readonly _auth = inject(AuthService); - private readonly _types = inject(DbmsTypesService); - - public listener: WritableSignal = signal(this); - public state: WritableSignal = signal(CatalogState.INIT); - - private httpUrl = this._settings.getConnection('crud.rest'); - - private assets: AssetsModel; - - private snapshot: LogicalSnapshotModel; - public readonly namespaces: WritableSignal> = signal(new Map()); - public readonly namespacesNames: WritableSignal> = signal(new Map()); - public readonly entities: WritableSignal> = signal(new Map()); - public readonly fields: WritableSignal> = signal(new Map()); - public readonly fieldNames: WritableSignal> = signal(new Map()); - public readonly keys: WritableSignal> = signal(new Map()); - public readonly constraints: WritableSignal> = signal(new Map()); - - public readonly placements: WritableSignal> = signal(new Map()); - public readonly partitions: WritableSignal> = signal(new Map()); - public readonly allocations: WritableSignal> = signal(new Map()); - public readonly allocationColumns: WritableSignal> = signal(new Map()); - - public readonly adapters: WritableSignal> = signal(new Map()); - public readonly adapterTemplates: WritableSignal> = signal(new Map()); // typescript uses by reference comparisons, so this is necessary - - constructor() { - this.state.set(CatalogState.LOADING); - - effect(() => { - const id = this._auth.id(); - if (!id) { - return; - } - untracked(() => { - this.initWebsocket(id, this._auth.websocket); - }); - }); - - - this.updateIfNecessary().pipe(combineLatestWith(this.updateAssets()), combineLatestWith(this._types.getTypes())).subscribe(() => { - this.state.set(CatalogState.UP_TO_DATE); - }); - } - - private initWebsocket(id: string, websocket: WebSocket) { - websocket.onMessage().subscribe({ - next: (snapshot: LogicalSnapshotModel) => { - this.updateSnapshot(snapshot); - } - }); - } - - getSnapshot(): Observable { - return this._http.post(`${this.httpUrl}/getSnapshot`, this.snapshot ? this.snapshot.id : -1).pipe(map((snapshot: LogicalSnapshotModel) => { - console.log(snapshot); - if (snapshot) { - this.updateSnapshot(snapshot); - } - - return this; - })); - } - - private updateSnapshot(snapshot: LogicalSnapshotModel) { - this.snapshot = snapshot; - - this.namespaces.set(this.toIdMap(snapshot.namespaces)); - this.namespacesNames.set(this.toNameMap(snapshot.namespaces)); - this.entities.set(this.toIdMap(snapshot.entities)); - this.fields.set(this.toIdMap(snapshot.fields)); - this.fieldNames.set(this.toNameMap(snapshot.fields)); - this.keys.set(this.toIdMap(snapshot.keys)); - this.constraints.set(this.toIdMap(snapshot.constraints)); - this.placements.set(this.toIdMap(snapshot.placements)); - this.partitions.set(this.toIdMap(snapshot.partitions)); - this.allocations.set(this.toIdMap(snapshot.allocations)); - this.allocationColumns.set(this.toIdMap(snapshot.allocColumns)); - this.adapters.set(this.toIdMap(snapshot.adapters)); - this.adapterTemplates.set(new Map(snapshot.adapterTemplates.map(t => [t.adapterName + '_' + t.adapterType, t]))); - - this.listener.set(null); // force notify - this.listener.set(this); - - this.state.set(CatalogState.UP_TO_DATE); - } - - updateIfNecessary(): Observable { - const sub: Subject = new Subject(); - this._http.get(`${this.httpUrl}/getCurrentSnapshot`).subscribe((id: number) => { - this.getSnapshot().subscribe(() => { - sub.next(this); - }); - }); - return sub.pipe(); - } - - getSchemaTree(routerLinkRoot: string, views: boolean, depth: number, schemaEdit?: boolean, dataModels: DataModel[] = [DataModel.RELATIONAL, DataModel.DOCUMENT, DataModel.GRAPH]) { - return this.buildSchemaTree(routerLinkRoot, views, depth, schemaEdit, dataModels); - - } - - - private toIdMap(idEntities: T[]) { - return new Map(idEntities.map(n => [n.id, n])); - } - - private toNameMap(idEntities: T[]) { - return new Map(idEntities.map(n => [n.name, n])); - } - - getNamespaceFromName(name: string): NamespaceModel { - return this.namespacesNames().get(name); - } - - getNamespaceFromId(id: number): NamespaceModel { - return this.namespaces().get(id); - } - - getNamespaces(): NamespaceModel[] { - return Array.from(this.namespaces().values()); - } - - getEntityFromName(namespace: string, name: string): EntityModel { - const namespaces = Array.from(this.namespaces().values()).filter(n => (n.caseSensitive ? n.name === namespace : n.name.toLowerCase() === namespace.toLowerCase()) - || n.dataModel === DataModel.GRAPH && name.toLowerCase() === n.name.toLowerCase() || namespace.toLowerCase() === n.name.toLowerCase()); - if (namespaces.length === 0) { - return null; - } - - return Array.from(this.entities().values()).filter(e => e.namespaceId === namespaces[0].id && e.name === name || (e.dataModel === DataModel.GRAPH && namespace.toLowerCase() === e.name.toLowerCase()))[0]; - } - - getFullEntityName(entityId: number): String { - const entity = this.entities().get(entityId); - const namespace = this.namespaces().get(entity.namespaceId); - return namespace.name + '.' + entity.name; - } - - //// UTIL - - - private buildSchemaTree(routerLinkRoot: string, views: boolean, depth: number, schemaEdit: boolean, dataModels: DataModel[]): SidebarNode[] { - const nodes: SidebarNode[] = []; - for (const namespace of this.namespaces().values()) { - const namespaceNode = new SidebarNode(namespace.name, namespace.name, this.getNamespaceIcon(namespace.dataModel) + ' me-1', ''); - - if (depth > 1) { - switch (namespace.dataModel) { - case DataModel.DOCUMENT: - this.attachDocumentTree(namespace, namespaceNode, routerLinkRoot, depth, views); - break; - case DataModel.RELATIONAL: - this.attachRelationalTree(namespace, namespaceNode, routerLinkRoot, depth, views); - break; - case DataModel.GRAPH: - namespaceNode.routerLink = routerLinkRoot + '' + namespace.name; - break; + private readonly _http = inject(HttpClient); + private readonly _settings = inject(WebuiSettingsService); + private readonly _auth = inject(AuthService); + private readonly _types = inject(DbmsTypesService); + + public listener: WritableSignal = signal(this); + public state: WritableSignal = signal(CatalogState.INIT); + + private httpUrl = this._settings.getConnection('crud.rest'); + + private assets: AssetsModel; + + private snapshot: LogicalSnapshotModel; + public readonly namespaces: WritableSignal> = signal(new Map()); + public readonly namespacesNames: WritableSignal> = signal(new Map()); + public readonly entities: WritableSignal> = signal(new Map()); + public readonly fields: WritableSignal> = signal(new Map()); + public readonly fieldNames: WritableSignal> = signal(new Map()); + public readonly keys: WritableSignal> = signal(new Map()); + public readonly constraints: WritableSignal> = signal(new Map()); + + public readonly placements: WritableSignal> = signal(new Map()); + public readonly partitions: WritableSignal> = signal(new Map()); + public readonly allocations: WritableSignal> = signal(new Map()); + public readonly allocationColumns: WritableSignal> = signal(new Map()); + + public readonly adapters: WritableSignal> = signal(new Map()); + public readonly adapterTemplates: WritableSignal> = signal(new Map()); // typescript uses by reference comparisons, so this is necessary + + constructor() { + this.state.set(CatalogState.LOADING); + + effect(() => { + const id = this._auth.id(); + if (!id) { + return; + } + untracked(() => { + this.initWebsocket(id, this._auth.websocket); + }); + }); + + + this.updateIfNecessary().pipe(combineLatestWith(this.updateAssets()), combineLatestWith(this._types.getTypes())).subscribe(() => { + this.state.set(CatalogState.UP_TO_DATE); + }); + } + + private initWebsocket(id: string, websocket: WebSocket) { + websocket.onMessage().subscribe({ + next: (snapshot: LogicalSnapshotModel) => { + this.updateSnapshot(snapshot); + } + }); + } + + getSnapshot(): Observable { + return this._http.post(`${this.httpUrl}/getSnapshot`, this.snapshot ? this.snapshot.id : -1).pipe(map((snapshot: LogicalSnapshotModel) => { + console.log(snapshot); + if (snapshot) { + this.updateSnapshot(snapshot); + } + + return this; + })); + } + + private updateSnapshot(snapshot: LogicalSnapshotModel) { + this.snapshot = snapshot; + + this.namespaces.set(this.toIdMap(snapshot.namespaces)); + this.namespacesNames.set(this.toNameMap(snapshot.namespaces)); + this.entities.set(this.toIdMap(snapshot.entities)); + this.fields.set(this.toIdMap(snapshot.fields)); + this.fieldNames.set(this.toNameMap(snapshot.fields)); + this.keys.set(this.toIdMap(snapshot.keys)); + this.constraints.set(this.toIdMap(snapshot.constraints)); + this.placements.set(this.toIdMap(snapshot.placements)); + this.partitions.set(this.toIdMap(snapshot.partitions)); + this.allocations.set(this.toIdMap(snapshot.allocations)); + this.allocationColumns.set(this.toIdMap(snapshot.allocColumns)); + this.adapters.set(this.toIdMap(snapshot.adapters)); + this.adapterTemplates.set(new Map(snapshot.adapterTemplates.map(t => [t.adapterName + '_' + t.adapterType, t]))); + + this.listener.set(null); // force notify + this.listener.set(this); + + this.state.set(CatalogState.UP_TO_DATE); + } + + updateIfNecessary(): Observable { + const sub: Subject = new Subject(); + this._http.get(`${this.httpUrl}/getCurrentSnapshot`).subscribe((id: number) => { + this.getSnapshot().subscribe(() => { + sub.next(this); + }); + }); + return sub.pipe(); + } + + getSchemaTree(routerLinkRoot: string, views: boolean, depth: number, schemaEdit?: boolean, dataModels: DataModel[] = [DataModel.RELATIONAL, DataModel.DOCUMENT, DataModel.GRAPH]) { + return this.buildSchemaTree(routerLinkRoot, views, depth, schemaEdit, dataModels); + + } + + + private toIdMap(idEntities: T[]) { + return new Map(idEntities.map(n => [n.id, n])); + } + + private toNameMap(idEntities: T[]) { + return new Map(idEntities.map(n => [n.name, n])); + } + + getNamespaceFromName(name: string): NamespaceModel { + return this.namespacesNames().get(name); + } + + getNamespaceFromId(id: number): NamespaceModel { + return this.namespaces().get(id); + } + + getNamespaces(): NamespaceModel[] { + return Array.from(this.namespaces().values()); + } + + getEntityFromName(namespace: string, name: string): EntityModel { + const namespaces = Array.from(this.namespaces().values()).filter(n => (n.caseSensitive ? n.name === namespace : n.name.toLowerCase() === namespace.toLowerCase()) + || n.dataModel === DataModel.GRAPH && name.toLowerCase() === n.name.toLowerCase() || namespace.toLowerCase() === n.name.toLowerCase()); + if (namespaces.length === 0) { + return null; } - } - nodes.push(namespaceNode); - } - - return nodes; - } - - private attachDocumentTree(namespace: NamespaceModel, namespaceNode: SidebarNode, routerLinkRoot: string, depth: number, views: boolean) { - const nodes: SidebarNode[] = []; - const collections: EntityModel[] = Array.from(this.entities().values()).filter(e => e.namespaceId === namespace.id); - - for (const collection of collections) { - - let icon = this.getNamespaceIcon(collection.dataModel); - switch (collection.entityType) { - case EntityType.SOURCE: - icon = this.assets.SOURCE_ICON; - break; - case EntityType.VIEW: - icon = this.assets.VIEW_ICON; - break; - } - const collectionTree = new SidebarNode(namespace.name + '.' + collection.name, collection.name, icon + ' me-1', routerLinkRoot + namespace.name + '.' + collection.name); - - nodes.push(collectionTree); - } - namespaceNode.children.push(...nodes); - } - - private attachRelationalTree(namespace: NamespaceModel, namespaceNode: SidebarNode, routerLinkRoot: string, depth: number, views: boolean) { - const nodes: SidebarNode[] = []; - const tables: EntityModel[] = Array.from(this.entities().values()).filter(t => t.namespaceId === namespace.id); - for (const table of tables) { - let icon = this.assets.TABLE_ICON; - - switch (table.entityType) { - case EntityType.SOURCE: - icon = this.assets.SOURCE_ICON; - break; - case EntityType.VIEW: - case EntityType.MATERIALIZED_VIEW: - icon = this.assets.VIEW_ICON; - break; - } - - const tableNode = new SidebarNode(namespace.name + '.' + table.name, table.name, icon + ' me-1', routerLinkRoot + namespace.name + '.' + table.name); - - if (depth > 2) { - const columns = Array.from(this.snapshot.fields.values()).filter(f => f.entityId === table.id); - for (const column of columns) { - tableNode.children.push(new SidebarNode(namespace.name + '.' + table.name + '.' + column.name, column.name, icon, routerLinkRoot)); + + return Array.from(this.entities().values()).filter(e => e.namespaceId === namespaces[0].id && e.name === name || (e.dataModel === DataModel.GRAPH && namespace.toLowerCase() === e.name.toLowerCase()))[0]; + } + + getFullEntityName(entityId: number): String { + const entity = this.entities().get(entityId); + const namespace = this.namespaces().get(entity.namespaceId); + return namespace.name + '.' + entity.name; + } + + //// UTIL + + + private buildSchemaTree(routerLinkRoot: string, views: boolean, depth: number, schemaEdit: boolean, dataModels: DataModel[]): SidebarNode[] { + const nodes: SidebarNode[] = []; + for (const namespace of this.namespaces().values()) { + const namespaceNode = new SidebarNode(namespace.name, namespace.name, this.getNamespaceIcon(namespace.dataModel) + ' me-1', ''); + + if (depth > 1) { + switch (namespace.dataModel) { + case DataModel.DOCUMENT: + this.attachDocumentTree(namespace, namespaceNode, routerLinkRoot, depth, views); + break; + case DataModel.RELATIONAL: + this.attachRelationalTree(namespace, namespaceNode, routerLinkRoot, depth, views); + break; + case DataModel.GRAPH: + namespaceNode.routerLink = routerLinkRoot + '' + namespace.name; + break; + } + } + nodes.push(namespaceNode); } - } - nodes.push(tableNode); + return nodes; + } + + private attachDocumentTree(namespace: NamespaceModel, namespaceNode: SidebarNode, routerLinkRoot: string, depth: number, views: boolean) { + const nodes: SidebarNode[] = []; + const collections: EntityModel[] = Array.from(this.entities().values()).filter(e => e.namespaceId === namespace.id); + + for (const collection of collections) { + + let icon = this.getNamespaceIcon(collection.dataModel); + switch (collection.entityType) { + case EntityType.SOURCE: + icon = this.assets.SOURCE_ICON; + break; + case EntityType.VIEW: + icon = this.assets.VIEW_ICON; + break; + } + const collectionTree = new SidebarNode(namespace.name + '.' + collection.name, collection.name, icon + ' me-1', routerLinkRoot + namespace.name + '.' + collection.name); + + nodes.push(collectionTree); + } + namespaceNode.children.push(...nodes); } - namespaceNode.children.push(...nodes); - namespaceNode.routerLink = ''; - } - private updateAssets() { - return new Observable(subscriber => this._http.get(`${this.httpUrl}/getAssetsDefinition`).subscribe((assets: AssetsModel) => { - this.assets = assets; - subscriber.next({}); + private attachRelationalTree(namespace: NamespaceModel, namespaceNode: SidebarNode, routerLinkRoot: string, depth: number, views: boolean) { + const nodes: SidebarNode[] = []; + const tables: EntityModel[] = Array.from(this.entities().values()).filter(t => t.namespaceId === namespace.id); + for (const table of tables) { + let icon = this.assets.TABLE_ICON; + + switch (table.entityType) { + case EntityType.SOURCE: + icon = this.assets.SOURCE_ICON; + break; + case EntityType.VIEW: + case EntityType.MATERIALIZED_VIEW: + icon = this.assets.VIEW_ICON; + break; + } + + const tableNode = new SidebarNode(namespace.name + '.' + table.name, table.name, icon + ' me-1', routerLinkRoot + namespace.name + '.' + table.name); + + if (depth > 2) { + const columns = Array.from(this.snapshot.fields.values()).filter(f => f.entityId === table.id); + for (const column of columns) { + tableNode.children.push(new SidebarNode(namespace.name + '.' + table.name + '.' + column.name, column.name, icon, routerLinkRoot)); + } + } + nodes.push(tableNode); - return { - unsubscribe() { } - }; - })); - } - - private getNamespaceIcon(dataModel: DataModel): string { - switch (dataModel) { - case DataModel.DOCUMENT: - return this.assets.DOCUMENT_ICON; - case DataModel.RELATIONAL: - return this.assets.RELATIONAL_ICON; - case DataModel.GRAPH: - return this.assets.GRAPH_ICON; - } - } - - getEntities(namespaceId: number): EntityModel[] { - return Array.from(this.entities().values()).filter(n => n.namespaceId === namespaceId); - } - - getColumns(entityId: number): ColumnModel[] { - return Array.from(this.fields().values()).filter(f => f.entityId === entityId).map(f => f); - } - - getPrimaryKey(entityId: number): KeyModel { - return Array.from(this.keys().values()).filter(k => k.isPrimary && k.entityId === entityId)[0]; - } - - getKey(keyId: number): KeyModel { - return this.keys().get(keyId); - } - - getKeys(entityId: number): KeyModel[] { - return Array.from(this.keys().values()).filter(k => k.entityId === entityId); - } - - getConstraint(constraintId: number): ConstraintModel { - return this.constraints().get(constraintId); - } - - getConstraintName(keyId: number): string { - const constraintNames = Array.from(this.constraints().values()).filter(c => c.keyId === keyId).map(c => c.name); - return constraintNames.length >= 1 ? constraintNames[0] : null; - } - - getConstraints(entityId: number): ConstraintModel[] { - const constraints = Array.from(this.constraints().values()); - const keys = Array.from(this.keys().values()).filter(k => k.entityId === entityId).map(k => k.id); - return constraints.filter(c => keys.includes(c.keyId)); - } - - getPlacements(entityId: number): AllocationPlacementModel[] { - return Array.from(this.placements().values()).filter(p => p.logicalEntityId === entityId); - } - - getPartitions(entityId: number): AllocationPartitionModel[] { - return Array.from(this.partitions().values()).filter(p => p.logicalEntityId === entityId); - } - - - getAllocColumns(placemenId: number): AllocationColumnModel[] { - return Array.from(this.allocationColumns().values()).filter(a => a.placementId === placemenId); - } - - getAllocColumn(id: number): AllocationColumnModel { - return Array.from(this.allocationColumns().values()).filter(c => c.id === id)[0]; - } - - getAdapter(adapterId: number) { - return this.adapters().get(adapterId); - } - - getAvailableStoresForIndexes(entityId: number): AdapterModel[] { - const adapterIds = Array.from(this.placements().values()).map(p => p.adapterId); - return Array.from(this.adapters().values()).filter(a => adapterIds.includes(a.id)).filter(a => a.type === AdapterType.STORE); - } - - getStores(): AdapterModel[] { - return Array.from(this.adapters().values()).filter(a => { - return a.type === AdapterType.STORE; - }); - } - - getSources() { - return Array.from(this.adapters().values()).filter(a => { - return a.type === AdapterType.SOURCE; - }); - } - - getAdapterTemplate(adapterName: string, type: AdapterType): AdapterTemplateModel { - return this.adapterTemplates().get(adapterName + '_' + type); - } - - getAdapterTemplates() { - return Array.from(this.adapterTemplates().values()); - } - - getLogicalField(id: number) { - return Array.from(this.fields().values()).filter(f => f.id === id)[0]; - } - - getLogicalColumn(id: number) { - const column = this.getLogicalField(id); - return column as ColumnModel; - } - - getAllocsOfPlacement(logicalId: number, allocId: number, adapterId: number): AllocationPartitionModel[] { - const partitions = Array.from(this.partitions().values()).filter(p => p.logicalEntityId === logicalId); - const allocPartitionIds = Array.from(this.allocations().values()).filter(a => a.id === allocId).map(a => a.partitionId); - - return partitions.filter(p => allocPartitionIds.includes(p.id)); - } - - getAllocations(logicalEntityId: number) { - return Array.from(this.allocations().values()).filter(a => a.logicalEntityId === logicalEntityId); - } - - getEntityFromIdName(namespaceId: number, entityName: string) { - return Array.from(this.entities().values()).filter(e => e.namespaceId === namespaceId && e.name === entityName)[0]; - } + namespaceNode.children.push(...nodes); + namespaceNode.routerLink = ''; + } + + private updateAssets() { + return new Observable(subscriber => this._http.get(`${this.httpUrl}/getAssetsDefinition`).subscribe((assets: AssetsModel) => { + this.assets = assets; + subscriber.next({}); + + return { + unsubscribe() { + } + }; + })); + } + + private getNamespaceIcon(dataModel: DataModel): string { + switch (dataModel) { + case DataModel.DOCUMENT: + return this.assets.DOCUMENT_ICON; + case DataModel.RELATIONAL: + return this.assets.RELATIONAL_ICON; + case DataModel.GRAPH: + return this.assets.GRAPH_ICON; + } + } + + getEntities(namespaceId: number): EntityModel[] { + return Array.from(this.entities().values()).filter(n => n.namespaceId === namespaceId); + } + + getColumns(entityId: number): ColumnModel[] { + return Array.from(this.fields().values()).filter(f => f.entityId === entityId).map(f => f); + } + + getPrimaryKey(entityId: number): KeyModel { + return Array.from(this.keys().values()).filter(k => k.isPrimary && k.entityId === entityId)[0]; + } + + getKey(keyId: number): KeyModel { + return this.keys().get(keyId); + } + + getKeys(entityId: number): KeyModel[] { + return Array.from(this.keys().values()).filter(k => k.entityId === entityId); + } + + getConstraint(constraintId: number): ConstraintModel { + return this.constraints().get(constraintId); + } + + getConstraintName(keyId: number): string { + const constraintNames = Array.from(this.constraints().values()).filter(c => c.keyId === keyId).map(c => c.name); + return constraintNames.length >= 1 ? constraintNames[0] : null; + } + + getConstraints(entityId: number): ConstraintModel[] { + const constraints = Array.from(this.constraints().values()); + const keys = Array.from(this.keys().values()).filter(k => k.entityId === entityId).map(k => k.id); + return constraints.filter(c => keys.includes(c.keyId)); + } + + getPlacements(entityId: number): AllocationPlacementModel[] { + return Array.from(this.placements().values()).filter(p => p.logicalEntityId === entityId); + } + + getPartitions(entityId: number): AllocationPartitionModel[] { + return Array.from(this.partitions().values()).filter(p => p.logicalEntityId === entityId); + } + + + getAllocColumns(placemenId: number): AllocationColumnModel[] { + return Array.from(this.allocationColumns().values()).filter(a => a.placementId === placemenId); + } + + getAllocColumn(id: number): AllocationColumnModel { + return Array.from(this.allocationColumns().values()).filter(c => c.id === id)[0]; + } + + getAdapter(adapterId: number) { + return this.adapters().get(adapterId); + } + + getAvailableStoresForIndexes(entityId: number): AdapterModel[] { + const adapterIds = Array.from(this.placements().values()).map(p => p.adapterId); + return Array.from(this.adapters().values()).filter(a => adapterIds.includes(a.id)).filter(a => a.type === AdapterType.STORE); + } + + getStores(): AdapterModel[] { + return Array.from(this.adapters().values()).filter(a => { + return a.type === AdapterType.STORE; + }); + } + + getSources() { + return Array.from(this.adapters().values()).filter(a => { + return a.type === AdapterType.SOURCE; + }); + } + + getAdapterTemplate(adapterName: string, type: AdapterType): AdapterTemplateModel { + return this.adapterTemplates().get(adapterName + '_' + type); + } + + getAdapterTemplates() { + return Array.from(this.adapterTemplates().values()); + } + + getLogicalField(id: number) { + return Array.from(this.fields().values()).filter(f => f.id === id)[0]; + } + + getLogicalColumn(id: number) { + const column = this.getLogicalField(id); + return column as ColumnModel; + } + + getAllocsOfPlacement(logicalId: number, allocId: number, adapterId: number): AllocationPartitionModel[] { + const partitions = Array.from(this.partitions().values()).filter(p => p.logicalEntityId === logicalId); + const allocPartitionIds = Array.from(this.allocations().values()).filter(a => a.id === allocId).map(a => a.partitionId); + + return partitions.filter(p => allocPartitionIds.includes(p.id)); + } + + getAllocations(logicalEntityId: number) { + return Array.from(this.allocations().values()).filter(a => a.logicalEntityId === logicalEntityId); + } + + getEntityFromIdName(namespaceId: number, entityName: string) { + return Array.from(this.entities().values()).filter(e => e.namespaceId === namespaceId && e.name === entityName)[0]; + } } diff --git a/src/app/services/config.service.ts b/src/app/services/config.service.ts index ce573ec8..13a01990 100644 --- a/src/app/services/config.service.ts +++ b/src/app/services/config.service.ts @@ -4,82 +4,82 @@ import {webSocket} from 'rxjs/webSocket'; import {WebuiSettingsService} from './webui-settings.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class ConfigService { - private readonly _http = inject(HttpClient); - private readonly _settings = inject(WebuiSettingsService); - - private socket; - public connected = false; - private reconnected = new EventEmitter(); - httpUrl; - httpOptions; - - constructor() { - this.initWebSocket(); - this.httpUrl = this._settings.getConnection('config.rest'); - this.httpOptions = {headers: new HttpHeaders({'Content-Type': 'application/json'})}; - } - - - getPage(pageId: string) { - return this._http.post(`${this.httpUrl}/getPage`, pageId, this.httpOptions); - } - - getPageList() { - return this._http.get(`${this.httpUrl}/getPageList`, this.httpOptions); - } - - saveChanges(data) { - return this._http.post(`${this.httpUrl}/updateConfigs`, data, this.httpOptions); - } - - testConnection(dockerInstanceId: number) { - // we only have a connection for the configs but this is a more general request and goes to crud - return this._http.get(`${this._settings.getConnection('crud.rest')}/testDockerInstance/${dockerInstanceId}`, this.httpOptions); - } - - - //https://rxjs-dev.firebaseapp.com/api/webSocket/webSocket - private initWebSocket() { - this.socket = webSocket({ - url: this._settings.getConnection('config.socket'), - openObserver: { - next: (n) => { - this.reconnected.emit(true); - this.connected = true; - } - } - }); - this.socket.subscribe( - msg => { - }, - err => { - //this.reconnected.emit(false); - this.connected = false; - setTimeout(() => { - this.initWebSocket(); - }, +this._settings.getSetting('reconnection.timeout')); - } - ); - } - - socketSend(msg: string) { - this.socket.next(msg); - } - - onSocketEvent() { - return this.socket; - } - - closeSocket() { - this.socket.complete(); - } - - onReconnection() { - return this.reconnected; - } + private readonly _http = inject(HttpClient); + private readonly _settings = inject(WebuiSettingsService); + + private socket; + public connected = false; + private reconnected = new EventEmitter(); + httpUrl; + httpOptions; + + constructor() { + this.initWebSocket(); + this.httpUrl = this._settings.getConnection('config.rest'); + this.httpOptions = {headers: new HttpHeaders({'Content-Type': 'application/json'})}; + } + + + getPage(pageId: string) { + return this._http.post(`${this.httpUrl}/getPage`, pageId, this.httpOptions); + } + + getPageList() { + return this._http.get(`${this.httpUrl}/getPageList`, this.httpOptions); + } + + saveChanges(data) { + return this._http.post(`${this.httpUrl}/updateConfigs`, data, this.httpOptions); + } + + testConnection(dockerInstanceId: number) { + // we only have a connection for the configs but this is a more general request and goes to crud + return this._http.get(`${this._settings.getConnection('crud.rest')}/testDockerInstance/${dockerInstanceId}`, this.httpOptions); + } + + + //https://rxjs-dev.firebaseapp.com/api/webSocket/webSocket + private initWebSocket() { + this.socket = webSocket({ + url: this._settings.getConnection('config.socket'), + openObserver: { + next: (n) => { + this.reconnected.emit(true); + this.connected = true; + } + } + }); + this.socket.subscribe( + msg => { + }, + err => { + //this.reconnected.emit(false); + this.connected = false; + setTimeout(() => { + this.initWebSocket(); + }, +this._settings.getSetting('reconnection.timeout')); + } + ); + } + + socketSend(msg: string) { + this.socket.next(msg); + } + + onSocketEvent() { + return this.socket; + } + + closeSocket() { + this.socket.complete(); + } + + onReconnection() { + return this.reconnected; + } } diff --git a/src/app/services/crud.service.ts b/src/app/services/crud.service.ts index 1988ae7a..52d6fccb 100644 --- a/src/app/services/crud.service.ts +++ b/src/app/services/crud.service.ts @@ -1,9 +1,33 @@ import {EventEmitter, inject, Injectable} from '@angular/core'; import {HttpClient, HttpHeaders, HttpUrlEncodingCodec} from '@angular/common/http'; import {WebuiSettingsService} from './webui-settings.service'; -import {EntityMeta, IndexModel, ModifyPartitionRequest, PartitionFunctionModel, PartitioningRequest, PathAccessRequest, PlacementMeta} from '../components/data-view/models/result-set.model'; +import { + EntityMeta, + IndexModel, + ModifyPartitionRequest, + PartitionFunctionModel, + PartitioningRequest, + PathAccessRequest, + PlacementMeta +} from '../components/data-view/models/result-set.model'; import {webSocket} from 'rxjs/webSocket'; -import {ColumnRequest, ConstraintRequest, DeleteRequest, EditCollectionRequest, EditTableRequest, EntityRequest, ExploreTable, GraphRequest, MaterializedRequest, Method, MonitoringRequest, Namespace, QueryRequest, RelAlgRequest, StatisticRequest} from '../models/ui-request.model'; +import { + ColumnRequest, + ConstraintRequest, + DeleteRequest, + EditCollectionRequest, + EditTableRequest, + EntityRequest, + ExploreTable, + GraphRequest, + MaterializedRequest, + Method, + MonitoringRequest, + Namespace, + QueryRequest, + RelAlgRequest, + StatisticRequest +} from '../models/ui-request.model'; import {DockerSettings} from '../models/docker.model'; import {ForeignKey, Uml} from '../views/uml/uml.model'; import {Validators} from '@angular/forms'; @@ -12,688 +36,688 @@ import {QueryInterface} from '../views/query-interfaces/query-interfaces.model'; import {AlgNodeModel, Node} from '../views/querying/algebra/algebra.model'; import {WebSocket} from './webSocket'; import {Observable} from 'rxjs'; -import {map} from "rxjs/operators"; +import {map} from 'rxjs/operators'; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class CrudService { - private readonly _http = inject(HttpClient); - private readonly _settings = inject(WebuiSettingsService); - - private enabledPlugins: [string] = null; - private enabledRequestFired: number = null; - private REQUEST_DELAY: number = 1000 * 20; - - - constructor() { - this.initWebSocket(); - setInterval(() => this.socket.next('keepalive'), 10_000); - } - - public connected = false; - private reconnected = new EventEmitter(); - private httpUrl = this._settings.getConnection('crud.rest'); - private httpOptions = {headers: new HttpHeaders({'Content-Type': 'application/json'})}; - private socket; - - // rendering routerLinks from string might not be possible:7 - // https://www.intertech.com/Blog/angular-4-case-study-caution-about-binding-html-content-using-innerhtml/ - // workarounds: - // https://stackoverflow.com/questions/44613069/angular4-routerlink-inside-innerhtml-turned-to-lowercase - - getEntityData(socket: WebSocket, data: EntityRequest): boolean { - return socket.sendMessage(data); - } - - getGraph(socket: WebSocket, data: GraphRequest): boolean { - return socket.sendMessage(data); - } - - insertTuple(formData: FormData) { - return this._http.post(`${this.httpUrl}/insertTuple`, formData, {reportProgress: true, observe: 'events'}); - } - - /** - * @param socket Socket object that is used to send the query - * @param query Any query that should be executed on the server - */ - anyQuery(socket: WebSocket, query: QueryRequest): boolean { - return socket.sendMessage(query); - } - - /** - * @param query will be converted in the back end to return an initial table for exploration - */ - createInitialExploreQuery(query) { - return this._http.post(`${this.httpUrl}/createInitialExploreQuery`, query, this.httpOptions); - } - - getEnabledPlugins(): string[] { - if (this.enabledRequestFired === null) { - this.enabledRequestFired = Date.now() - (this.REQUEST_DELAY + 100); - } - if (this.enabledPlugins === null) { - const today = Date.now(); - if ((this.enabledRequestFired + this.REQUEST_DELAY) < today) { - this.enabledRequestFired = today; - this._http.get(`${this.httpUrl}/getEnabledPlugins`, this.httpOptions) - .subscribe(res => { - this.enabledPlugins = <[string]>res; + private readonly _http = inject(HttpClient); + private readonly _settings = inject(WebuiSettingsService); + + private enabledPlugins: [string] = null; + private enabledRequestFired: number = null; + private REQUEST_DELAY: number = 1000 * 20; + + + constructor() { + this.initWebSocket(); + setInterval(() => this.socket.next('keepalive'), 10_000); + } + + public connected = false; + private reconnected = new EventEmitter(); + private httpUrl = this._settings.getConnection('crud.rest'); + private httpOptions = {headers: new HttpHeaders({'Content-Type': 'application/json'})}; + private socket; + + // rendering routerLinks from string might not be possible:7 + // https://www.intertech.com/Blog/angular-4-case-study-caution-about-binding-html-content-using-innerhtml/ + // workarounds: + // https://stackoverflow.com/questions/44613069/angular4-routerlink-inside-innerhtml-turned-to-lowercase + + getEntityData(socket: WebSocket, data: EntityRequest): boolean { + return socket.sendMessage(data); + } + + getGraph(socket: WebSocket, data: GraphRequest): boolean { + return socket.sendMessage(data); + } + + insertTuple(formData: FormData) { + return this._http.post(`${this.httpUrl}/insertTuple`, formData, {reportProgress: true, observe: 'events'}); + } + + /** + * @param socket Socket object that is used to send the query + * @param query Any query that should be executed on the server + */ + anyQuery(socket: WebSocket, query: QueryRequest): boolean { + return socket.sendMessage(query); + } + + /** + * @param query will be converted in the back end to return an initial table for exploration + */ + createInitialExploreQuery(query) { + return this._http.post(`${this.httpUrl}/createInitialExploreQuery`, query, this.httpOptions); + } + + getEnabledPlugins(): string[] { + if (this.enabledRequestFired === null) { + this.enabledRequestFired = Date.now() - (this.REQUEST_DELAY + 100); + } + if (this.enabledPlugins === null) { + const today = Date.now(); + if ((this.enabledRequestFired + this.REQUEST_DELAY) < today) { + this.enabledRequestFired = today; + this._http.get(`${this.httpUrl}/getEnabledPlugins`, this.httpOptions) + .subscribe(res => { + this.enabledPlugins = <[string]>res; + }); + } + return []; + } + return this.enabledPlugins; + } + + /** + * @param exploration labeled rows for classification, back end creates a decision tree with this data and returns classified initial data and tree + */ + exploreUserInput(exploration) { + return this._http.post(`${this.httpUrl}/exploration`, exploration, this.httpOptions); + } + + /** + * @param info classification of all data is requested from server + */ + classifyData(info) { + return this._http.post(`${this.httpUrl}/classifyData`, info, this.httpOptions); + } + + /** + * @param getExploreTables pagination for exploration tables + */ + getExploreTables(getExploreTables: ExploreTable) { + return this._http.post(`${this.httpUrl}/getExploreTables`, getExploreTables, this.httpOptions); + } + + /** + * Request all available statistic from the server + */ + allStatistics(statistics: StatisticRequest) { + return this._http.post(`${this.httpUrl}/allStatistics`, statistics, this.httpOptions); + } + + /** + * Request all available table statistics from the server + */ + getTableStatistics(statistics: StatisticRequest) { + return this._http.post(`${this.httpUrl}/getTableStatistics`, statistics, this.httpOptions); + } + + /** + * Request all dashboard information from the server + */ + getDashboardInformation(statistics: StatisticRequest) { + return this._http.post(`${this.httpUrl}/getDashboardInformation`, statistics, this.httpOptions); + } + + /** + * Request all available dml information from the monitoring (server) + */ + getDashboardDiagram(monitoringRequest: MonitoringRequest) { + return this._http.post(`${this.httpUrl}/getDashboardDiagram`, monitoringRequest, this.httpOptions); + } + + /** + * delete a row from a table + * @param request UIRequest + */ + deleteTuple(request: DeleteRequest) { + return this._http.post(`${this.httpUrl}/deleteTuple`, request, this.httpOptions); + } + + /** + * Update a row from a table + * @param formData Data in the form of a FormData + */ + updateTuple(formData: FormData) { + return this._http.post(`${this.httpUrl}/updateTuple`, formData, {reportProgress: true, observe: 'events'}); + } + + /** + * get the columns of a DataStore + */ + getColumns(columnRequest: ColumnRequest) { + return this._http.post(`${this.httpUrl}/getColumns`, columnRequest, this.httpOptions); + } + + /** + * get the columns of a DataStore + */ + getFixedFields(columnRequest: ColumnRequest) { + return this._http.post(`${this.httpUrl}/getFixedFields`, columnRequest, this.httpOptions); + } + + /** + * Get the columns of a DataSource + */ + getDataSourceColumns(request: EntityRequest) { + return this._http.post(`${this.httpUrl}/getDataSourceColumns`, request, this.httpOptions); + } + + getAvailableSourceColumns(request: EntityRequest) { + return this._http.post(`${this.httpUrl}/getAvailableSourceColumns`, request, this.httpOptions); + } + + /** + * Update a column of a Table + */ + updateColumn(columnRequest: ColumnRequest) { + return this._http.post(`${this.httpUrl}/updateColumn`, columnRequest, this.httpOptions); + } + + updateMaterialized(materializedRequest: MaterializedRequest) { + return this._http.post(`${this.httpUrl}/updateMaterialized`, materializedRequest, this.httpOptions); + } + + getMaterializedInfo(materializedRequest: EditTableRequest) { + return this._http.post(`${this.httpUrl}/getMaterializedInfo`, materializedRequest, this.httpOptions); + } + + createColumn(columnRequest: ColumnRequest) { + return this._http.post(`${this.httpUrl}/createColumn`, columnRequest, this.httpOptions); + } + + dropColumn(columnRequest: ColumnRequest) { + return this._http.post(`${this.httpUrl}/dropColumn`, columnRequest, this.httpOptions); + } + + /** + * Get list of tables of a schema to truncate/drop them + */ + getTables(tableRequest: EditTableRequest) { + return this._http.post(`${this.httpUrl}/getTables`, tableRequest, this.httpOptions); + } + + /** + * Drop or truncate a table + */ + dropTruncateTable(tableRequest: EditTableRequest) { + return this._http.post(`${this.httpUrl}/dropTruncateTable`, tableRequest, this.httpOptions); + } + + /** + * Create a new table + */ + createTable(tableRequest: EditTableRequest) { + return this._http.post(`${this.httpUrl}/createTable`, tableRequest, this.httpOptions); + } + + /** + * Create a new collection + */ + createCollection(collectionRequest: EditCollectionRequest) { + return this._http.post(`${this.httpUrl}/createCollection`, collectionRequest, this.httpOptions); + } + + getGeneratedNames() { + return this._http.get(`${this.httpUrl}/getGeneratedNames`, this.httpOptions); + } + + /** + * Get constraints of a table + */ + getConstraints(tableRequest: ColumnRequest) { + return this._http.post(`${this.httpUrl}/getConstraints`, tableRequest, this.httpOptions); + } + + /** + * Drop a onstraint of a table + */ + dropConstraint(request: ConstraintRequest) { + return this._http.post(`${this.httpUrl}/dropConstraint`, request, this.httpOptions); + } + + /** + * Add a primary key to a table + */ + createPrimaryKey(request: ConstraintRequest) { + return this._http.post(`${this.httpUrl}/createPrimaryKey`, request, this.httpOptions); + } + + /** + * Add a unique constraint to a table + */ + createUniqueConstraint(request: ConstraintRequest) { + return this._http.post(`${this.httpUrl}/createUniqueConstraint`, request, this.httpOptions); + } + + /** + * Get indexes of a table + */ + getIndexes(request: EditTableRequest) { + return this._http.post(`${this.httpUrl}/getIndexes`, request, this.httpOptions); + } + + /** + * Drop an index of a table + */ + dropIndex(index: IndexModel) { + return this._http.post(`${this.httpUrl}/dropIndex`, index, this.httpOptions); + } + + /** + * Create an index + */ + createIndex(index: IndexModel) { + return this._http.post(`${this.httpUrl}/createIndex`, index, this.httpOptions); + } + + /** + * Get data placement information + */ + getDataPlacements(namespaceId: number, entityId: number, fields: number[] = null) { + const meta = new EntityMeta(namespaceId, entityId, null, fields); + return this._http.post(`${this.httpUrl}/getPlacements`, meta, this.httpOptions); + } + + /** + * Get data placement information + */ + getCollectionPlacements(namespaceId: number, entityId: number, fields: number[]) { + const meta = new EntityMeta(namespaceId, entityId, null, fields); + return this._http.post(`${this.httpUrl}/getCollectionPlacements`, meta, this.httpOptions); + } + + getGraphPlacements(namespaceId: number, entityId: number, fields: number[]) { + const meta = new EntityMeta(namespaceId, entityId, null, fields); + return this._http.post(`${this.httpUrl}/getGraphPlacements`, meta, this.httpOptions); + } + + getUnderlyingTable(request: EntityRequest) { + return this._http.post(`${this.httpUrl}/getUnderlyingTable`, request, this.httpOptions); + } + + + /** + * Add or drop a placement + */ + addDropPlacement(namespaceId: number, entityId: number, storeId: number, method: Method, columns = []) { + const meta = new PlacementMeta(namespaceId, entityId, null, storeId, method, columns); + return this._http.post(`${this.httpUrl}/addDropPlacement`, meta, this.httpOptions); + } + + /** + * Add or drop a placement + */ + addDropGraphPlacement(graph: string, store: number, method: Method) { + let code: string; + switch (method) { + case Method.ADD: + code = `CREATE PLACEMENT OF ${graph} ON STORE ${store}`; + break; + case Method.DROP: + code = `DROP PLACEMENT OF ${graph} ON STORE ${store}`; + break; + } + const request = new QueryRequest(code, false, true, 'cypher', graph); + + return this.anyQueryBlocking(request); + } + + /** + * Add or drop a placement + */ + addDropCollectionPlacement(namespace: string, collection: string, store: string, method: Method) { + let code: string; + switch (method) { + case 'ADD': + code = `db.${collection}.addPlacement( "${store}" )`; + break; + case 'DROP': + code = `db.${collection}.deletePlacement( "${store}" )`; + break; + } + const request = new QueryRequest(code, false, true, 'cypher', namespace); + return this.anyQueryBlocking(request); + } + + + // PARTITIONING + + getPartitionTypes() { + return this._http.get(`${this.httpUrl}/getPartitionTypes`, this.httpOptions); + } + + getPartitionFunctionModel(request: PartitioningRequest) { + return this._http.post(`${this.httpUrl}/getPartitionFunctionModel`, request, this.httpOptions); + } + + partitionTable(request: PartitionFunctionModel) { + return this._http.post(`${this.httpUrl}/partitionTable`, request, this.httpOptions); + } + + mergePartitions(request: PartitioningRequest) { + return this._http.post(`${this.httpUrl}/mergePartitions`, request, this.httpOptions); + } + + modifyPartitions(request: ModifyPartitionRequest) { + return this._http.post(`${this.httpUrl}/modifyPartitions`, request, this.httpOptions); + } + + /** + * Get information for the Uml view, such as + * the list of all tables of a schema with their columns + * and a list of all the foreign keys of a schema + */ + getUml(request: EditTableRequest): Observable { + return >this._http.post(`${this.httpUrl}/getUml`, request, this.httpOptions); + } + + /** + * Initialize the websocket for the queryAnalyzer + */ + private initWebSocket() { + this.socket = webSocket({ + url: this._settings.getConnection('crud.socket'), + openObserver: { + next: (n) => { + this.reconnected.emit(true); + this.connected = true; + } + } + }); + this.socket.subscribe( + msg => { + }, + err => { + //this.reconnected.emit(false); + this.connected = false; + setTimeout(() => { + this.initWebSocket(); + }, +this._settings.getSetting('reconnection.timeout')); + } + ); + } + + + anyQueryBlocking(queryRequest: QueryRequest) { + return this._http.post(`${this.httpUrl}/anyQuery`, queryRequest, this.httpOptions); + } + + onSocketEvent() { + return this.socket; + } + + onReconnection() { + return this.reconnected; + } + + getAnalyzerPage(analyzerId: string, analyzerPage: string) { + return this._http.post(`${this.httpUrl}/getAnalyzerPage`, [analyzerId, analyzerPage], this.httpOptions); + } + + /** + * Add a foreign key (in the Uml view) + */ + createForeignKey(fk: ForeignKey) { + return this._http.post(`${this.httpUrl}/createForeignKey`, fk, this.httpOptions); + } + + /** + * Execute a relational algebra + */ + 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); + } + return socket.sendMessage(request); + } + + + renameTable(meta: EntityMeta) { + return this._http.post(`${this.httpUrl}/renameTable`, meta, this.httpOptions); + } + + /** + * Send a request to either create or drop a schema + */ + createOrDropNamespace(namespace: Namespace) { + return this._http.post(`${this.httpUrl}/namespaceRequest`, namespace, this.httpOptions); + } + + /** + * Get all supported data types of the DBMS. + */ + getTypeInfo() { + return this._http.get(`${this.httpUrl}/getTypeInfo`, this.httpOptions); + } + + /** + * Fetch available actions for foreign key constraints + */ + getFkActions() { + return this._http.get(`${this.httpUrl}/getForeignKeyActions`, this.httpOptions); + } + + getTypeSchemas() { + return this._http.get(`${this.httpUrl}/getTypeSchemas`, this.httpOptions); + } + + getStores() { + return this._http.get(`${this.httpUrl}/getStores`); + } + + getAvailableStoresForIndexes(request: IndexModel) { + return this._http.post(`${this.httpUrl}/getAvailableStoresForIndexes`, request, this.httpOptions); + } + + updateAdapterSettings(adapter: AdapterModel) { + return this._http.post(`${this.httpUrl}/updateAdapterSettings`, adapter); + } + + getSources() { + return this._http.get(`${this.httpUrl}/getSources`); + } + + getAvailableStores() { + return this._http.get(`${this.httpUrl}/getAvailableStores`); + } + + getAvailableSources() { + return this._http.get(`${this.httpUrl}/getAvailableSources`); + } + + createAdapter(adapter: AdapterModel) { + return this._http.post(`${this.httpUrl}/createAdapter`, adapter, this.httpOptions); + } + + + pathAccess(req: PathAccessRequest) { + return this._http.post(`${this.httpUrl}/pathAccess`, req); + } + + removeAdapter(storeId: string) { + return this._http.post(`${this.httpUrl}/removeAdapter`, storeId, this.httpOptions); + } + + getQueryInterfaces() { + return this._http.get(`${this.httpUrl}/getQueryInterfaces`); + } + + getAvailableQueryInterfaces() { + return this._http.get(`${this.httpUrl}/getAvailableQueryInterfaces`); + } + + createQueryInterface(request: any) { + return this._http.post(`${this.httpUrl}/createQueryInterface`, request, this.httpOptions); + } + + updateQueryInterfaceSettings(request: QueryInterface) { + 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); + } + + getUsedDockerPorts() { + return this._http.get(`${this.httpUrl}/usedDockerPorts`); + } + + getDocumentDatabases() { + return this._http.get(`${this.httpUrl}/getDocumentDatabases`); + } + + + /** + * Get the http url with which multimedia files can be displayed or downloaded + */ + getFileUrl(fileName: string): string { + if (fileName.startsWith('http://') || fileName.startsWith('https://')) { + return fileName; + } + return `${this.httpUrl}/getFile/${fileName}`; + } + + getFile(fileName: string) { + const url = this.getFileUrl(fileName); + + //blob as json: https://stackoverflow.com/questions/42898162/how-to-read-content-disposition-headers-from-server-response-angular-2 + return this._http.get(url, { + reportProgress: true, + observe: 'events', + responseType: 'blob' as 'json', + headers: new HttpHeaders({'Content-Type': 'application/octet-stream'}) }); - } - return []; - } - return this.enabledPlugins; - } - - /** - * @param exploration labeled rows for classification, back end creates a decision tree with this data and returns classified initial data and tree - */ - exploreUserInput(exploration) { - return this._http.post(`${this.httpUrl}/exploration`, exploration, this.httpOptions); - } - - /** - * @param info classification of all data is requested from server - */ - classifyData(info) { - return this._http.post(`${this.httpUrl}/classifyData`, info, this.httpOptions); - } - - /** - * @param getExploreTables pagination for exploration tables - */ - getExploreTables(getExploreTables: ExploreTable) { - return this._http.post(`${this.httpUrl}/getExploreTables`, getExploreTables, this.httpOptions); - } - - /** - * Request all available statistic from the server - */ - allStatistics(statistics: StatisticRequest) { - return this._http.post(`${this.httpUrl}/allStatistics`, statistics, this.httpOptions); - } - - /** - * Request all available table statistics from the server - */ - getTableStatistics(statistics: StatisticRequest) { - return this._http.post(`${this.httpUrl}/getTableStatistics`, statistics, this.httpOptions); - } - - /** - * Request all dashboard information from the server - */ - getDashboardInformation(statistics: StatisticRequest) { - return this._http.post(`${this.httpUrl}/getDashboardInformation`, statistics, this.httpOptions); - } - - /** - * Request all available dml information from the monitoring (server) - */ - getDashboardDiagram(monitoringRequest: MonitoringRequest) { - return this._http.post(`${this.httpUrl}/getDashboardDiagram`, monitoringRequest, this.httpOptions); - } - - /** - * delete a row from a table - * @param request UIRequest - */ - deleteTuple(request: DeleteRequest) { - return this._http.post(`${this.httpUrl}/deleteTuple`, request, this.httpOptions); - } - - /** - * Update a row from a table - * @param formData Data in the form of a FormData - */ - updateTuple(formData: FormData) { - return this._http.post(`${this.httpUrl}/updateTuple`, formData, {reportProgress: true, observe: 'events'}); - } - - /** - * get the columns of a DataStore - */ - getColumns(columnRequest: ColumnRequest) { - return this._http.post(`${this.httpUrl}/getColumns`, columnRequest, this.httpOptions); - } - - /** - * get the columns of a DataStore - */ - getFixedFields(columnRequest: ColumnRequest) { - return this._http.post(`${this.httpUrl}/getFixedFields`, columnRequest, this.httpOptions); - } - - /** - * Get the columns of a DataSource - */ - getDataSourceColumns(request: EntityRequest) { - return this._http.post(`${this.httpUrl}/getDataSourceColumns`, request, this.httpOptions); - } - - getAvailableSourceColumns(request: EntityRequest) { - return this._http.post(`${this.httpUrl}/getAvailableSourceColumns`, request, this.httpOptions); - } - - /** - * Update a column of a Table - */ - updateColumn(columnRequest: ColumnRequest) { - return this._http.post(`${this.httpUrl}/updateColumn`, columnRequest, this.httpOptions); - } - - updateMaterialized(materializedRequest: MaterializedRequest) { - return this._http.post(`${this.httpUrl}/updateMaterialized`, materializedRequest, this.httpOptions); - } - - getMaterializedInfo(materializedRequest: EditTableRequest) { - return this._http.post(`${this.httpUrl}/getMaterializedInfo`, materializedRequest, this.httpOptions); - } - - createColumn(columnRequest: ColumnRequest) { - return this._http.post(`${this.httpUrl}/createColumn`, columnRequest, this.httpOptions); - } - - dropColumn(columnRequest: ColumnRequest) { - return this._http.post(`${this.httpUrl}/dropColumn`, columnRequest, this.httpOptions); - } - - /** - * Get list of tables of a schema to truncate/drop them - */ - getTables(tableRequest: EditTableRequest) { - return this._http.post(`${this.httpUrl}/getTables`, tableRequest, this.httpOptions); - } - - /** - * Drop or truncate a table - */ - dropTruncateTable(tableRequest: EditTableRequest) { - return this._http.post(`${this.httpUrl}/dropTruncateTable`, tableRequest, this.httpOptions); - } - - /** - * Create a new table - */ - createTable(tableRequest: EditTableRequest) { - return this._http.post(`${this.httpUrl}/createTable`, tableRequest, this.httpOptions); - } - - /** - * Create a new collection - */ - createCollection(collectionRequest: EditCollectionRequest) { - return this._http.post(`${this.httpUrl}/createCollection`, collectionRequest, this.httpOptions); - } - - getGeneratedNames() { - return this._http.get(`${this.httpUrl}/getGeneratedNames`, this.httpOptions); - } - - /** - * Get constraints of a table - */ - getConstraints(tableRequest: ColumnRequest) { - return this._http.post(`${this.httpUrl}/getConstraints`, tableRequest, this.httpOptions); - } - - /** - * Drop a onstraint of a table - */ - dropConstraint(request: ConstraintRequest) { - return this._http.post(`${this.httpUrl}/dropConstraint`, request, this.httpOptions); - } - - /** - * Add a primary key to a table - */ - createPrimaryKey(request: ConstraintRequest) { - return this._http.post(`${this.httpUrl}/createPrimaryKey`, request, this.httpOptions); - } - - /** - * Add a unique constraint to a table - */ - createUniqueConstraint(request: ConstraintRequest) { - return this._http.post(`${this.httpUrl}/createUniqueConstraint`, request, this.httpOptions); - } - - /** - * Get indexes of a table - */ - getIndexes(request: EditTableRequest) { - return this._http.post(`${this.httpUrl}/getIndexes`, request, this.httpOptions); - } - - /** - * Drop an index of a table - */ - dropIndex(index: IndexModel) { - return this._http.post(`${this.httpUrl}/dropIndex`, index, this.httpOptions); - } - - /** - * Create an index - */ - createIndex(index: IndexModel) { - return this._http.post(`${this.httpUrl}/createIndex`, index, this.httpOptions); - } - - /** - * Get data placement information - */ - getDataPlacements(namespaceId: number, entityId: number, fields: number[] = null) { - const meta = new EntityMeta(namespaceId, entityId, null, fields); - return this._http.post(`${this.httpUrl}/getPlacements`, meta, this.httpOptions); - } - - /** - * Get data placement information - */ - getCollectionPlacements(namespaceId: number, entityId: number, fields: number[]) { - const meta = new EntityMeta(namespaceId, entityId, null, fields); - return this._http.post(`${this.httpUrl}/getCollectionPlacements`, meta, this.httpOptions); - } - - getGraphPlacements(namespaceId: number, entityId: number, fields: number[]) { - const meta = new EntityMeta(namespaceId, entityId, null, fields); - return this._http.post(`${this.httpUrl}/getGraphPlacements`, meta, this.httpOptions); - } - - getUnderlyingTable(request: EntityRequest) { - return this._http.post(`${this.httpUrl}/getUnderlyingTable`, request, this.httpOptions); - } - - - /** - * Add or drop a placement - */ - addDropPlacement(namespaceId: number, entityId: number, storeId: number, method: Method, columns = []) { - const meta = new PlacementMeta(namespaceId, entityId, null, storeId, method, columns); - return this._http.post(`${this.httpUrl}/addDropPlacement`, meta, this.httpOptions); - } - - /** - * Add or drop a placement - */ - addDropGraphPlacement(graph: string, store: number, method: Method) { - let code: string; - switch (method) { - case Method.ADD: - code = `CREATE PLACEMENT OF ${graph} ON STORE ${store}`; - break; - case Method.DROP: - code = `DROP PLACEMENT OF ${graph} ON STORE ${store}`; - break; - } - const request = new QueryRequest(code, false, true, 'cypher', graph); - - return this.anyQueryBlocking(request); - } - - /** - * Add or drop a placement - */ - addDropCollectionPlacement(namespace: string, collection: string, store: string, method: Method) { - let code: string; - switch (method) { - case 'ADD': - code = `db.${collection}.addPlacement( "${store}" )`; - break; - case 'DROP': - code = `db.${collection}.deletePlacement( "${store}" )`; - break; - } - const request = new QueryRequest(code, false, true, 'cypher', namespace); - return this.anyQueryBlocking(request); - } - - - // PARTITIONING - - getPartitionTypes() { - return this._http.get(`${this.httpUrl}/getPartitionTypes`, this.httpOptions); - } - - getPartitionFunctionModel(request: PartitioningRequest) { - return this._http.post(`${this.httpUrl}/getPartitionFunctionModel`, request, this.httpOptions); - } - - partitionTable(request: PartitionFunctionModel) { - return this._http.post(`${this.httpUrl}/partitionTable`, request, this.httpOptions); - } - - mergePartitions(request: PartitioningRequest) { - return this._http.post(`${this.httpUrl}/mergePartitions`, request, this.httpOptions); - } - - modifyPartitions(request: ModifyPartitionRequest) { - return this._http.post(`${this.httpUrl}/modifyPartitions`, request, this.httpOptions); - } - - /** - * Get information for the Uml view, such as - * the list of all tables of a schema with their columns - * and a list of all the foreign keys of a schema - */ - getUml(request: EditTableRequest): Observable { - return >this._http.post(`${this.httpUrl}/getUml`, request, this.httpOptions); - } - - /** - * Initialize the websocket for the queryAnalyzer - */ - private initWebSocket() { - this.socket = webSocket({ - url: this._settings.getConnection('crud.socket'), - openObserver: { - next: (n) => { - this.reconnected.emit(true); - this.connected = true; + } + + addDockerInstance(host: string, alias: string, registry: string, communicationPort: number, handshakePort: number, proxyPort: number) { + return this._http.post(`${this.httpUrl}/addDockerInstance`, { + 'host': host, + 'alias': alias, + 'communicationPort': communicationPort, + 'handshakePort': handshakePort, + 'proxyPort': proxyPort, + }, this.httpOptions); + } + + testDockerInstance(id: number) { + return this._http.post(`${this.httpUrl}/testDockerInstance/${id}`, null, this.httpOptions); + } + + getDockerInstance(id: number) { + return this._http.get(`${this.httpUrl}/getDockerInstance/${id}`); + } + + getDockerInstances() { + return this._http.get(`${this.httpUrl}/getDockerInstances`); + } + + updateDockerInstance(id: number, hostname: string, alias: string, registry: string) { + return this._http.post(`${this.httpUrl}/updateDockerInstance`, { + 'id': id.toString(), + 'hostname': hostname, + 'alias': alias, + 'registry': registry + }, this.httpOptions); + } + + reconnectToDockerInstance(id: number) { + return this._http.post(`${this.httpUrl}/reconnectToDockerInstance`, {'id': id.toString()}, this.httpOptions); + } + + removeDockerInstance(id: number) { + return this._http.post(`${this.httpUrl}/removeDockerInstance`, {'id': id.toString()}, this.httpOptions); + } + + getAutoDockerStatus() { + return this._http.get(`${this.httpUrl}/getAutoDockerStatus`); + } + + doAutoHandshake() { + return this._http.post(`${this.httpUrl}/doAutoHandshake`, '', this.httpOptions); + } + + startHandshake(hostname: string) { + return this._http.post(`${this.httpUrl}/startHandshake`, hostname, this.httpOptions); + } + + getHandshake(hostname: string) { + return this._http.get(`${this.httpUrl}/getHandshake/${new HttpUrlEncodingCodec().encodeKey(hostname)}`); + } + + cancelHandshake(hostname: string) { + return this._http.post(`${this.httpUrl}/cancelHandshake`, hostname, this.httpOptions); + } + + getDockerSettings() { + return this._http.get(`${this.httpUrl}/getDockerSettings/`); + } + + changeDockerSettings(settings: DockerSettings) { + return this._http.post(`${this.httpUrl}/changeDockerSettings`, settings, this.httpOptions); + } + + getNameValidator(required: boolean = false) { + if (required) { + return [Validators.pattern('^[a-zA-Z_][a-zA-Z0-9_]*$'), Validators.required, Validators.max(100)]; + } else { + return [Validators.pattern('^[a-zA-Z_][a-zA-Z0-9_]*$'), Validators.max(100)]; } - } - }); - this.socket.subscribe( - msg => { - }, - err => { - //this.reconnected.emit(false); - this.connected = false; - setTimeout(() => { - this.initWebSocket(); - }, +this._settings.getSetting('reconnection.timeout')); + } + + invalidNameMessage(type: string = '') { + type = type + ' '; + return `Please provide a valid ${type}name`; + } + + getValidationRegex() { + return new RegExp('^[a-zA-Z_][a-zA-Z0-9_]*$'); + } + + getNamespaceValidationRegex() { + return new RegExp('^[a-z_][a-z0-9_]*$'); + } + + getAdapterNameValidationRegex() { + // TODO: re-enable underscores when graph namespaces work with it + //return new RegExp('^[a-z][a-z0-9_]*$'); + return new RegExp('^[a-z][a-z0-9]*$'); + } + + nameIsValid(name: string) { + const regex = this.getValidationRegex(); + return regex.test(name) && name.length <= 100; + } + + getValidationClass(name: string) { + const regex = this.getValidationRegex(); + if (name === '') { + return ''; + } else if (regex.test(name) && name.length <= 100) { + return 'is-valid'; + } else { + return 'is-invalid'; } - ); - } - - - anyQueryBlocking(queryRequest: QueryRequest) { - return this._http.post(`${this.httpUrl}/anyQuery`, queryRequest, this.httpOptions); - } - - onSocketEvent() { - return this.socket; - } - - onReconnection() { - return this.reconnected; - } - - getAnalyzerPage(analyzerId: string, analyzerPage: string) { - return this._http.post(`${this.httpUrl}/getAnalyzerPage`, [analyzerId, analyzerPage], this.httpOptions); - } - - /** - * Add a foreign key (in the Uml view) - */ - createForeignKey(fk: ForeignKey) { - return this._http.post(`${this.httpUrl}/createForeignKey`, fk, this.httpOptions); - } - - /** - * Execute a relational algebra - */ - 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); - } - return socket.sendMessage(request); - } - - - renameTable(meta: EntityMeta) { - return this._http.post(`${this.httpUrl}/renameTable`, meta, this.httpOptions); - } - - /** - * Send a request to either create or drop a schema - */ - createOrDropNamespace(namespace: Namespace) { - return this._http.post(`${this.httpUrl}/namespaceRequest`, namespace, this.httpOptions); - } - - /** - * Get all supported data types of the DBMS. - */ - getTypeInfo() { - return this._http.get(`${this.httpUrl}/getTypeInfo`, this.httpOptions); - } - - /** - * Fetch available actions for foreign key constraints - */ - getFkActions() { - return this._http.get(`${this.httpUrl}/getForeignKeyActions`, this.httpOptions); - } - - getTypeSchemas() { - return this._http.get(`${this.httpUrl}/getTypeSchemas`, this.httpOptions); - } - - getStores() { - return this._http.get(`${this.httpUrl}/getStores`); - } - - getAvailableStoresForIndexes(request: IndexModel) { - return this._http.post(`${this.httpUrl}/getAvailableStoresForIndexes`, request, this.httpOptions); - } - - updateAdapterSettings(adapter: AdapterModel) { - return this._http.post(`${this.httpUrl}/updateAdapterSettings`, adapter); - } - - getSources() { - return this._http.get(`${this.httpUrl}/getSources`); - } - - getAvailableStores() { - return this._http.get(`${this.httpUrl}/getAvailableStores`); - } - - getAvailableSources() { - return this._http.get(`${this.httpUrl}/getAvailableSources`); - } - - createAdapter(adapter: AdapterModel) { - return this._http.post(`${this.httpUrl}/createAdapter`, adapter, this.httpOptions); - } - - - pathAccess(req: PathAccessRequest) { - return this._http.post(`${this.httpUrl}/pathAccess`, req); - } - - removeAdapter(storeId: string) { - return this._http.post(`${this.httpUrl}/removeAdapter`, storeId, this.httpOptions); - } - - getQueryInterfaces() { - return this._http.get(`${this.httpUrl}/getQueryInterfaces`); - } - - getAvailableQueryInterfaces() { - return this._http.get(`${this.httpUrl}/getAvailableQueryInterfaces`); - } - - createQueryInterface(request: any) { - return this._http.post(`${this.httpUrl}/createQueryInterface`, request, this.httpOptions); - } - - updateQueryInterfaceSettings(request: QueryInterface) { - 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); - } - - getUsedDockerPorts() { - return this._http.get(`${this.httpUrl}/usedDockerPorts`); - } - - getDocumentDatabases() { - return this._http.get(`${this.httpUrl}/getDocumentDatabases`); - } - - - /** - * Get the http url with which multimedia files can be displayed or downloaded - */ - getFileUrl(fileName: string): string { - if (fileName.startsWith('http://') || fileName.startsWith('https://')) { - return fileName; - } - return `${this.httpUrl}/getFile/${fileName}`; - } - - getFile(fileName: string) { - const url = this.getFileUrl(fileName); - - //blob as json: https://stackoverflow.com/questions/42898162/how-to-read-content-disposition-headers-from-server-response-angular-2 - return this._http.get(url, { - reportProgress: true, - observe: 'events', - responseType: 'blob' as 'json', - headers: new HttpHeaders({'Content-Type': 'application/octet-stream'}) - }); - } - - addDockerInstance(host: string, alias: string, registry: string, communicationPort: number, handshakePort: number, proxyPort: number) { - return this._http.post(`${this.httpUrl}/addDockerInstance`, { - 'host': host, - 'alias': alias, - 'communicationPort': communicationPort, - 'handshakePort': handshakePort, - 'proxyPort': proxyPort, - }, this.httpOptions); - } - - testDockerInstance(id: number) { - return this._http.post(`${this.httpUrl}/testDockerInstance/${id}`, null, this.httpOptions); - } - - getDockerInstance(id: number) { - return this._http.get(`${this.httpUrl}/getDockerInstance/${id}`); - } - - getDockerInstances() { - return this._http.get(`${this.httpUrl}/getDockerInstances`); - } - - updateDockerInstance(id: number, hostname: string, alias: string, registry: string) { - return this._http.post(`${this.httpUrl}/updateDockerInstance`, { - 'id': id.toString(), - 'hostname': hostname, - 'alias': alias, - 'registry': registry - }, this.httpOptions); - } - - reconnectToDockerInstance(id: number) { - return this._http.post(`${this.httpUrl}/reconnectToDockerInstance`, {'id': id.toString()}, this.httpOptions); - } - - removeDockerInstance(id: number) { - return this._http.post(`${this.httpUrl}/removeDockerInstance`, {'id': id.toString()}, this.httpOptions); - } - - getAutoDockerStatus() { - return this._http.get(`${this.httpUrl}/getAutoDockerStatus`); - } - - doAutoHandshake() { - return this._http.post(`${this.httpUrl}/doAutoHandshake`, '', this.httpOptions); - } - - startHandshake(hostname: string) { - return this._http.post(`${this.httpUrl}/startHandshake`, hostname, this.httpOptions); - } - - getHandshake(hostname: string) { - return this._http.get(`${this.httpUrl}/getHandshake/${new HttpUrlEncodingCodec().encodeKey(hostname)}`); - } - - cancelHandshake(hostname: string) { - return this._http.post(`${this.httpUrl}/cancelHandshake`, hostname, this.httpOptions); - } - - getDockerSettings() { - return this._http.get(`${this.httpUrl}/getDockerSettings/`); - } - - changeDockerSettings(settings: DockerSettings) { - return this._http.post(`${this.httpUrl}/changeDockerSettings`, settings, this.httpOptions); - } - - getNameValidator(required: boolean = false) { - if (required) { - return [Validators.pattern('^[a-zA-Z_][a-zA-Z0-9_]*$'), Validators.required, Validators.max(100)]; - } else { - return [Validators.pattern('^[a-zA-Z_][a-zA-Z0-9_]*$'), Validators.max(100)]; - } - } - - invalidNameMessage(type: string = '') { - type = type + ' '; - return `Please provide a valid ${type}name`; - } - - getValidationRegex() { - return new RegExp('^[a-zA-Z_][a-zA-Z0-9_]*$'); - } - - getNamespaceValidationRegex() { - return new RegExp('^[a-z_][a-z0-9_]*$'); - } - - getAdapterNameValidationRegex() { - // TODO: re-enable underscores when graph namespaces work with it - //return new RegExp('^[a-z][a-z0-9_]*$'); - return new RegExp('^[a-z][a-z0-9]*$'); - } - - nameIsValid(name: string) { - const regex = this.getValidationRegex(); - return regex.test(name) && name.length <= 100; - } - - getValidationClass(name: string) { - const regex = this.getValidationRegex(); - if (name === '') { - return ''; - } else if (regex.test(name) && name.length <= 100) { - return 'is-valid'; - } else { - return 'is-invalid'; - } - } - - loadPlugins(files: File[]) { - const formData = new FormData(); - formData.append('action', 'loadPlugins'); - files.forEach(f => { - formData.append('plugins', f); - }); - - return this._http.post(`${this.httpUrl}/loadPlugins`, formData); - } + } + + loadPlugins(files: File[]) { + const formData = new FormData(); + formData.append('action', 'loadPlugins'); + files.forEach(f => { + formData.append('plugins', f); + }); + + return this._http.post(`${this.httpUrl}/loadPlugins`, formData); + } } diff --git a/src/app/services/dbms-types.service.ts b/src/app/services/dbms-types.service.ts index 7b0edf27..4f0d7aef 100644 --- a/src/app/services/dbms-types.service.ts +++ b/src/app/services/dbms-types.service.ts @@ -5,203 +5,203 @@ import {ToasterService} from '../components/toast-exposer/toaster.service'; import {from} from 'rxjs'; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class DbmsTypesService { - private readonly _crud = inject(CrudService); - private readonly _toast = inject(ToasterService); - - constructor() { - this.fetchTypes(); - this.fetchFkActions(); - } - - private numericArray = ['int2', 'int4', 'int8', 'integer', 'bigint', 'smallint', 'float', 'float4', 'float8', 'double', 'bigint']; - private booleanArray = ['bool', 'boolean']; - private dateTimeArray = ['date', 'time', 'timestamp']; - private multimediaArray = ['file', 'image', 'video', 'audio']; - private types = new EventEmitter(); - private _types: PolyType[]; - private foreignKeyActions = new EventEmitter(); - private fetchedFkActions; - - private static removeNull(input: string) { - return input.replace(' NOT NULL', ''); - } - - /** - * Fetches all supported data types of the DBMS. - */ - private fetchTypes() { - this._crud.getTypeInfo().subscribe({ - next: (result: PolyType[]) => { - if (!result) { - this._toast.error('Could not retrieve DBMS types.'); - return; - } - result.sort((a: PolyType, b: PolyType) => { - if (a.name.toLowerCase() < b.name.toLowerCase()) { - return -1; - } - if (a.name.toLowerCase() > b.name.toLowerCase()) { - return 1; - } - return 0; + private readonly _crud = inject(CrudService); + private readonly _toast = inject(ToasterService); + + constructor() { + this.fetchTypes(); + this.fetchFkActions(); + } + + private numericArray = ['int2', 'int4', 'int8', 'integer', 'bigint', 'smallint', 'float', 'float4', 'float8', 'double', 'bigint']; + private booleanArray = ['bool', 'boolean']; + private dateTimeArray = ['date', 'time', 'timestamp']; + private multimediaArray = ['file', 'image', 'video', 'audio']; + private types = new EventEmitter(); + private _types: PolyType[]; + private foreignKeyActions = new EventEmitter(); + private fetchedFkActions; + + private static removeNull(input: string) { + return input.replace(' NOT NULL', ''); + } + + /** + * Fetches all supported data types of the DBMS. + */ + private fetchTypes() { + this._crud.getTypeInfo().subscribe({ + next: (result: PolyType[]) => { + if (!result) { + this._toast.error('Could not retrieve DBMS types.'); + return; + } + result.sort((a: PolyType, b: PolyType) => { + if (a.name.toLowerCase() < b.name.toLowerCase()) { + return -1; + } + if (a.name.toLowerCase() > b.name.toLowerCase()) { + return 1; + } + return 0; + }); + this.types.next(result); + this._types = result; + }, error: err => { + this._toast.error('Could not retrieve DBMS types.'); + } }); - this.types.next(result); - this._types = result; - }, error: err => { - this._toast.error('Could not retrieve DBMS types.'); - } - }); - - } - - /** - * Fetch available actions for foreign key constraints - */ - private fetchFkActions() { - this._crud.getFkActions().subscribe({ - next: res => { - this.foreignKeyActions.next(res); - this.fetchedFkActions = res; - }, error: err => { - this._toast.error('Could not retrieve DBMS foreign key actions.'); - } - }); - } - - /** - * @return EventEmitter with the available foreign key actions - */ - getFkActions() { - if (this.fetchedFkActions) { - return from([this.fetchedFkActions]); - } else { - return this.foreignKeyActions; - } - } - - /** - * @return EventEmitter with the available DBMS data types - */ - getTypes() { - this.fetchTypes(); //this was not even finished for the return - return this.types; - } - - /** - * for ngIf and ngSwitchCase - * usage: _types.numericTypes().includes( val ) - */ - numericTypes() { - return this.numericArray; - } - - /** - * for ngIf and ngSwitchCase - * usage: _types.booleanTypes().includes( val ) - */ - booleanTypes() { - return this.booleanArray; - } - - /** - * for ngIf and ngSwitchCase - * usage: _types.dateTimeTypes().includes( val ) - */ - dateTimeTypes() { - return this.dateTimeArray; - } - - multimediaTypes() { - return this.multimediaArray; - } - - /** - * @param type dmbs type name - * @return if the dbms type is numeric - */ - isNumeric(type: string) { - return this.numericArray.includes(DbmsTypesService.removeNull(type).toLowerCase()); - } - - /** - * @param type dmbs type name - * @return if the dbms type is of boolean type - */ - isBoolean(type: string) { - return this.booleanArray.includes(DbmsTypesService.removeNull(type).toLowerCase()); - } - - /** - * @param type dmbs type name - * @return if the dbms type is of date / time / timestamp type - */ - isDateTime(type: string) { - return this.dateTimeArray.includes(DbmsTypesService.removeNull(type).toLowerCase()); - } - - /** - * @param type PolyType - * @return Return true if the type is of the multimedia family - */ - isMultimedia(type: string) { - return this.multimediaArray.includes(DbmsTypesService.removeNull(type).toLowerCase()); - } - - /** - * @param type dmbs type name - * @return if the dbms type supports precision - */ - supportsPrecision(type: string): boolean { - return this.getTypeSignature(type) >= 3; - } - - - /** - * @param type dmbs type name - * @return if the dbms type supports scale - */ - supportsScale(type: string): boolean { - return this.getTypeSignature(type) >= 7; - } - - - /** - * Check if the labels and placeholders for the precision value should be displayed as "length" or as "precision" - */ - precisionPlaceholder(type: string): string { - switch (DbmsTypesService.removeNull(type).toLowerCase()) { - case 'varchar': - return 'length'; - default: - return 'precision'; - } - } - - - /** - * Get the type signature from a string - * 1 if NO_NO, 3 if YES_NO, 7 if YES_YES - */ - private getTypeSignature(typeName: string): number { - if (this._types == null || typeName == null) { - return 0; - } - const type: PolyType = this._types.find((v: PolyType) => v.name === typeName); - if (type == null) { - return 0; - } else { - return type.signatures; - } - } - - isDocument(dataModel: string) { - return dataModel.toLowerCase() === 'document'; - } - - isGraph(dataModel: string) { - return dataModel.toLowerCase() === 'graph'; - } + + } + + /** + * Fetch available actions for foreign key constraints + */ + private fetchFkActions() { + this._crud.getFkActions().subscribe({ + next: res => { + this.foreignKeyActions.next(res); + this.fetchedFkActions = res; + }, error: err => { + this._toast.error('Could not retrieve DBMS foreign key actions.'); + } + }); + } + + /** + * @return EventEmitter with the available foreign key actions + */ + getFkActions() { + if (this.fetchedFkActions) { + return from([this.fetchedFkActions]); + } else { + return this.foreignKeyActions; + } + } + + /** + * @return EventEmitter with the available DBMS data types + */ + getTypes() { + this.fetchTypes(); //this was not even finished for the return + return this.types; + } + + /** + * for ngIf and ngSwitchCase + * usage: _types.numericTypes().includes( val ) + */ + numericTypes() { + return this.numericArray; + } + + /** + * for ngIf and ngSwitchCase + * usage: _types.booleanTypes().includes( val ) + */ + booleanTypes() { + return this.booleanArray; + } + + /** + * for ngIf and ngSwitchCase + * usage: _types.dateTimeTypes().includes( val ) + */ + dateTimeTypes() { + return this.dateTimeArray; + } + + multimediaTypes() { + return this.multimediaArray; + } + + /** + * @param type dmbs type name + * @return if the dbms type is numeric + */ + isNumeric(type: string) { + return this.numericArray.includes(DbmsTypesService.removeNull(type).toLowerCase()); + } + + /** + * @param type dmbs type name + * @return if the dbms type is of boolean type + */ + isBoolean(type: string) { + return this.booleanArray.includes(DbmsTypesService.removeNull(type).toLowerCase()); + } + + /** + * @param type dmbs type name + * @return if the dbms type is of date / time / timestamp type + */ + isDateTime(type: string) { + return this.dateTimeArray.includes(DbmsTypesService.removeNull(type).toLowerCase()); + } + + /** + * @param type PolyType + * @return Return true if the type is of the multimedia family + */ + isMultimedia(type: string) { + return this.multimediaArray.includes(DbmsTypesService.removeNull(type).toLowerCase()); + } + + /** + * @param type dmbs type name + * @return if the dbms type supports precision + */ + supportsPrecision(type: string): boolean { + return this.getTypeSignature(type) >= 3; + } + + + /** + * @param type dmbs type name + * @return if the dbms type supports scale + */ + supportsScale(type: string): boolean { + return this.getTypeSignature(type) >= 7; + } + + + /** + * Check if the labels and placeholders for the precision value should be displayed as "length" or as "precision" + */ + precisionPlaceholder(type: string): string { + switch (DbmsTypesService.removeNull(type).toLowerCase()) { + case 'varchar': + return 'length'; + default: + return 'precision'; + } + } + + + /** + * Get the type signature from a string + * 1 if NO_NO, 3 if YES_NO, 7 if YES_YES + */ + private getTypeSignature(typeName: string): number { + if (this._types == null || typeName == null) { + return 0; + } + const type: PolyType = this._types.find((v: PolyType) => v.name === typeName); + if (type == null) { + return 0; + } else { + return type.signatures; + } + } + + isDocument(dataModel: string) { + return dataModel.toLowerCase() === 'document'; + } + + isGraph(dataModel: string) { + return dataModel.toLowerCase() === 'graph'; + } } diff --git a/src/app/services/information.service.ts b/src/app/services/information.service.ts index 365aa537..0505e539 100644 --- a/src/app/services/information.service.ts +++ b/src/app/services/information.service.ts @@ -5,101 +5,101 @@ import {WebuiSettingsService} from './webui-settings.service'; import {InformationObject} from '../models/information-page.model'; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class InformationService { - private readonly _http = inject(HttpClient); - private readonly _settings = inject(WebuiSettingsService); - - constructor() { - this.initWebSocket(); - } - - public connected = false; - private reconnected = new EventEmitter(); - public socket; - httpUrl = this._settings.getConnection('information.rest'); - httpOptions = {headers: new HttpHeaders({'Content-Type': 'application/json'})}; - reconnetor: number; - - getPage(pageId: string) { - return this._http.post(`${this.httpUrl}/getPage`, pageId, this.httpOptions); - } - - getPageList() { - return this._http.get(`${this.httpUrl}/getPageList`, this.httpOptions); - } - - refreshPage(id: string) { - return this._http.post(`${this.httpUrl}/refreshPage`, id, this.httpOptions); - } - - refreshGroup(id: string) { - return this._http.post(`${this.httpUrl}/refreshGroup`, id, this.httpOptions); - } - - executeAction(i: InformationObject) { - return this._http.post(`${this.httpUrl}/executeAction`, JSON.stringify(i), this.httpOptions); - } - - - //websocket: - //https://rxjs-dev.firebaseapp.com/api/webSocket/webSocket - //openObserver: - //https://rxjs-dev.firebaseapp.com/api/webSocket/WebSocketSubjectConfig - //it's not possible to suppress the websocket exception during the reconnect: - //https://stackoverflow.com/questions/31978298/suppress-websocket-connection-to-xyz-failed - private initWebSocket() { - this.socket = webSocket({ - url: this._settings.getConnection('information.socket'), - openObserver: { - next: (n) => { - this.reconnected.emit(true); - this.connected = true; - } - } - }); - this.socket.subscribe( - msg => { - }, - err => { - //this.reconnected.emit(false); - this.connected = false; - this.startReconnecting(); + private readonly _http = inject(HttpClient); + private readonly _settings = inject(WebuiSettingsService); + + constructor() { + this.initWebSocket(); + } + + public connected = false; + private reconnected = new EventEmitter(); + public socket; + httpUrl = this._settings.getConnection('information.rest'); + httpOptions = {headers: new HttpHeaders({'Content-Type': 'application/json'})}; + reconnetor: number; + + getPage(pageId: string) { + return this._http.post(`${this.httpUrl}/getPage`, pageId, this.httpOptions); + } + + getPageList() { + return this._http.get(`${this.httpUrl}/getPageList`, this.httpOptions); + } + + refreshPage(id: string) { + return this._http.post(`${this.httpUrl}/refreshPage`, id, this.httpOptions); + } + + refreshGroup(id: string) { + return this._http.post(`${this.httpUrl}/refreshGroup`, id, this.httpOptions); + } + + executeAction(i: InformationObject) { + return this._http.post(`${this.httpUrl}/executeAction`, JSON.stringify(i), this.httpOptions); + } + + + //websocket: + //https://rxjs-dev.firebaseapp.com/api/webSocket/webSocket + //openObserver: + //https://rxjs-dev.firebaseapp.com/api/webSocket/WebSocketSubjectConfig + //it's not possible to suppress the websocket exception during the reconnect: + //https://stackoverflow.com/questions/31978298/suppress-websocket-connection-to-xyz-failed + private initWebSocket() { + this.socket = webSocket({ + url: this._settings.getConnection('information.socket'), + openObserver: { + next: (n) => { + this.reconnected.emit(true); + this.connected = true; + } + } + }); + this.socket.subscribe( + msg => { + }, + err => { + //this.reconnected.emit(false); + this.connected = false; + this.startReconnecting(); + } + ); + } + + private startReconnecting() { + if (this.reconnetor) { + clearTimeout(this.reconnetor); } - ); - } + this.reconnetor = setTimeout(() => { + this.initWebSocket(); + }, +this._settings.getSetting('reconnection.timeout')); + } + + socketSend(msg: string) { + this.socket.next(msg); + } - private startReconnecting() { - if (this.reconnetor) { - clearTimeout(this.reconnetor); + onSocketEvent() { + return this.socket; } - this.reconnetor = setTimeout(() => { - this.initWebSocket(); - }, +this._settings.getSetting('reconnection.timeout')); - } - - socketSend(msg: string) { - this.socket.next(msg); - } - - onSocketEvent() { - return this.socket; - } - - closeSocket() { - this.socket.complete(); - } - - onReconnection() { - return this.reconnected; - } - - manualReconnect() { - if (this.reconnetor) { - clearTimeout(this.reconnetor); + + closeSocket() { + this.socket.complete(); + } + + onReconnection() { + return this.reconnected; + } + + manualReconnect() { + if (this.reconnetor) { + clearTimeout(this.reconnetor); + } + this.initWebSocket(); } - this.initWebSocket(); - } } diff --git a/src/app/services/plugin.service.ts b/src/app/services/plugin.service.ts index fa33483d..11b0c857 100644 --- a/src/app/services/plugin.service.ts +++ b/src/app/services/plugin.service.ts @@ -5,52 +5,52 @@ import {inject, Injectable} from '@angular/core'; import {PluginEntity} from '../models/ui-request.model'; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class PluginService { - private readonly _http = inject(HttpClient); - private readonly _settings = inject(WebuiSettingsService); - private readonly _crud = inject(CrudService) + private readonly _http = inject(HttpClient); + private readonly _settings = inject(WebuiSettingsService); + private readonly _crud = inject(CrudService); - private httpUrl = this._settings.getConnection('crud.rest'); + private httpUrl = this._settings.getConnection('crud.rest'); - private infoUrl = this._settings.getConnection('information.rest'); + private infoUrl = this._settings.getConnection('information.rest'); - private httpOptions = {headers: new HttpHeaders({'Content-Type': 'application/json'})}; - - constructor() { - } + private httpOptions = {headers: new HttpHeaders({'Content-Type': 'application/json'})}; + constructor() { + } - private availablePlugins: [PluginEntity] = null; - private pluginRequestFired: number = null; - private REQUEST_DELAY: number = 1000 * 20; + private availablePlugins: [PluginEntity] = null; + private pluginRequestFired: number = null; + private REQUEST_DELAY: number = 1000 * 20; - getEnabledPlugins(): string[] { - return this.getAvailablePlugins().map(p => p.id); - } - getAvailablePlugins(): PluginEntity[] { - if (this.pluginRequestFired === null) { - this.pluginRequestFired = Date.now() - (this.REQUEST_DELAY + 100); + getEnabledPlugins(): string[] { + return this.getAvailablePlugins().map(p => p.id); } - if (this.availablePlugins === null) { - const today = Date.now(); - if ((this.pluginRequestFired + this.REQUEST_DELAY) < today) { - this.pluginRequestFired = today; - this._http.get(`${this.httpUrl}/getAvailablePlugins`, this.httpOptions) - .subscribe(res => { - this.availablePlugins = <[PluginEntity]>res; - }); - } - return []; + + getAvailablePlugins(): PluginEntity[] { + if (this.pluginRequestFired === null) { + this.pluginRequestFired = Date.now() - (this.REQUEST_DELAY + 100); + } + if (this.availablePlugins === null) { + const today = Date.now(); + if ((this.pluginRequestFired + this.REQUEST_DELAY) < today) { + this.pluginRequestFired = today; + this._http.get(`${this.httpUrl}/getAvailablePlugins`, this.httpOptions) + .subscribe(res => { + this.availablePlugins = <[PluginEntity]>res; + }); + } + return []; + } + return this.availablePlugins; } - return this.availablePlugins; - } - loadPlugins(files: File[]) { - return this._crud.loadPlugins(files); - } + loadPlugins(files: File[]) { + return this._crud.loadPlugins(files); + } } diff --git a/src/app/services/right-sidebar-to-relationalalgebra.service.ts b/src/app/services/right-sidebar-to-relationalalgebra.service.ts index 362a9a35..86e0ecaf 100644 --- a/src/app/services/right-sidebar-to-relationalalgebra.service.ts +++ b/src/app/services/right-sidebar-to-relationalalgebra.service.ts @@ -1,17 +1,17 @@ import {EventEmitter, Injectable, Output} from '@angular/core'; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class RightSidebarToRelationalalgebraService { - constructor() { - } + constructor() { + } - @Output() change: EventEmitter = new EventEmitter(); + @Output() change: EventEmitter = new EventEmitter(); - toggle() { - this.change.emit(true); - } + toggle() { + this.change.emit(true); + } } diff --git a/src/app/services/util.service.ts b/src/app/services/util.service.ts index ecbb4620..2147d5d3 100644 --- a/src/app/services/util.service.ts +++ b/src/app/services/util.service.ts @@ -1,58 +1,58 @@ import {Injectable} from '@angular/core'; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class UtilService { - textareaHeight(val: string) { - val = val || ''; - return Math.min(val.split('\n').length, 5); - } - - //see https://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable-string/10420404 - humanFileSize(size: number): string { - if (size !== 0 && !size) { - return; + textareaHeight(val: string) { + val = val || ''; + return Math.min(val.split('\n').length, 5); } - const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1000)); - return +(size / Math.pow(1000, i)).toFixed(2) + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]; - } - limitedString(str: string, maxLength = 120, postfix = '...') { - if (str === undefined || str === null) { - return; + //see https://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable-string/10420404 + humanFileSize(size: number): string { + if (size !== 0 && !size) { + return; + } + const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1000)); + return +(size / Math.pow(1000, i)).toFixed(2) + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]; } - if (str.length <= maxLength) { - return str; + + limitedString(str: string, maxLength = 120, postfix = '...') { + if (str === undefined || str === null) { + return; + } + if (str.length <= maxLength) { + return str; + } + return str.slice(0, maxLength) + postfix; } - return str.slice(0, maxLength) + postfix; - } - /** - * Modulo for negative numbers - * from https://stackoverflow.com/questions/4467539/javascript-modulo-gives-a-negative-result-for-negative-numbers - */ - mod(n, m) { - return ((n % m) + m) % m; - } + /** + * Modulo for negative numbers + * from https://stackoverflow.com/questions/4467539/javascript-modulo-gives-a-negative-result-for-negative-numbers + */ + mod(n, m) { + return ((n % m) + m) % m; + } - /** - * Copy a string to the clipboard - */ - // from https://stackoverflow.com/questions/49102724/angular-5-copy-to-clipboard - clipboard(msg: string) { - const selBox = document.createElement('textarea'); - selBox.style.position = 'fixed'; - selBox.style.left = '0'; - selBox.style.top = '0'; - selBox.style.opacity = '0'; - selBox.value = msg; - document.body.appendChild(selBox); - selBox.focus(); - selBox.select(); - document.execCommand('copy'); - document.body.removeChild(selBox); - } + /** + * Copy a string to the clipboard + */ + // from https://stackoverflow.com/questions/49102724/angular-5-copy-to-clipboard + clipboard(msg: string) { + const selBox = document.createElement('textarea'); + selBox.style.position = 'fixed'; + selBox.style.left = '0'; + selBox.style.top = '0'; + selBox.style.opacity = '0'; + selBox.value = msg; + document.body.appendChild(selBox); + selBox.focus(); + selBox.select(); + document.execCommand('copy'); + document.body.removeChild(selBox); + } } diff --git a/src/app/services/webSocket.ts b/src/app/services/webSocket.ts index fce0d2bf..88d979e6 100644 --- a/src/app/services/webSocket.ts +++ b/src/app/services/webSocket.ts @@ -4,59 +4,59 @@ import {WebuiSettingsService} from './webui-settings.service'; import {webSocket, WebSocketSubject} from 'rxjs/webSocket'; export class WebSocket { - public readonly _settings = inject(WebuiSettingsService); - private socket: WebSocketSubject; - public readonly connected = new BehaviorSubject(false); - private readonly msgSubject: Subject = new Subject(); - public readonly reconnecting = new Subject(); + public readonly _settings = inject(WebuiSettingsService); + private socket: WebSocketSubject; + public readonly connected = new BehaviorSubject(false); + private readonly msgSubject: Subject = new Subject(); + public readonly reconnecting = new Subject(); - constructor() { - this.initWebSocket(false); - setInterval(() => { - if (this.connected) { - this.socket.next('keepalive'); - } - }, +this._settings.getSetting('reconnection.timeout')); - } - - private initWebSocket(reconnect: boolean) { - this.socket = webSocket({ - url: this._settings.getConnection('crud.socket'), - openObserver: { - next: (n) => { - this.connected.next(true); - if (reconnect) { - this.reconnecting.next(true); - } - } - } - }); - this.socket.subscribe({ - next: msg => { - this.msgSubject.next(msg); - }, - error: err => { - console.log(err); - this.connected.next(false); - setTimeout(() => { - this.initWebSocket(true); + constructor() { + this.initWebSocket(false); + setInterval(() => { + if (this.connected) { + this.socket.next('keepalive'); + } }, +this._settings.getSetting('reconnection.timeout')); - } - }); - } + } + + private initWebSocket(reconnect: boolean) { + this.socket = webSocket({ + url: this._settings.getConnection('crud.socket'), + openObserver: { + next: (n) => { + this.connected.next(true); + if (reconnect) { + this.reconnecting.next(true); + } + } + } + }); + this.socket.subscribe({ + next: msg => { + this.msgSubject.next(msg); + }, + error: err => { + console.log(err); + this.connected.next(false); + setTimeout(() => { + this.initWebSocket(true); + }, +this._settings.getSetting('reconnection.timeout')); + } + }); + } - sendMessage(obj: any): boolean { - this.socket.next(obj); - return this.connected.value; - } + sendMessage(obj: any): boolean { + this.socket.next(obj); + return this.connected.value; + } - onMessage() { - return this.msgSubject; - } + onMessage() { + return this.msgSubject; + } - close() { - this.socket.complete(); - //this will unsubscribe all listeners, see https://stackoverflow.com/questions/52198240 - this.msgSubject.complete(); - } + close() { + this.socket.complete(); + //this will unsubscribe all listeners, see https://stackoverflow.com/questions/52198240 + this.msgSubject.complete(); + } } diff --git a/src/app/services/webui-settings.service.ts b/src/app/services/webui-settings.service.ts index c4280e23..561f9fd5 100644 --- a/src/app/services/webui-settings.service.ts +++ b/src/app/services/webui-settings.service.ts @@ -1,104 +1,104 @@ import {Injectable} from '@angular/core'; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class WebuiSettingsService { - connections = new Map(); - settings = new Map(); - settingsGR = new Map(); - host: string; - version = 'v1'; - - constructor() { - - - // tslint:disable:no-unused-expression - new Setting(this.settings, 'host', 'localhost'); - new Setting(this.settings, 'webUI.port', '7659'); - new Setting(this.settings, 'config.prefix', 'config'); - new Setting(this.settings, 'information.prefix', 'info'); - new Setting(this.settings, 'reconnection.timeout', '500'); - - this.host = localStorage.getItem('host'); - - this.connections.set('config.rest', - 'http://' + this.host + ':' + localStorage.getItem('webUI.port') + '/' + localStorage.getItem('config.prefix') + '/' + this.version); - this.connections.set('config.socket', - 'ws://' + this.host + ':' + localStorage.getItem('webUI.port') + '/config'); - this.connections.set('information.rest', - 'http://' + this.host + ':' + localStorage.getItem('webUI.port') + '/' + localStorage.getItem('information.prefix') + '/' + this.version); - this.connections.set('information.socket', - 'ws://' + this.host + ':' + localStorage.getItem('webUI.port') + '/info'); - this.connections.set('crud.rest', - 'http://' + this.host + ':' + localStorage.getItem('webUI.port')); - this.connections.set('crud.socket', - 'ws://' + this.host + ':' + localStorage.getItem('webUI.port') + '/webSocket'); - this.connections.set('main.socket', 'http://' + this.host + ':' + localStorage.getItem('webUI.port')); - this.connections.set('notebooks.rest', - 'http://' + this.host + ':' + localStorage.getItem('webUI.port') + '/notebooks'); - this.connections.set('notebooks.socket', - 'ws://' + this.host + ':' + localStorage.getItem('webUI.port') + '/notebooks/webSocket'); - this.connections.set('notebooks.file', - 'http://' + this.host + ':' + localStorage.getItem('webUI.port') + '/notebooks/file'); - - } - - public getConnection(key: string) { - return this.connections.get(key); - } - - public getSettings() { - return this.settings; - } - - - public setSetting(key: string, val: string) { - this.settings.get(key).value = val; - localStorage.setItem(key, val); - } - - public getSetting(key: string) { - return this.settings.get(key).value; - } - - public getSettingsGR() { - return this.settingsGR; - } - - public setSettingGR(key: string, val: string) { - this.settingsGR.set(key, val); - localStorage.setItem(key, val); - } - - public reset() { - for (const s of this.settings.values()) { - localStorage.setItem(s.key, s.default); + connections = new Map(); + settings = new Map(); + settingsGR = new Map(); + host: string; + version = 'v1'; + + constructor() { + + + // tslint:disable:no-unused-expression + new Setting(this.settings, 'host', 'localhost'); + new Setting(this.settings, 'webUI.port', '7659'); + new Setting(this.settings, 'config.prefix', 'config'); + new Setting(this.settings, 'information.prefix', 'info'); + new Setting(this.settings, 'reconnection.timeout', '500'); + + this.host = localStorage.getItem('host'); + + this.connections.set('config.rest', + 'http://' + this.host + ':' + localStorage.getItem('webUI.port') + '/' + localStorage.getItem('config.prefix') + '/' + this.version); + this.connections.set('config.socket', + 'ws://' + this.host + ':' + localStorage.getItem('webUI.port') + '/config'); + this.connections.set('information.rest', + 'http://' + this.host + ':' + localStorage.getItem('webUI.port') + '/' + localStorage.getItem('information.prefix') + '/' + this.version); + this.connections.set('information.socket', + 'ws://' + this.host + ':' + localStorage.getItem('webUI.port') + '/info'); + this.connections.set('crud.rest', + 'http://' + this.host + ':' + localStorage.getItem('webUI.port')); + this.connections.set('crud.socket', + 'ws://' + this.host + ':' + localStorage.getItem('webUI.port') + '/webSocket'); + this.connections.set('main.socket', 'http://' + this.host + ':' + localStorage.getItem('webUI.port')); + this.connections.set('notebooks.rest', + 'http://' + this.host + ':' + localStorage.getItem('webUI.port') + '/notebooks'); + this.connections.set('notebooks.socket', + 'ws://' + this.host + ':' + localStorage.getItem('webUI.port') + '/notebooks/webSocket'); + this.connections.set('notebooks.file', + 'http://' + this.host + ':' + localStorage.getItem('webUI.port') + '/notebooks/file'); + + } + + public getConnection(key: string) { + return this.connections.get(key); + } + + public getSettings() { + return this.settings; + } + + + public setSetting(key: string, val: string) { + this.settings.get(key).value = val; + localStorage.setItem(key, val); + } + + public getSetting(key: string) { + return this.settings.get(key).value; + } + + public getSettingsGR() { + return this.settingsGR; + } + + public setSettingGR(key: string, val: string) { + this.settingsGR.set(key, val); + localStorage.setItem(key, val); + } + + public reset() { + for (const s of this.settings.values()) { + localStorage.setItem(s.key, s.default); + } + location.reload(); } - location.reload(); - } } export class Setting { - key: string; - value: string; - default: string; - order: number; - - constructor(map: Map, key: string, defaultValue: string, order = 1) { - this.key = key; - this.default = defaultValue; - if (localStorage.getItem(key) === null) { - this.value = defaultValue; - localStorage.setItem(key, defaultValue); - } else { - this.value = localStorage.getItem(key); + key: string; + value: string; + default: string; + order: number; + + constructor(map: Map, key: string, defaultValue: string, order = 1) { + this.key = key; + this.default = defaultValue; + if (localStorage.getItem(key) === null) { + this.value = defaultValue; + localStorage.setItem(key, defaultValue); + } else { + this.value = localStorage.getItem(key); + } + this.order = order; + + map.set(key, this); } - this.order = order; - - map.set(key, this); - } } diff --git a/src/app/views/about/about.component.ts b/src/app/views/about/about.component.ts index 77653220..fb342c30 100644 --- a/src/app/views/about/about.component.ts +++ b/src/app/views/about/about.component.ts @@ -2,19 +2,19 @@ import {Component, inject, OnInit} from '@angular/core'; import {LeftSidebarService} from '../../components/left-sidebar/left-sidebar.service'; @Component({ - selector: 'app-about', - templateUrl: './about.component.html', - styleUrls: ['./about.component.scss'] + selector: 'app-about', + templateUrl: './about.component.html', + styleUrls: ['./about.component.scss'] }) export class AboutComponent implements OnInit { - private readonly _sidebar = inject(LeftSidebarService); + private readonly _sidebar = inject(LeftSidebarService); - constructor() { - } + constructor() { + } - ngOnInit() { - this._sidebar.hide(); - } + ngOnInit() { + this._sidebar.hide(); + } } diff --git a/src/app/views/adapters/adapter.model.ts b/src/app/views/adapters/adapter.model.ts index 4654473a..fbbad8cd 100644 --- a/src/app/views/adapters/adapter.model.ts +++ b/src/app/views/adapters/adapter.model.ts @@ -2,74 +2,74 @@ import {IndexMethodModel, ResultException} from '../../components/data-view/mode import {AdapterSettingValueModel, DeployMode, IdEntity} from '../../models/catalog.model'; export class AdapterModel extends IdEntity { - readonly adapterName: string; - readonly settings: Map; - readonly persistent: boolean; - readonly type: AdapterType; - readonly mode: DeployMode; - indexMethods: IndexMethodModel[]; + readonly adapterName: string; + readonly settings: Map; + readonly persistent: boolean; + readonly type: AdapterType; + readonly mode: DeployMode; + indexMethods: IndexMethodModel[]; - constructor(uniqueName: string, adapterName: string, settings: Map, persistent: boolean, type: AdapterType, deployMode: DeployMode) { - super(-1, uniqueName); - this.adapterName = adapterName; - this.settings = settings; - this.persistent = persistent; - this.type = type; - this.mode = deployMode; - } + constructor(uniqueName: string, adapterName: string, settings: Map, persistent: boolean, type: AdapterType, deployMode: DeployMode) { + super(-1, uniqueName); + this.adapterName = adapterName; + this.settings = settings; + this.persistent = persistent; + this.type = type; + this.mode = deployMode; + } } export enum AdapterType { - STORE = 'STORE', - SOURCE = 'SOURCE' + STORE = 'STORE', + SOURCE = 'SOURCE' } export interface AdapterInformation { - name: string; - description: string; - adapterName: string; - type: string; - adapterSettings: AdapterSettingValueModel[]; + name: string; + description: string; + adapterName: string; + type: string; + adapterSettings: AdapterSettingValueModel[]; } export interface Placements { - stores: AdapterModel[]; - exception: ResultException; - isPartitioned: boolean; - partitionNames: string[]; - tableType: string; + stores: AdapterModel[]; + exception: ResultException; + isPartitioned: boolean; + partitionNames: string[]; + tableType: string; } export interface UnderlyingTables { - exception: ResultException; - underlyingTable: {}; + exception: ResultException; + underlyingTable: {}; } export interface MaterializedInfos { - exception: ResultException; - materializedInfo: []; + exception: ResultException; + materializedInfo: []; } export enum PlacementType { - MANUAL = 'MANUAL', - AUTOMATIC = 'AUTOMATIC' + MANUAL = 'MANUAL', + AUTOMATIC = 'AUTOMATIC' } export enum PartitionType { - NONE, - RANGE, - LIST, - HASH, - ROUNDROBIN + NONE, + RANGE, + LIST, + HASH, + ROUNDROBIN } export class PolyMap extends Map { - public toJSON() { - return Object.fromEntries(this.entries()); - } + public toJSON() { + return Object.fromEntries(this.entries()); + } } diff --git a/src/app/views/adapters/adapters.component.ts b/src/app/views/adapters/adapters.component.ts index 3dc23d83..163f07cd 100644 --- a/src/app/views/adapters/adapters.component.ts +++ b/src/app/views/adapters/adapters.component.ts @@ -1,642 +1,667 @@ -import {Component, computed, effect, inject, Injector, OnDestroy, OnInit, Signal, signal, WritableSignal} from '@angular/core'; +import { + Component, + computed, + effect, + inject, + Injector, + OnDestroy, + OnInit, + Signal, + signal, + WritableSignal +} from '@angular/core'; import {CrudService} from '../../services/crud.service'; import {ActivatedRoute, Router} from '@angular/router'; import {AdapterModel, AdapterType, PolyMap} from './adapter.model'; import {ToasterService} from '../../components/toast-exposer/toaster.service'; -import {AbstractControl, FormGroup, UntypedFormArray, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, ValidatorFn, Validators} from '@angular/forms'; +import { + AbstractControl, + FormGroup, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + ValidatorFn, + Validators +} from '@angular/forms'; import {PathAccessRequest, RelationalResult} from '../../components/data-view/models/result-set.model'; import {Subscription} from 'rxjs'; import {CatalogService} from '../../services/catalog.service'; -import {AdapterSettingModel, AdapterSettingValueModel, AdapterTemplateModel, DeployMode} from '../../models/catalog.model'; +import { + AdapterSettingModel, + AdapterSettingValueModel, + AdapterTemplateModel, + DeployMode +} from '../../models/catalog.model'; import {LeftSidebarService} from '../../components/left-sidebar/left-sidebar.service'; @Component({ - selector: 'app-adapters', - templateUrl: './adapters.component.html', - styleUrls: ['./adapters.component.scss'] + selector: 'app-adapters', + templateUrl: './adapters.component.html', + styleUrls: ['./adapters.component.scss'] }) export class AdaptersComponent implements OnInit, OnDestroy { - private readonly _crud = inject(CrudService); - private readonly _route = inject(ActivatedRoute); - private readonly _router = inject(Router); - private readonly _toast = inject(ToasterService); - private readonly _fb = inject(UntypedFormBuilder); - private readonly _catalog = inject(CatalogService); - private readonly _left = inject(LeftSidebarService); - - constructor(private injector: Injector) { - this.availableAdapters = computed(() => { - console.log(this.currentRoute()); - const route = this.currentRoute(); - return this._catalog.getAdapterTemplates().filter(a => a.adapterType === this.getMatchingAdapterType()); - }); + private readonly _crud = inject(CrudService); + private readonly _route = inject(ActivatedRoute); + private readonly _router = inject(Router); + private readonly _toast = inject(ToasterService); + private readonly _fb = inject(UntypedFormBuilder); + private readonly _catalog = inject(CatalogService); + private readonly _left = inject(LeftSidebarService); + + constructor(private injector: Injector) { + this.availableAdapters = computed(() => { + console.log(this.currentRoute()); + const route = this.currentRoute(); + return this._catalog.getAdapterTemplates().filter(a => a.adapterType === this.getMatchingAdapterType()); + }); + + this.stores = computed(() => { + this._catalog.listener(); + return this._catalog.getStores(); + }); + + this.sources = computed(() => { + this._catalog.listener(); + return this._catalog.getSources(); + }); + } - this.stores = computed(() => { - this._catalog.listener(); - return this._catalog.getStores(); - }); + readonly stores: Signal; + readonly sources: Signal; + readonly availableAdapters: Signal; + readonly currentRoute: WritableSignal = signal(null); + private subscriptions = new Subscription(); - this.sources = computed(() => { - this._catalog.listener(); - return this._catalog.getSources(); - }); - } + readonly adapter: WritableSignal = signal(null); + editingAdapterForm: FormGroup; + deletingAdapter; + deletingInProgress: AdapterModel[]; - readonly stores: Signal; - readonly sources: Signal; - readonly availableAdapters: Signal; - readonly currentRoute: WritableSignal = signal(null); - private subscriptions = new Subscription(); + editingAvailableAdapterForm: UntypedFormGroup; + readonly activeMode: WritableSignal = signal(null); + settingHeaders: string[]; - readonly adapter: WritableSignal = signal(null); - editingAdapterForm: FormGroup; - deletingAdapter; - deletingInProgress: AdapterModel[]; + fileLabel = 'Choose File'; + deploying = false; + handshaking = false; - editingAvailableAdapterForm: UntypedFormGroup; - readonly activeMode: WritableSignal = signal(null); - settingHeaders: string[]; + subgroups = new Map(); - fileLabel = 'Choose File'; - deploying = false; - handshaking = false; + public accessId: String; + private data: { data: FormData; deploy: any }; - subgroups = new Map(); + modalActive = false; - public accessId: String; - private data: { data: FormData; deploy: any }; + protected readonly Array = Array; - modalActive = false; + protected readonly DeployMode = DeployMode; - protected readonly Array = Array; + readonly _name: string = 'uniqueName'; - protected readonly DeployMode = DeployMode; + protected readonly fetch = fetch; - readonly _name: string = 'uniqueName'; + protected readonly AdapterModel = AdapterModel; - protected readonly fetch = fetch; + protected readonly Task = Task; - protected readonly AdapterModel = AdapterModel; - protected readonly Task = Task; + readonly positionOrder = () => { + return (a, b) => { + return a.position - b.position; + }; + }; - readonly positionOrder = () => { - return (a, b) => { - return a.position - b.position; - }; - } + ngOnInit() { + this._left.close(); + this.deletingInProgress = []; + this.subscribeActiveChange(); + this.currentRoute.set(this._route.snapshot.paramMap.get('action')); - ngOnInit() { - this._left.close(); - this.deletingInProgress = []; - this.subscribeActiveChange(); + const sub = this._route.params.subscribe(params => { + this.currentRoute.set(params['action']); + }); + this.subscriptions.add(sub); + } - this.currentRoute.set(this._route.snapshot.paramMap.get('action')); + ngOnDestroy() { + this.subscriptions.unsubscribe(); + } - const sub = this._route.params.subscribe(params => { - this.currentRoute.set(params['action']); - }); - this.subscriptions.add(sub); - } + subscribeActiveChange() { - ngOnDestroy() { - this.subscriptions.unsubscribe(); - } + effect(() => { + const mode = this.activeMode(); + const adapter = this.adapter(); + if (!mode || !adapter) { + return; + } + const fc = {}; + + for (const setting of adapter.settings.values()) { + const validators = []; + if (setting.template.required) { + validators.push(Validators.required); + } + let val = setting.template.defaultValue; + if (setting.template.fileNames) { + fc[setting.template.name] = this._fb.array([]); + } else { + if (setting.template.options) { + val = setting.template.options[0]; + } else if (setting.template.fileNames) { + val = new UntypedFormControl(val, validators).value; + } + fc[setting.template.name] = new UntypedFormControl(val, validators); + } + } - subscribeActiveChange() { + if (adapter.task === Task.DEPLOY) { + fc['uniqueName'] = new UntypedFormControl(this.getDefaultUniqueName(), [Validators.required, Validators.pattern(this._crud.getAdapterNameValidationRegex()), validateUniqueName([...this.stores(), ...this.sources()])]); + this.editingAvailableAdapterForm = new UntypedFormGroup(fc); + this.editingAvailableAdapterForm.controls['mode'].setValue(this.activeMode().toLowerCase()); + } else { + fc['uniqueName'] = new UntypedFormControl(adapter.uniqueName, [Validators.required, Validators.pattern(this._crud.getAdapterNameValidationRegex()), validateUniqueName([...this.stores(), ...this.sources()].filter(a => a.name !== adapter.uniqueName))]); + this.editingAdapterForm = new UntypedFormGroup(fc); + } - effect(() => { - const mode = this.activeMode(); - const adapter = this.adapter(); - if (!mode || !adapter) { - return; - } - const fc = {}; + }, {injector: this.injector}); + } - for (const setting of adapter.settings.values()) { - const validators = []; - if (setting.template.required) { - validators.push(Validators.required); + private getMatchingAdapterType() { + if (this.currentRoute() === 'addStore') { + return AdapterType.STORE; + } else if (this.currentRoute() === 'addSource') { + return AdapterType.SOURCE; } - let val = setting.template.defaultValue; - if (setting.template.fileNames) { - fc[setting.template.name] = this._fb.array([]); - } else { - if (setting.template.options) { - val = setting.template.options[0]; - } else if (setting.template.fileNames) { - val = new UntypedFormControl(val, validators).value; - } - fc[setting.template.name] = new UntypedFormControl(val, validators); - } - } - - if (adapter.task === Task.DEPLOY) { - fc['uniqueName'] = new UntypedFormControl(this.getDefaultUniqueName(), [Validators.required, Validators.pattern(this._crud.getAdapterNameValidationRegex()), validateUniqueName([...this.stores(), ...this.sources()])]); - this.editingAvailableAdapterForm = new UntypedFormGroup(fc); - this.editingAvailableAdapterForm.controls['mode'].setValue(this.activeMode().toLowerCase()); - } else { - fc['uniqueName'] = new UntypedFormControl(adapter.uniqueName, [Validators.required, Validators.pattern(this._crud.getAdapterNameValidationRegex()), validateUniqueName([...this.stores(), ...this.sources()].filter(a => a.name !== adapter.uniqueName))]); - this.editingAdapterForm = new UntypedFormGroup(fc); - } - - }, {injector: this.injector}); - } - - private getMatchingAdapterType() { - if (this.currentRoute() === 'addStore') { - return AdapterType.STORE; - } else if (this.currentRoute() === 'addSource') { - return AdapterType.SOURCE; + return null; } - return null; - } - onVisibilityChange(status: boolean) { + onVisibilityChange(status: boolean) { - if (this.modalActive) { - if (!status) { - this.modalActive = false; - } + if (this.modalActive) { + if (!status) { + this.modalActive = false; + } - return; + return; + } + this.adapter.set(null); + this.editingAdapterForm = null; + this.editingAvailableAdapterForm = null; + this.activeMode.set(null); + this.settingHeaders = null; + this.fileLabel = 'Choose File'; } - this.adapter.set(null); - this.editingAdapterForm = null; - this.editingAvailableAdapterForm = null; - this.activeMode.set(null); - this.settingHeaders = null; - this.fileLabel = 'Choose File'; - } - - initAdapterSettingsConfigureModal(adapter: AdapterModel) { - const allSettings = this._catalog.getAdapterTemplate(adapter.adapterName, adapter.type); - - const current = Adapter.from(allSettings, adapter, Task.CHANGE); - this.adapter.set(current); - this.activeMode.set(current.modes[0]); - - this.handshaking = false; - this.modalActive = true; - } - - saveAdapterSettings() { - const adapter = this.adapter; - adapter.settings = {}; - for (const [k, v] of Object.entries(this.editingAdapterForm.controls)) { - const setting = this.getAdapterSetting(k); - if (!setting[0].modifiable || setting[0].fileNames) { - continue; - } - adapter.settings[k] = v.value; + + initAdapterSettingsConfigureModal(adapter: AdapterModel) { + const allSettings = this._catalog.getAdapterTemplate(adapter.adapterName, adapter.type); + + const current = Adapter.from(allSettings, adapter, Task.CHANGE); + this.adapter.set(current); + this.activeMode.set(current.modes[0]); + + this.handshaking = false; + this.modalActive = true; } - this._crud.updateAdapterSettings(adapter).subscribe({ - next: res => { - const result = res; - if (result.error) { - this._toast.exception(result); - } else { - this._toast.success('Updated adapter settings'); - } - this.modalActive = false; - // this._catalog.updateIfNecessary(); - }, - error: err => { - this._toast.error('Could not update adapter settings'); - console.log(err); - } - }); - } - - getDefaultUniqueName(): string { - if (this.adapter !== undefined) { - const base = this.adapter().adapterName.toLowerCase(); // + "_"; // TODO: re-enable underscores when graph namespaces work with it - let max_i = 0; - for (const store of this.stores()) { - if (store.name.startsWith(base)) { - const suffix = store.name.slice(base.length); - const i = parseInt(suffix, 10); - if (!isNaN(i)) { - max_i = Math.max(max_i, i); - } - } - } - for (const store of this.sources()) { - if (store.name.startsWith(base)) { - const suffix = store.name.slice(base.length); - const i = parseInt(suffix, 10); - if (!isNaN(i)) { - max_i = Math.max(max_i, i); - } + + saveAdapterSettings() { + const adapter = this.adapter; + adapter.settings = {}; + for (const [k, v] of Object.entries(this.editingAdapterForm.controls)) { + const setting = this.getAdapterSetting(k); + if (!setting[0].modifiable || setting[0].fileNames) { + continue; + } + adapter.settings[k] = v.value; } - } - return base + (max_i + 1).toString(10); - } - return null; - } - - async initDeployModal(adapter: AdapterTemplateModel) { - this.activeMode.set(null); - // if we only have one mode we directly set it - if (adapter.modes.length === 0) { - this.activeMode.set(DeployMode.ALL); - } else if (adapter.modes.length === 1) { - this.activeMode.set(adapter.modes[0]); + this._crud.updateAdapterSettings(adapter).subscribe({ + next: res => { + const result = res; + if (result.error) { + this._toast.exception(result); + } else { + this._toast.success('Updated adapter settings'); + } + this.modalActive = false; + // this._catalog.updateIfNecessary(); + }, + error: err => { + this._toast.error('Could not update adapter settings'); + console.log(err); + } + }); } - this.adapter.set(Adapter.from(adapter, null, Task.DEPLOY)); - - this.modalActive = true; - } - - onFileChange(event, form: UntypedFormGroup, key) { - const files = event.target.files; - if (files) { - const fileNames = []; - const arr = form.controls[key] as UntypedFormArray; - arr.clear(); - for (let i = 0; i < files.length; i++) { - fileNames.push(files.item(i)._name); - arr.push(this._fb.control(files.item(i))); - } - this.fileLabel = fileNames.join(', '); - } else { - const arr = form.controls[key] as UntypedFormArray; - arr.clear(); - this.fileLabel = 'Choose File'; + getDefaultUniqueName(): string { + if (this.adapter !== undefined) { + const base = this.adapter().adapterName.toLowerCase(); // + "_"; // TODO: re-enable underscores when graph namespaces work with it + let max_i = 0; + for (const store of this.stores()) { + if (store.name.startsWith(base)) { + const suffix = store.name.slice(base.length); + const i = parseInt(suffix, 10); + if (!isNaN(i)) { + max_i = Math.max(max_i, i); + } + } + } + for (const store of this.sources()) { + if (store.name.startsWith(base)) { + const suffix = store.name.slice(base.length); + const i = parseInt(suffix, 10); + if (!isNaN(i)) { + max_i = Math.max(max_i, i); + } + } + } + return base + (max_i + 1).toString(10); + } + return null; } - } - - getFeedback(form: UntypedFormGroup) { - const errors = form.controls['adapterName']?.errors; - if (errors) { - if (errors.required) { - return 'missing unique name'; - } else if (errors.pattern) { - return 'invalid unique name: unique name must only contain lower case letters, digits and underscores'; - } else if (errors.unique) { - return 'name is not unique'; - } + + async initDeployModal(adapter: AdapterTemplateModel) { + this.activeMode.set(null); + // if we only have one mode we directly set it + if (adapter.modes.length === 0) { + this.activeMode.set(DeployMode.ALL); + } else if (adapter.modes.length === 1) { + this.activeMode.set(adapter.modes[0]); + } + + this.adapter.set(Adapter.from(adapter, null, Task.DEPLOY)); + + this.modalActive = true; } - return ''; - } - - getGenericFeedback(key: string | any) { - let errors = this.editingAvailableAdapterForm.errors; - if (errors) { - if (errors.usedPort) { - return errors.usedPort; - } else if (errors.notNumber) { - return errors.notNumber; - } else if (errors.noDockerRunning) { - return errors.noDockerRunning; - } + + onFileChange(event, form: UntypedFormGroup, key) { + const files = event.target.files; + if (files) { + const fileNames = []; + const arr = form.controls[key] as UntypedFormArray; + arr.clear(); + for (let i = 0; i < files.length; i++) { + fileNames.push(files.item(i)._name); + arr.push(this._fb.control(files.item(i))); + } + this.fileLabel = fileNames.join(', '); + } else { + const arr = form.controls[key] as UntypedFormArray; + arr.clear(); + this.fileLabel = 'Choose File'; + } } - errors = this.editingAvailableAdapterForm.controls[key].errors; - if (errors) { - if (errors.required) { - return 'required'; - } else if (errors.pattern) { - return 'is not correctly formatted'; - } else if (errors.unique) { - return 'name is not unique'; - } + + getFeedback(form: UntypedFormGroup) { + const errors = form.controls['adapterName']?.errors; + if (errors) { + if (errors.required) { + return 'missing unique name'; + } else if (errors.pattern) { + return 'invalid unique name: unique name must only contain lower case letters, digits and underscores'; + } else if (errors.unique) { + return 'name is not unique'; + } + } + return ''; } - return ''; - } + getGenericFeedback(key: string | any) { + let errors = this.editingAvailableAdapterForm.errors; + if (errors) { + if (errors.usedPort) { + return errors.usedPort; + } else if (errors.notNumber) { + return errors.notNumber; + } else if (errors.noDockerRunning) { + return errors.noDockerRunning; + } + } + errors = this.editingAvailableAdapterForm.controls[key].errors; + if (errors) { + if (errors.required) { + return 'required'; + } else if (errors.pattern) { + return 'is not correctly formatted'; + } else if (errors.unique) { + return 'name is not unique'; + } + } - getAdapterSetting(key: string | unknown): MergedSetting { - return this.adapter().settings.get(key); - } + return ''; + } - deploy() { - if (!this.editingAvailableAdapterForm.valid) { - return; + getAdapterSetting(key: string | unknown): MergedSetting { + return this.adapter().settings.get(key); } - const deploy = new AdapterModel(this.editingAvailableAdapterForm.controls['uniqueName'].value, this.adapter().adapterName, new PolyMap(), this.adapter().persistent, this.adapter().type, this.activeMode()); - const fd: FormData = new FormData(); - - for (const [k, v] of Object.entries(this.editingAvailableAdapterForm.controls)) { - const setting = this.getAdapterSetting(k); - if (!setting) { - continue; - } - - if (!setting.current) { - setting.current = new AdapterSettingValueModel(k, null); - } - - if (setting.template.fileNames) { - const fileNames = []; - const arr = v as UntypedFormArray; - for (let i = 0; i < arr.length; i++) { - const file = arr.at(i).value as File; - fd.append(file.name, file); - fileNames.push(file.name); + + deploy() { + if (!this.editingAvailableAdapterForm.valid) { + return; } - setting.current.value = fileNames.toString(); - } else { - setting.current.value = v.value; - } + const deploy = new AdapterModel(this.editingAvailableAdapterForm.controls['uniqueName'].value, this.adapter().adapterName, new PolyMap(), this.adapter().persistent, this.adapter().type, this.activeMode()); + const fd: FormData = new FormData(); - deploy.settings.set(k, setting.current); - } - console.log(deploy); - - if (deploy.settings.hasOwnProperty('method') && deploy.settings['method'].defaultValue === 'link') { - // secure deploy - this.handshaking = true; - this._crud.pathAccess(new PathAccessRequest(deploy.name, deploy.settings['directoryName'].defaultValue)).subscribe( - res => { - const id = res; - this.accessId = id; - deploy.settings['access'] = id; - this.data = {data: fd, deploy: deploy}; - if (!id || id.trim() === '') { - // file is already placed - this.continueSecureDeploy(); + for (const [k, v] of Object.entries(this.editingAvailableAdapterForm.controls)) { + const setting = this.getAdapterSetting(k); + if (!setting) { + continue; } - } - ); - } else { - // normal deploy - this.startDeploying(deploy); - } + if (!setting.current) { + setting.current = new AdapterSettingValueModel(k, null); + } - } - - continueSecureDeploy() { - this.handshaking = false; - this.startDeploying(this.data.deploy); - } - - createSecureFile() { - const file = new Blob(['test'], {type: '.access'}); - const a = document.createElement('a'), - url = URL.createObjectURL(file); - a.href = url; - a.download = 'polypheny.access'; - document.body.appendChild(a); - a.click(); - setTimeout(function () { - document.body.removeChild(a); - window.URL.revokeObjectURL(url); - }, 0); - } - - private startDeploying(deploy: AdapterModel) { - - this.deploying = true; - this._crud.createAdapter(deploy).subscribe({ - next: (result: RelationalResult) => { - if (!result.error) { - this._toast.success('Deployed "' + deploy.name + '"', result.query); - this._router.navigate(['./../'], {relativeTo: this._route}).then(r => null); - } else { - this._toast.exception(result, 'Could not deploy adapter'); + if (setting.template.fileNames) { + const fileNames = []; + const arr = v as UntypedFormArray; + for (let i = 0; i < arr.length; i++) { + const file = arr.at(i).value as File; + fd.append(file.name, file); + fileNames.push(file.name); + } + setting.current.value = fileNames.toString(); + } else { + setting.current.value = v.value; + } + + deploy.settings.set(k, setting.current); } - this.modalActive = false; - }, - error: _err => { - this._toast.error('Could not deploy adapter'); - } - }).add(() => this.deploying = false); - } - - removeAdapter(adapter: AdapterModel) { - if (this.deletingAdapter !== adapter) { - this.deletingAdapter = adapter; - } else { - if (this.deletingInProgress.includes(adapter)) { - return; - } - - this.deletingInProgress.push(adapter); - this._crud.removeAdapter(adapter.name).subscribe({ - next: (result: RelationalResult) => { - if (!result.error) { - this._toast.success('Dropped "' + adapter.name + '"', result.query); - } else { - this._toast.exception(result); - } - this.deletingInProgress = this.deletingInProgress.filter(el => el !== adapter); - this.deletingAdapter = undefined; - // this._catalog.updateIfNecessary(); - }, error: err => { - this._toast.error('Could not remove adapter', 'server error'); - console.log(err); - this.deletingInProgress = this.deletingInProgress.filter(el => el !== adapter); - this.deletingAdapter = undefined; + console.log(deploy); + + if (deploy.settings.hasOwnProperty('method') && deploy.settings['method'].defaultValue === 'link') { + // secure deploy + this.handshaking = true; + this._crud.pathAccess(new PathAccessRequest(deploy.name, deploy.settings['directoryName'].defaultValue)).subscribe( + res => { + const id = res; + this.accessId = id; + deploy.settings['access'] = id; + this.data = {data: fd, deploy: deploy}; + if (!id || id.trim() === '') { + // file is already placed + this.continueSecureDeploy(); + } + } + ); + + } else { + // normal deploy + this.startDeploying(deploy); } - }); - } - } - validate(form: AbstractControl | unknown, key) { - if (form === undefined) { - return; } - if (form instanceof UntypedFormControl) { - return this.validateControl(form, key); + continueSecureDeploy() { + this.handshaking = false; + this.startDeploying(this.data.deploy); } - if (!(form instanceof UntypedFormGroup)) { - return; + + createSecureFile() { + const file = new Blob(['test'], {type: '.access'}); + const a = document.createElement('a'), + url = URL.createObjectURL(file); + a.href = url; + a.download = 'polypheny.access'; + document.body.appendChild(a); + a.click(); + setTimeout(function () { + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }, 0); } - if (form.controls[key].status === 'DISABLED') { - return; + + private startDeploying(deploy: AdapterModel) { + + this.deploying = true; + this._crud.createAdapter(deploy).subscribe({ + next: (result: RelationalResult) => { + if (!result.error) { + this._toast.success('Deployed "' + deploy.name + '"', result.query); + this._router.navigate(['./../'], {relativeTo: this._route}).then(r => null); + } else { + this._toast.exception(result, 'Could not deploy adapter'); + } + this.modalActive = false; + }, + error: _err => { + this._toast.error('Could not deploy adapter'); + } + }).add(() => this.deploying = false); } + removeAdapter(adapter: AdapterModel) { + if (this.deletingAdapter !== adapter) { + this.deletingAdapter = adapter; + } else { + if (this.deletingInProgress.includes(adapter)) { + return; + } - if (form.controls[key].valid) { - return 'is-valid'; - } else { - return 'is-invalid'; - } - } - - getLogo(adapterName: string) { - const path = 'assets/dbms-logos/'; - switch (adapterName.toLowerCase()) { - case 'csv': - return path + 'csv.png'; - case 'hsqldb': - return path + 'hsqldb.png'; - case 'postgresql': - return path + 'postgres.svg'; - case 'monetdb': - return path + 'monetdb.png'; - case 'cassandra': - return path + 'cassandra.png'; - case 'cottontail': - case 'cottontail-db': - return path + 'cottontaildb.png'; - case 'file': - return 'fa fa-file-image-o'; - case 'mysql': - return path + 'mysql.png'; - case 'qfs': - return 'fa fa-folder-open-o'; - case 'mongodb': - return path + 'mongodb.png'; - case 'ethereum': - return path + 'ethereum.png'; - case 'neo4j': - return path + 'neo4j.png'; - case 'excel': - return path + 'xls.png'; - case 'googlesheets': - return path + 'google.png'; - default: - return 'fa fa-database'; + this.deletingInProgress.push(adapter); + this._crud.removeAdapter(adapter.name).subscribe({ + next: (result: RelationalResult) => { + if (!result.error) { + this._toast.success('Dropped "' + adapter.name + '"', result.query); + } else { + this._toast.exception(result); + } + this.deletingInProgress = this.deletingInProgress.filter(el => el !== adapter); + this.deletingAdapter = undefined; + // this._catalog.updateIfNecessary(); + }, error: err => { + this._toast.error('Could not remove adapter', 'server error'); + console.log(err); + this.deletingInProgress = this.deletingInProgress.filter(el => el !== adapter); + this.deletingAdapter = undefined; + } + }); + } } - } - - private validateControl(form: UntypedFormControl, key: string) { - if ((key === 'port' || key === 'instanceId') && this.activeMode() === DeployMode.DOCKER) { - if (this.editingAvailableAdapterForm.valid) { - return 'is-valid'; - } else { - return 'is-invalid'; - } + + validate(form: AbstractControl | unknown, key) { + if (form === undefined) { + return; + } + + if (form instanceof UntypedFormControl) { + return this.validateControl(form, key); + } + if (!(form instanceof UntypedFormGroup)) { + return; + } + if (form.controls[key].status === 'DISABLED') { + return; + } + + + if (form.controls[key].valid) { + return 'is-valid'; + } else { + return 'is-invalid'; + } } - if (form.valid) { - return 'is-valid'; - } else { - return 'is-invalid'; + getLogo(adapterName: string) { + const path = 'assets/dbms-logos/'; + switch (adapterName.toLowerCase()) { + case 'csv': + return path + 'csv.png'; + case 'hsqldb': + return path + 'hsqldb.png'; + case 'postgresql': + return path + 'postgres.svg'; + case 'monetdb': + return path + 'monetdb.png'; + case 'cassandra': + return path + 'cassandra.png'; + case 'cottontail': + case 'cottontail-db': + return path + 'cottontaildb.png'; + case 'file': + return 'fa fa-file-image-o'; + case 'mysql': + return path + 'mysql.png'; + case 'qfs': + return 'fa fa-folder-open-o'; + case 'mongodb': + return path + 'mongodb.png'; + case 'ethereum': + return path + 'ethereum.png'; + case 'neo4j': + return path + 'neo4j.png'; + case 'excel': + return path + 'xls.png'; + case 'googlesheets': + return path + 'google.png'; + default: + return 'fa fa-database'; + } } - } - resetDeletingAdapter(adapter: AdapterModel) { - if (this.deletingAdapter === adapter && this.deletingInProgress.includes(adapter)) { - return; + private validateControl(form: UntypedFormControl, key: string) { + if ((key === 'port' || key === 'instanceId') && this.activeMode() === DeployMode.DOCKER) { + if (this.editingAvailableAdapterForm.valid) { + return 'is-valid'; + } else { + return 'is-invalid'; + } + } + + if (form.valid) { + return 'is-valid'; + } else { + return 'is-invalid'; + } } - this.deletingAdapter = undefined; - } - isDeleting(adapter: AdapterModel) { - return this.deletingInProgress.includes(adapter); - } + resetDeletingAdapter(adapter: AdapterModel) { + if (this.deletingAdapter === adapter && this.deletingInProgress.includes(adapter)) { + return; + } + this.deletingAdapter = undefined; + } - subIsActive(subOf: string) { - if (!subOf) { - return true; + isDeleting(adapter: AdapterModel) { + return this.deletingInProgress.includes(adapter); } - const keys = subOf.split('_'); - if (!this.subgroups.has(keys[0])) { + subIsActive(subOf: string) { + if (!subOf) { + return true; + } + const keys = subOf.split('_'); - const setting = this.getAdapterSetting(keys[0]); + if (!this.subgroups.has(keys[0])) { - this.subgroups.set(keys[0], setting[1].value); + const setting = this.getAdapterSetting(keys[0]); - } + this.subgroups.set(keys[0], setting[1].value); + } - return this.subgroups.has(keys[0]) && this.subgroups.get(keys[0]) === keys[1]; - } - onChange(key: string | unknown, value: AbstractControl | unknown) { - if (key == null || value == null) { - return; + return this.subgroups.has(keys[0]) && this.subgroups.get(keys[0]) === keys[1]; } - this.subgroups.set(key, (value).value); - } - - setMode(mode: DeployMode) { - this.activeMode.set(mode); - } - isSettingDisplayed(key: string) { - const setting = this.getAdapterSetting(key); + onChange(key: string | unknown, value: AbstractControl | unknown) { + if (key == null || value == null) { + return; + } + this.subgroups.set(key, (value).value); + } - if (setting.template.subOf && !this.subIsActive(setting.template.subOf)) { - // parent is inactive - return false; + setMode(mode: DeployMode) { + this.activeMode.set(mode); } - if (this.activeMode()) { - if (key === 'mode') { - return false; - } else if (setting.template.appliesTo.includes(DeployMode.ALL)) { - return true; - } else if (setting.template.appliesTo.includes(this.activeMode())) { + isSettingDisplayed(key: string) { + const setting = this.getAdapterSetting(key); + + if (setting.template.subOf && !this.subIsActive(setting.template.subOf)) { + // parent is inactive + return false; + } + + if (this.activeMode()) { + if (key === 'mode') { + return false; + } else if (setting.template.appliesTo.includes(DeployMode.ALL)) { + return true; + } else if (setting.template.appliesTo.includes(this.activeMode())) { + return true; + } + return false; + } return true; - } - return false; } - return true; - } } // see https://angular.io/guide/form-validation#custom-validators function validateUniqueName(adapters: AdapterModel[]): ValidatorFn { - return (control: AbstractControl): { [key: string]: any } | null => { - if (!control.value) { - return null; - } - for (const s of adapters) { - if (s.name === control.value) { - return {unique: true}; - } - } - return null; - }; + return (control: AbstractControl): { [key: string]: any } | null => { + if (!control.value) { + return null; + } + for (const s of adapters) { + if (s.name === control.value) { + return {unique: true}; + } + } + return null; + }; } class Adapter { - uniqueName: string; - adapterName: string; - persistent: boolean; - modes: DeployMode[]; - mode: DeployMode; - task: Task; - type: AdapterType; - - constructor(uniqueName: string, adapterName: string, persistent: boolean, modes: DeployMode[], type: AdapterType, settings: Map, task: Task) { - this.uniqueName = uniqueName; - this.settings = settings; - this.adapterName = adapterName; - this.persistent = persistent; - this.modes = modes; - this.task = task; - this.type = type; - } - - - settings: Map; - - public static from(adapter: AdapterTemplateModel, current: AdapterModel | null, task: Task): Adapter { - const settings: Map = new Map(); - - for (const template of adapter.settings) { - const temp = current === null ? null : current.settings[template.name]; - const val = new MergedSetting(template, new AdapterSettingValueModel(template.name, template.defaultValue)); - val.current = temp; - - settings.set(template.name, val); + uniqueName: string; + adapterName: string; + persistent: boolean; + modes: DeployMode[]; + mode: DeployMode; + task: Task; + type: AdapterType; + + constructor(uniqueName: string, adapterName: string, persistent: boolean, modes: DeployMode[], type: AdapterType, settings: Map, task: Task) { + this.uniqueName = uniqueName; + this.settings = settings; + this.adapterName = adapterName; + this.persistent = persistent; + this.modes = modes; + this.task = task; + this.type = type; + } + + + settings: Map; + + public static from(adapter: AdapterTemplateModel, current: AdapterModel | null, task: Task): Adapter { + const settings: Map = new Map(); + + for (const template of adapter.settings) { + const temp = current === null ? null : current.settings[template.name]; + const val = new MergedSetting(template, new AdapterSettingValueModel(template.name, template.defaultValue)); + val.current = temp; + + settings.set(template.name, val); + } + return new Adapter(current === null ? '' : current.name, adapter.adapterName, adapter.persistent, adapter.modes, adapter.adapterType, settings, task); } - return new Adapter(current === null ? '' : current.name, adapter.adapterName, adapter.persistent, adapter.modes, adapter.adapterType, settings, task); - } } class MergedSetting { - template: AdapterSettingModel; - current: AdapterSettingValueModel; + template: AdapterSettingModel; + current: AdapterSettingValueModel; - constructor(template: AdapterSettingModel, current: AdapterSettingValueModel) { - this.template = template; - this.current = current; - } + constructor(template: AdapterSettingModel, current: AdapterSettingValueModel) { + this.template = template; + this.current = current; + } } enum Task { - DEPLOY = 'DEPLOY', - CHANGE = 'CHANGE' + DEPLOY = 'DEPLOY', + CHANGE = 'CHANGE' } diff --git a/src/app/views/dashboard/dashboard.component.ts b/src/app/views/dashboard/dashboard.component.ts index e4f3f100..fb4143b1 100644 --- a/src/app/views/dashboard/dashboard.component.ts +++ b/src/app/views/dashboard/dashboard.component.ts @@ -6,156 +6,156 @@ import {CatalogService} from '../../services/catalog.service'; @Component({ - selector: 'app-dashboard', - templateUrl: './dashboard.component.html', - styleUrls: ['./dashboard.component.scss'] + selector: 'app-dashboard', + templateUrl: './dashboard.component.html', + styleUrls: ['./dashboard.component.scss'] }) export class DashboardComponent implements OnInit, OnDestroy { - public readonly _crud = inject(CrudService); - public readonly _catalog = inject(CatalogService); - - dataWorkload = []; - dataDql = []; - labels = []; - colorList = []; - line = 'line'; - min = 0; - max = 0; - diagram = []; - - dashboardSet: DashboardSet; - dashboardInformation: DashboardData; - xLabel: string; - yLabel: string; - digramInterval: number; - informationInterval: number; - infoCounter: number; - diagramCounter: number; - selectIntervalDisplay = 'All'; - - constructor() { - } - - ngOnInit() { - this.infoCounter = 0; - this.diagramCounter = 0; - - this.getDiagram('all'); - this.getDashboardInformation(); - this.checkIfInformationAvailable(); - - } - - ngOnDestroy() { - clearInterval(this.digramInterval); - clearInterval(this.informationInterval); - } - - - private checkIfInformationAvailable() { - if (this.dashboardInformation == null) { - this.digramInterval = setInterval(this.getDiagram.bind(this), 1000); + public readonly _crud = inject(CrudService); + public readonly _catalog = inject(CatalogService); + + dataWorkload = []; + dataDql = []; + labels = []; + colorList = []; + line = 'line'; + min = 0; + max = 0; + diagram = []; + + dashboardSet: DashboardSet; + dashboardInformation: DashboardData; + xLabel: string; + yLabel: string; + digramInterval: number; + informationInterval: number; + infoCounter: number; + diagramCounter: number; + selectIntervalDisplay = 'All'; + + constructor() { } - if (this.dashboardSet == null) { - this.informationInterval = setInterval(this.getDashboardInformation.bind(this), 1000); + + ngOnInit() { + this.infoCounter = 0; + this.diagramCounter = 0; + + this.getDiagram('all'); + this.getDashboardInformation(); + this.checkIfInformationAvailable(); + } - } - - - getDiagram(interval: string) { - this.dataWorkload = []; - this.dataDql = []; - this.labels = []; - this.min = 0; - this.max = 0; - this._crud.getDashboardDiagram(new MonitoringRequest(interval)).subscribe( - res => { - console.log(res); - this.dashboardInformation = res; - - if (this.dashboardInformation != null || this.diagramCounter > 120) { - clearInterval(this.digramInterval); - Object.entries(this.dashboardInformation).forEach( - ([key, value]) => { - const left = +Object.keys(value)[0]; - const right = +Object.values(value)[0]; - - this.labels.push(key); - - this.dataWorkload.push(right); - this.dataDql.push(left); - - //find min and max between Workload and Query Information - if (this.min > right) { - this.min = right; - } - if (this.max < right) { - this.max = right; - } - if (this.min > left) { - this.min = left; - } - if (this.max < left) { - this.max = left; - } - } - ); - } - this.diagramCounter++; + + ngOnDestroy() { + clearInterval(this.digramInterval); + clearInterval(this.informationInterval); + } + + + private checkIfInformationAvailable() { + if (this.dashboardInformation == null) { + this.digramInterval = setInterval(this.getDiagram.bind(this), 1000); } - ); - - this.diagram = [{ - label: 'DML', - borderColor: 'rgb(255, 99, 132)', - data: this.dataWorkload, - }, - { - label: 'DQL', - borderColor: 'rgb(18,105,199)', - data: this.dataDql - }]; - - this.xLabel = 'Time'; - this.yLabel = 'Number of Statements'; - - } - - getDashboardInformation() { - this._crud.getDashboardInformation(new StatisticRequest()).subscribe( - res => { - this.dashboardSet = res; - console.log(this.dashboardSet); - if (this.dashboardSet != null || this.infoCounter > 120) { - clearInterval(this.informationInterval); - } - this.infoCounter++; + if (this.dashboardSet == null) { + this.informationInterval = setInterval(this.getDashboardInformation.bind(this), 1000); } - ); - } + } + + + getDiagram(interval: string) { + this.dataWorkload = []; + this.dataDql = []; + this.labels = []; + this.min = 0; + this.max = 0; + this._crud.getDashboardDiagram(new MonitoringRequest(interval)).subscribe( + res => { + console.log(res); + this.dashboardInformation = res; + + if (this.dashboardInformation != null || this.diagramCounter > 120) { + clearInterval(this.digramInterval); + Object.entries(this.dashboardInformation).forEach( + ([key, value]) => { + const left = +Object.keys(value)[0]; + const right = +Object.values(value)[0]; + + this.labels.push(key); + + this.dataWorkload.push(right); + this.dataDql.push(left); + + //find min and max between Workload and Query Information + if (this.min > right) { + this.min = right; + } + if (this.max < right) { + this.max = right; + } + if (this.min > left) { + this.min = left; + } + if (this.max < left) { + this.max = left; + } + } + ); + } + this.diagramCounter++; + } + ); + + this.diagram = [{ + label: 'DML', + borderColor: 'rgb(255, 99, 132)', + data: this.dataWorkload, + }, + { + label: 'DQL', + borderColor: 'rgb(18,105,199)', + data: this.dataDql + }]; + + this.xLabel = 'Time'; + this.yLabel = 'Number of Statements'; - public setSelectInterval(interval: string) { - if (interval === 'all') { - this.selectIntervalDisplay = 'All'; } - const numberInterval = Number(interval); - if (isNaN(numberInterval)) { - this.selectIntervalDisplay = 'All'; - } else { - this.selectIntervalDisplay = this.getIntervalString(numberInterval); + getDashboardInformation() { + this._crud.getDashboardInformation(new StatisticRequest()).subscribe( + res => { + this.dashboardSet = res; + console.log(this.dashboardSet); + if (this.dashboardSet != null || this.infoCounter > 120) { + clearInterval(this.informationInterval); + } + this.infoCounter++; + } + ); } - this.getDiagram(interval); - } + public setSelectInterval(interval: string) { + if (interval === 'all') { + this.selectIntervalDisplay = 'All'; + } + const numberInterval = Number(interval); - private getIntervalString(numberInterval: number): string { - const hours = Math.floor(numberInterval / 60); + if (isNaN(numberInterval)) { + this.selectIntervalDisplay = 'All'; + } else { + this.selectIntervalDisplay = this.getIntervalString(numberInterval); + } - const minutes = numberInterval % 60; + this.getDiagram(interval); + } + + private getIntervalString(numberInterval: number): string { + const hours = Math.floor(numberInterval / 60); - return (hours > 0 ? ('' + hours + (hours === 1 ? ' hour' : ' hours')) : '') + (minutes > 0 ? ('' + minutes + (minutes === 1 ? ' minute' : ' minutes')) : ''); - } + const minutes = numberInterval % 60; + + return (hours > 0 ? ('' + hours + (hours === 1 ? ' hour' : ' hours')) : '') + (minutes > 0 ? ('' + minutes + (minutes === 1 ? ' minute' : ' minutes')) : ''); + } } diff --git a/src/app/views/dockerconfig/dockerconfig.component.ts b/src/app/views/dockerconfig/dockerconfig.component.ts index 8bcdcde3..9c0d6ef9 100644 --- a/src/app/views/dockerconfig/dockerconfig.component.ts +++ b/src/app/views/dockerconfig/dockerconfig.component.ts @@ -6,189 +6,189 @@ import {BreadcrumbService} from '../../components/breadcrumb/breadcrumb.service' import {BreadcrumbItem} from '../../components/breadcrumb/breadcrumb-item'; @Component({ - selector: 'app-dockerconfig', - templateUrl: './dockerconfig.component.html', - styleUrls: ['./dockerconfig.component.scss'] + selector: 'app-dockerconfig', + templateUrl: './dockerconfig.component.html', + styleUrls: ['./dockerconfig.component.scss'] }) export class DockerconfigComponent implements OnInit, OnDestroy { - private readonly _breadcrumb = inject(BreadcrumbService); - private readonly _crud = inject(CrudService); - private readonly _sidebar = inject(LeftSidebarService); - private readonly _toast = inject(ToasterService); - - instances: DockerInstance[]; - error: string = null; - status: AutoDockerStatus = {available: false, connected: false, running: false, message: ''}; - autoConnectRunning = false; - timeoutId: number = null; - modalId: number = null; - activeModal: null | 'add_edit' | 'settings' = null; - - constructor() { - this._sidebar.listConfigManagerPages(); - } - - ngOnInit(): void { - this.updateList(); - this._breadcrumb.setBreadcrumbs([new BreadcrumbItem('Config', '/views/config/'), - new BreadcrumbItem('Docker')]); - this._breadcrumb.hideZoom(); - this._sidebar.open(); - } - - ngOnDestroy() { - if (this.timeoutId !== null) { - clearTimeout(this.timeoutId); + private readonly _breadcrumb = inject(BreadcrumbService); + private readonly _crud = inject(CrudService); + private readonly _sidebar = inject(LeftSidebarService); + private readonly _toast = inject(ToasterService); + + instances: DockerInstance[]; + error: string = null; + status: AutoDockerStatus = {available: false, connected: false, running: false, message: ''}; + autoConnectRunning = false; + timeoutId: number = null; + modalId: number = null; + activeModal: null | 'add_edit' | 'settings' = null; + + constructor() { + this._sidebar.listConfigManagerPages(); } - this._breadcrumb.hide(); - this._sidebar.close(); - } - - updateList() { - this._crud.getDockerInstances().subscribe({ - next: (res: DockerInstance[]) => { - this.instances = res; - }, - error: err => { - console.log(err); - }, - }); - this._crud.getAutoDockerStatus().subscribe({ - next: (res: AutoDockerStatus) => { - this.status = res; - }, - error: err => { - console.log(err); - } - }); - } - - autoDocker() { - this.autoConnectRunning = true; - this.status.message = 'Sending start command...'; - this._toast.info('Sending start command...'); - this.timeoutId = setTimeout(() => this.updateAutoDockerStatus(), 500); - this._crud.doAutoHandshake().subscribe({ - next: res => { - this.autoConnectRunning = false; + + ngOnInit(): void { + this.updateList(); + this._breadcrumb.setBreadcrumbs([new BreadcrumbItem('Config', '/views/config/'), + new BreadcrumbItem('Docker')]); + this._breadcrumb.hideZoom(); + this._sidebar.open(); + } + + ngOnDestroy() { if (this.timeoutId !== null) { - clearTimeout(this.timeoutId); - this.timeoutId = null; + clearTimeout(this.timeoutId); } - const autoDockerResult = res; - if (autoDockerResult.success) { - this._toast.success('Connected to local docker instance'); - } else { - this._toast.error('Failed to connect to local docker instance'); - } - this.status = autoDockerResult.status; - this.instances = autoDockerResult.instances; - }, - error: err => { - this.autoConnectRunning = false; - console.log(err); - } - }); - } - - updateAutoDockerStatus() { - if (this.timeoutId === null) { - return; + this._breadcrumb.hide(); + this._sidebar.close(); + } + + updateList() { + this._crud.getDockerInstances().subscribe({ + next: (res: DockerInstance[]) => { + this.instances = res; + }, + error: err => { + console.log(err); + }, + }); + this._crud.getAutoDockerStatus().subscribe({ + next: (res: AutoDockerStatus) => { + this.status = res; + }, + error: err => { + console.log(err); + } + }); + } + + autoDocker() { + this.autoConnectRunning = true; + this.status.message = 'Sending start command...'; + this._toast.info('Sending start command...'); + this.timeoutId = setTimeout(() => this.updateAutoDockerStatus(), 500); + this._crud.doAutoHandshake().subscribe({ + next: res => { + this.autoConnectRunning = false; + if (this.timeoutId !== null) { + clearTimeout(this.timeoutId); + this.timeoutId = null; + } + const autoDockerResult = res; + if (autoDockerResult.success) { + this._toast.success('Connected to local docker instance'); + } else { + this._toast.error('Failed to connect to local docker instance'); + } + this.status = autoDockerResult.status; + this.instances = autoDockerResult.instances; + }, + error: err => { + this.autoConnectRunning = false; + console.log(err); + } + }); } - this._crud.getAutoDockerStatus().subscribe({ - next: res => { + updateAutoDockerStatus() { if (this.timeoutId === null) { - return; - } - const status = res; - if (this.status.message !== status.message) { - this._toast.info(status.message); + return; } - this.status = status; - this.timeoutId = setTimeout(() => this.updateAutoDockerStatus(), 500); - }, - error: err => { - console.log(err); - } - }); - } - - removeDockerInstance(instance: DockerInstance) { - this._crud.removeDockerInstance(instance.id).subscribe({ - next: res => { - const d = res; - if (d.error === '') { - this._toast.success('Deleted docker instance ' + instance.alias + '\''); + + this._crud.getAutoDockerStatus().subscribe({ + next: res => { + if (this.timeoutId === null) { + return; + } + const status = res; + if (this.status.message !== status.message) { + this._toast.info(status.message); + } + this.status = status; + this.timeoutId = setTimeout(() => this.updateAutoDockerStatus(), 500); + }, + error: err => { + console.log(err); + } + }); + } + + removeDockerInstance(instance: DockerInstance) { + this._crud.removeDockerInstance(instance.id).subscribe({ + next: res => { + const d = res; + if (d.error === '') { + this._toast.success('Deleted docker instance ' + instance.alias + '\''); + } else { + this._toast.error(d.error); + } + this.instances = d.instances; + this.status = d.status; + }, + error: err => { + console.log(err); + } + }); + } + + showModal(id: number) { + this.modalId = id; + this.activeModal = 'add_edit'; + } + + closeModal(newlist: DockerInstance[]) { + if (newlist !== undefined) { + this.instances = newlist; + this._crud.getAutoDockerStatus().subscribe({ + next: (res: AutoDockerStatus) => { + this.status = res; + }, + error: err => { + console.log(err); + } + }); } else { - this._toast.error(d.error); - } - this.instances = d.instances; - this.status = d.status; - }, - error: err => { - console.log(err); - } - }); - } - - showModal(id: number) { - this.modalId = id; - this.activeModal = 'add_edit'; - } - - closeModal(newlist: DockerInstance[]) { - if (newlist !== undefined) { - this.instances = newlist; - this._crud.getAutoDockerStatus().subscribe({ - next: (res: AutoDockerStatus) => { - this.status = res; - }, - error: err => { - console.log(err); + this.updateList(); } - }); - } else { - this.updateList(); + this.activeModal = null; + this.modalId = null; + } + + showSettingsModal() { + this.activeModal = 'settings'; + } + + closeSettingsModal() { + this.activeModal = null; + this.updateList(); } - this.activeModal = null; - this.modalId = null; - } - - showSettingsModal() { - this.activeModal = 'settings'; - } - - closeSettingsModal() { - this.activeModal = null; - this.updateList(); - } } export interface AutoDockerStatus { - available: boolean; - connected: boolean; - running: boolean; - message: string; + available: boolean; + connected: boolean; + running: boolean; + message: string; } export interface AutoDockerResult { - success: boolean; - status: AutoDockerStatus; - instances: DockerInstance[]; + success: boolean; + status: AutoDockerStatus; + instances: DockerInstance[]; } export interface DockerRemoveResponse { - error: string; - instances: DockerInstance[]; - status: AutoDockerStatus; + error: string; + instances: DockerInstance[]; + status: AutoDockerStatus; } export interface DockerInstance { - id: number; - host: string; - alias: string; - connected: boolean; - numberOfContainers: number; + id: number; + host: string; + alias: string; + connected: boolean; + numberOfContainers: number; } diff --git a/src/app/views/error/404.component.ts b/src/app/views/error/404.component.ts index 26a70201..fc9a12c2 100644 --- a/src/app/views/error/404.component.ts +++ b/src/app/views/error/404.component.ts @@ -1,11 +1,11 @@ import {Component} from '@angular/core'; @Component({ - templateUrl: '404.component.html' + templateUrl: '404.component.html' }) export class P404Component { - constructor() { - } + constructor() { + } } diff --git a/src/app/views/error/500.component.ts b/src/app/views/error/500.component.ts index e9dc5458..4a7bc69c 100644 --- a/src/app/views/error/500.component.ts +++ b/src/app/views/error/500.component.ts @@ -1,11 +1,11 @@ import {Component} from '@angular/core'; @Component({ - templateUrl: '500.component.html' + templateUrl: '500.component.html' }) export class P500Component { - constructor() { - } + constructor() { + } } diff --git a/src/app/views/forms/form-generator/file-uploader/file-uploader.component.ts b/src/app/views/forms/form-generator/file-uploader/file-uploader.component.ts index 48730b1d..58256104 100644 --- a/src/app/views/forms/form-generator/file-uploader/file-uploader.component.ts +++ b/src/app/views/forms/form-generator/file-uploader/file-uploader.component.ts @@ -3,56 +3,56 @@ import {PluginService} from '../../../../services/plugin.service'; import {ToasterService} from '../../../../components/toast-exposer/toaster.service'; @Component({ - selector: 'app-file-uploader', - templateUrl: './file-uploader.component.html', - styleUrls: ['./file-uploader.component.scss'] + selector: 'app-file-uploader', + templateUrl: './file-uploader.component.html', + styleUrls: ['./file-uploader.component.scss'] }) export class FileUploaderComponent implements OnInit { - public readonly _plugin = inject(PluginService); - public readonly _toast = inject(ToasterService); - - public files: File[]; - - public isLoading = false; - private uploadProgress = 0; - - @Input() loadPage: () => void; - - constructor() { - } - - ngOnInit(): void { - } - - onFileSelected(event: Event) { - this.files = Array.from((event.target as HTMLInputElement).files); - } - - loadPlugins() { - this.isLoading = true; - this.uploadProgress = 0; - this._plugin.loadPlugins(this.files).subscribe({ - next: res => { - this.files = null; - this.isLoading = false; - this.loadPage(); - }, error: err => { - console.log(err); - this._toast.error(err.message); - this.isLoading = false; - } - }); - } - - - removeFile(file - : - File - ) { - this.files = this.files.filter(f => f.name !== file.name); - } - - hasFiles() { - return this.files && this.files.length > 0; - } + public readonly _plugin = inject(PluginService); + public readonly _toast = inject(ToasterService); + + public files: File[]; + + public isLoading = false; + private uploadProgress = 0; + + @Input() loadPage: () => void; + + constructor() { + } + + ngOnInit(): void { + } + + onFileSelected(event: Event) { + this.files = Array.from((event.target as HTMLInputElement).files); + } + + loadPlugins() { + this.isLoading = true; + this.uploadProgress = 0; + this._plugin.loadPlugins(this.files).subscribe({ + next: res => { + this.files = null; + this.isLoading = false; + this.loadPage(); + }, error: err => { + console.log(err); + this._toast.error(err.message); + this.isLoading = false; + } + }); + } + + + removeFile(file + : + File + ) { + this.files = this.files.filter(f => f.name !== file.name); + } + + hasFiles() { + return this.files && this.files.length > 0; + } } diff --git a/src/app/views/forms/form-generator/form-generator.component.spec.ts b/src/app/views/forms/form-generator/form-generator.component.spec.ts index 3808f2e3..9db94f46 100644 --- a/src/app/views/forms/form-generator/form-generator.component.spec.ts +++ b/src/app/views/forms/form-generator/form-generator.component.spec.ts @@ -3,23 +3,23 @@ import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {FormGeneratorComponent} from './form-generator.component'; describe('FormGeneratorComponent', () => { - let component: FormGeneratorComponent; - let fixture: ComponentFixture; + let component: FormGeneratorComponent; + let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [FormGeneratorComponent] - }) - .compileComponents(); - })); + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [FormGeneratorComponent] + }) + .compileComponents(); + })); - beforeEach(() => { - fixture = TestBed.createComponent(FormGeneratorComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(FormGeneratorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); }); diff --git a/src/app/views/forms/form-generator/form-generator.component.ts b/src/app/views/forms/form-generator/form-generator.component.ts index f599ef01..2fbd2644 100644 --- a/src/app/views/forms/form-generator/form-generator.component.ts +++ b/src/app/views/forms/form-generator/form-generator.component.ts @@ -12,409 +12,409 @@ import {Subscription} from 'rxjs'; import {PluginStatus} from '../../../models/ui-request.model'; @Component({ - selector: 'app-form-generator', - templateUrl: './form-generator.component.html', - styleUrls: ['./form-generator.component.scss'] + selector: 'app-form-generator', + templateUrl: './form-generator.component.html', + styleUrls: ['./form-generator.component.scss'] }) export class FormGeneratorComponent implements OnInit, OnDestroy { - private readonly _config = inject(ConfigService); - private readonly _route = inject(ActivatedRoute); - private readonly _sidebar = inject(LeftSidebarService); - public readonly _breadcrumb = inject(BreadcrumbService); - private readonly _toast = inject(ToasterService); - private readonly _settings = inject(WebuiSettingsService); - - @ViewChild('submitButton') submitButton: ElementRef; - - formObj: JavaUiPage; - submitted = false; - form: UntypedFormGroup; - - pageId = ''; - pageNotFound = false; - pageList; - serverError; - private subscriptions = new Subscription(); - fileName: string; - - constructor() { - this.pageId = this._route.snapshot.paramMap.get('page') || ''; - - this._sidebar.listConfigManagerPages(); - } - - ngOnInit() { - this.onHashChange(); - this.initWebSocket(); - this.onReconnect(); - this._breadcrumb.setBreadcrumbs([new BreadcrumbItem('Config')]); - this._sidebar.open(); - } - - ngOnDestroy() { - //this._config.closeSocket(); - this._breadcrumb.hide(); - this._sidebar.close(); - this.subscriptions.unsubscribe(); - } - - private onHashChange() { - this._route.params.subscribe(params => { - this.pageId = params['page']; - this.loadPage(); - this.submitted = false; - }); - } - - private initWebSocket() { - //this._config.socketSend('hello world'); - const sub = this._config.onSocketEvent().subscribe(msg => { - const update = msg; - if (this.formObj && this.formObj.groups[update.webUiGroup] && this.formObj.groups[update.webUiGroup].configs[update.key]) { - const c = this.formObj.groups[update.webUiGroup].configs[update.key]; - if (this.form.controls[c.key].dirty === false) { - c.value = update.value; - } else {//has been edited - //if incoming value is different. use lodash.isEqual for arrays and == comparator for values - if ((Array.isArray(update.value) && JSON.stringify(this.form.controls[c.key].value) !== JSON.stringify(update.value)) || - (!Array.isArray(update.value) && this.form.controls[c.key].value !== update.value)) { - this._toast.warn( - 'The setting with id ' + c.key + ' has been changed to the new value "' + update.value + '" by the server. If you save, these changes will be overwritten.', - 'incoming change', ToastDuration.INFINITE); - } else { - c.value = update.value; - } - } - } else { - //console.log('could not update from WebSocket'); - } - }, err => { - setTimeout(() => { + private readonly _config = inject(ConfigService); + private readonly _route = inject(ActivatedRoute); + private readonly _sidebar = inject(LeftSidebarService); + public readonly _breadcrumb = inject(BreadcrumbService); + private readonly _toast = inject(ToasterService); + private readonly _settings = inject(WebuiSettingsService); + + @ViewChild('submitButton') submitButton: ElementRef; + + formObj: JavaUiPage; + submitted = false; + form: UntypedFormGroup; + + pageId = ''; + pageNotFound = false; + pageList; + serverError; + private subscriptions = new Subscription(); + fileName: string; + + constructor() { + this.pageId = this._route.snapshot.paramMap.get('page') || ''; + + this._sidebar.listConfigManagerPages(); + } + + ngOnInit() { + this.onHashChange(); this.initWebSocket(); - }, +this._settings.getSetting('reconnection.timeout')); - }); - this.subscriptions.add(sub); - } - - private onReconnect() { - const sub = this._config.onReconnection().subscribe( - b => { - if (b) { + this.onReconnect(); + this._breadcrumb.setBreadcrumbs([new BreadcrumbItem('Config')]); + this._sidebar.open(); + } + + ngOnDestroy() { + //this._config.closeSocket(); + this._breadcrumb.hide(); + this._sidebar.close(); + this.subscriptions.unsubscribe(); + } + + private onHashChange() { + this._route.params.subscribe(params => { + this.pageId = params['page']; this.loadPage(); this.submitted = false; - this._sidebar.listConfigManagerPages(); - } - } - ); - this.subscriptions.add(sub); - } - - protected loadPage() { - if (!this.pageId) { - this._config.getPageList().subscribe({ - next: res => { - this.pageList = res; - this.serverError = null; - this.pageNotFound = false; - }, error: err => { - this.serverError = err; + }); + } + + private initWebSocket() { + //this._config.socketSend('hello world'); + const sub = this._config.onSocketEvent().subscribe(msg => { + const update = msg; + if (this.formObj && this.formObj.groups[update.webUiGroup] && this.formObj.groups[update.webUiGroup].configs[update.key]) { + const c = this.formObj.groups[update.webUiGroup].configs[update.key]; + if (this.form.controls[c.key].dirty === false) { + c.value = update.value; + } else {//has been edited + //if incoming value is different. use lodash.isEqual for arrays and == comparator for values + if ((Array.isArray(update.value) && JSON.stringify(this.form.controls[c.key].value) !== JSON.stringify(update.value)) || + (!Array.isArray(update.value) && this.form.controls[c.key].value !== update.value)) { + this._toast.warn( + 'The setting with id ' + c.key + ' has been changed to the new value "' + update.value + '" by the server. If you save, these changes will be overwritten.', + 'incoming change', ToastDuration.INFINITE); + } else { + c.value = update.value; + } + } + } else { + //console.log('could not update from WebSocket'); + } + }, err => { + setTimeout(() => { + this.initWebSocket(); + }, +this._settings.getSetting('reconnection.timeout')); + }); + this.subscriptions.add(sub); + } + + private onReconnect() { + const sub = this._config.onReconnection().subscribe( + b => { + if (b) { + this.loadPage(); + this.submitted = false; + this._sidebar.listConfigManagerPages(); + } + } + ); + this.subscriptions.add(sub); + } + + protected loadPage() { + if (!this.pageId) { + this._config.getPageList().subscribe({ + next: res => { + this.pageList = res; + this.serverError = null; + this.pageNotFound = false; + }, error: err => { + this.serverError = err; + } + }); + } else { + this._config.getPage(this.pageId).subscribe({ + next: res => { + if (res == null) { + this.onPageNotFound(); + return; + } + + this.formObj = res; + this._breadcrumb.setBreadcrumbs([new BreadcrumbItem('Config', '/views/config/'), + new BreadcrumbItem(this.formObj.title.toString())]); + + this.buildFormGroup(); + + this.pageNotFound = false; + this.serverError = null; + }, + error: err => { + this.serverError = err; + } + }); } - }); - } else { - this._config.getPage(this.pageId).subscribe({ - next: res => { - if (res == null) { - this.onPageNotFound(); - return; - } - - this.formObj = res; - this._breadcrumb.setBreadcrumbs([new BreadcrumbItem('Config', '/views/config/'), - new BreadcrumbItem(this.formObj.title.toString())]); - - this.buildFormGroup(); - - this.pageNotFound = false; - this.serverError = null; - }, - error: err => { - this.serverError = err; + } + + private buildFormGroup() { + const formGroup = {}; + + // https://juristr.com/blog/2017/10/demystify-dynamic-angular-forms/ + for (const gKey of Object.keys(this.formObj.groups)) { + for (const cKey of Object.keys(this.formObj.groups[gKey].configs)) { + const config = this.formObj.groups[gKey].configs[cKey]; + let initValue; + if (config.webUiFormType === 'BOOLEAN') { + initValue = config.value || false; + } else if (config.webUiFormType === 'CHECKBOXES') { + initValue = config.value || []; + } else if (config.webUiFormType === 'LIST') { + initValue = config.values || []; + } else { + if (config.value === undefined || config.value === null) { + initValue = ''; + } else { + initValue = config.value; + } + } + formGroup[cKey] = new UntypedFormControl(initValue, + this.mapValidators(this.formObj.groups[gKey].configs[cKey])); + } } - }); + this.form = new UntypedFormGroup(formGroup); } - } - - private buildFormGroup() { - const formGroup = {}; - - // https://juristr.com/blog/2017/10/demystify-dynamic-angular-forms/ - for (const gKey of Object.keys(this.formObj.groups)) { - for (const cKey of Object.keys(this.formObj.groups[gKey].configs)) { - const config = this.formObj.groups[gKey].configs[cKey]; - let initValue; - if (config.webUiFormType === 'BOOLEAN') { - initValue = config.value || false; - } else if (config.webUiFormType === 'CHECKBOXES') { - initValue = config.value || []; - } else if (config.webUiFormType === 'LIST') { - initValue = config.values || []; - } else { - if (config.value === undefined || config.value === null) { - initValue = ''; - } else { - initValue = config.value; - } + + /** order groups within a page. + * groups with lower order value are rendered first + */ + public orderGroups(a: KeyValue, b: KeyValue) { + let out = 0; + if (a.value.order !== 0 && b.value.order === 0) { + out = -1; + } else if (a.value.order === 0 && b.value.order !== 0) { + out = 1; + } else if (a.value.order > b.value.order) { + out = 1; + } else if (a.value.order < b.value.order) { + out = -1; } - formGroup[cKey] = new UntypedFormControl(initValue, - this.mapValidators(this.formObj.groups[gKey].configs[cKey])); - } + return out; } - this.form = new UntypedFormGroup(formGroup); - } - - /** order groups within a page. - * groups with lower order value are rendered first - */ - public orderGroups(a: KeyValue, b: KeyValue) { - let out = 0; - if (a.value.order !== 0 && b.value.order === 0) { - out = -1; - } else if (a.value.order === 0 && b.value.order !== 0) { - out = 1; - } else if (a.value.order > b.value.order) { - out = 1; - } else if (a.value.order < b.value.order) { - out = -1; + + /** order configs within a group. + * configs with lower webUiOrder value are rendered first + */ + public orderConfigs(a: KeyValue, b: KeyValue) { + let out = 0; + if (a.value.webUiOrder > b.value.webUiOrder) { + out = 1; + } else if (a.value.webUiOrder < b.value.webUiOrder) { + out = -1; + } else if (a.value.webUiOrder != null && b.value.webUiOrder == null) { + out = -1; + } else if (a.value.webUiOrder == null && b.value.webUiOrder != null) { + out = 1; + } + return out; } - return out; - } - - /** order configs within a group. - * configs with lower webUiOrder value are rendered first - */ - public orderConfigs(a: KeyValue, b: KeyValue) { - let out = 0; - if (a.value.webUiOrder > b.value.webUiOrder) { - out = 1; - } else if (a.value.webUiOrder < b.value.webUiOrder) { - out = -1; - } else if (a.value.webUiOrder != null && b.value.webUiOrder == null) { - out = -1; - } else if (a.value.webUiOrder == null && b.value.webUiOrder != null) { - out = 1; + + private onPageNotFound() { + this.pageNotFound = true; + this.serverError = null; + this._config.getPageList().subscribe( + res => { + this.pageList = res; + }, + err => { + this.serverError = err; + } + ); } - return out; - } - - private onPageNotFound() { - this.pageNotFound = true; - this.serverError = null; - this._config.getPageList().subscribe( - res => { - this.pageList = res; - }, - err => { - this.serverError = err; + + private mapValidators(config) { + const formValidators = []; + const validators = config.webUiValidators; + + if (validators) { + for (const validation of Object.values(validators)) { + /*if(validation === 'REQUIRED') { + formValidators.push(Validators.required); + }*/ + if (validation === 'EMAIL') { + formValidators.push(Validators.email); + } + } } - ); - } - - private mapValidators(config) { - const formValidators = []; - const validators = config.webUiValidators; - - if (validators) { - for (const validation of Object.values(validators)) { - /*if(validation === 'REQUIRED') { - formValidators.push(Validators.required); - }*/ - if (validation === 'EMAIL') { - formValidators.push(Validators.email); + if (!['ConfigBoolean', 'ConfigClazzList', 'ConfigEnumList', 'ConfigList'].includes(config.configType)) { + formValidators.push(Validators.required);//by default, but not for checkboxes / clazzList / enumList } - } + return formValidators; } - if (!['ConfigBoolean', 'ConfigClazzList', 'ConfigEnumList', 'ConfigList'].includes(config.configType)) { - formValidators.push(Validators.required);//by default, but not for checkboxes / clazzList / enumList + + + inputValidation(key) { + if (this.submitted && this.form.controls[key].valid && this.form.controls[key].dirty) { + return {'is-valid': true}; + } else if (this.submitted && !this.form.controls[key].valid) { + return {'is-invalid': true}; + } } - return formValidators; - } + addElement(list, key, template) { + this.form.controls[key].markAsDirty(); + const copy = JSON.parse(JSON.stringify(template)); + copy.key = template.key + list.length; + // we can assign the biggest previous id + 1 as id for our new element + copy.id = Math.max(...list.map(el => el.id)) + 1; + list.push(copy); + } - inputValidation(key) { - if (this.submitted && this.form.controls[key].valid && this.form.controls[key].dirty) { - return {'is-valid': true}; - } else if (this.submitted && !this.form.controls[key].valid) { - return {'is-invalid': true}; + removeElement(list, key, index) { + this.form.controls[key].markAsDirty(); + list.splice(index, 1); } - } - - addElement(list, key, template) { - this.form.controls[key].markAsDirty(); - const copy = JSON.parse(JSON.stringify(template)); - copy.key = template.key + list.length; - // we can assign the biggest previous id + 1 as id for our new element - copy.id = Math.max(...list.map(el => el.id)) + 1; - list.push(copy); - } - - removeElement(list, key, index) { - this.form.controls[key].markAsDirty(); - list.splice(index, 1); - } - - onSubmit(form, e, error: () => {} = null) { - this.submitted = true; - - if (this.form.valid) { - const changes = {}; - for (const c of Object.keys(this.form.controls)) { - if (this.form.controls[c].dirty) { - changes[c] = this.form.controls[c].value; - } - } - this._config.saveChanges(changes).subscribe({ - next: res => { - interface Feedback { - success?: number; - warning?: string; - } - - const f: Feedback = res; - //console.log(f); - if (f.success) { - this._toast.success('Saved changes.', null, null, ToastDuration.SHORT); - this.loadPage();// reload config-page after updating a config, because it can lead to additional groups or elements - } else { - this._toast.warn(f.warning, null, ToastDuration.INFINITE); - console.log(f); - if (error != null) { - error(); + + onSubmit(form, e, error: () => {} = null) { + this.submitted = true; + + if (this.form.valid) { + const changes = {}; + for (const c of Object.keys(this.form.controls)) { + if (this.form.controls[c].dirty) { + changes[c] = this.form.controls[c].value; + } } - } - this.form.markAsPristine(); - }, - error: err => { - console.log(err); - this._toast.error('an error occurred on the server'); - if (error != null) { - error(); - } + this._config.saveChanges(changes).subscribe({ + next: res => { + interface Feedback { + success?: number; + warning?: string; + } + + const f: Feedback = res; + //console.log(f); + if (f.success) { + this._toast.success('Saved changes.', null, null, ToastDuration.SHORT); + this.loadPage();// reload config-page after updating a config, because it can lead to additional groups or elements + } else { + this._toast.warn(f.warning, null, ToastDuration.INFINITE); + console.log(f); + if (error != null) { + error(); + } + } + this.form.markAsPristine(); + }, + error: err => { + console.log(err); + this._toast.error('an error occurred on the server'); + if (error != null) { + error(); + } + } + }); + } else { + this._toast.warn('Changes could not be saved. Please check invalid input.', 'invalid input', ToastDuration.INFINITE); } - }); - } else { - this._toast.warn('Changes could not be saved. Please check invalid input.', 'invalid input', ToastDuration.INFINITE); } - } - - classOrEnumName(s: string) { - if (s.includes('$')) { - return s.split('$')[1]; - } else if (s.includes('.')) { - return s.split('.')[s.split('.').length - 1]; - } else { - return s; + + classOrEnumName(s: string) { + if (s.includes('$')) { + return s.split('$')[1]; + } else if (s.includes('.')) { + return s.split('.')[s.split('.').length - 1]; + } else { + return s; + } } - } - - handleClassList(key, val, isChecked) { - this.form.controls[key].markAsDirty(); - if (isChecked) { - this.form.controls[key].value.push(val); - } else { - const newVal = []; - for (const v of this.form.controls[key].value) { - if (v !== val) { - newVal.push(v); + + handleClassList(key, val, isChecked) { + this.form.controls[key].markAsDirty(); + if (isChecked) { + this.form.controls[key].value.push(val); + } else { + const newVal = []; + for (const v of this.form.controls[key].value) { + if (v !== val) { + newVal.push(v); + } + } + this.form.controls[key].setValue(newVal); } - } - this.form.controls[key].setValue(newVal); } - } - markElement(key: string) { - this.form.controls[key].markAsDirty(); - } + markElement(key: string) { + this.form.controls[key].markAsDirty(); + } - markElementReset(key: string, value: any) { - this.markElement(key); - value.dockerRunning = false; - } + markElementReset(key: string, value: any) { + this.markElement(key); + value.dockerRunning = false; + } - setProtocolAndMarkElement(el: any, e: Event, key: string, value: any) { - e.preventDefault(); - el.protocol = e.target['value']; + setProtocolAndMarkElement(el: any, e: Event, key: string, value: any) { + e.preventDefault(); + el.protocol = e.target['value']; - this.markElementReset(key, value); - } + this.markElementReset(key, value); + } - setInsecureAndMark(usingInsecure: boolean, key: string, el: any) { - if (usingInsecure) { - el.port = 2375.0; + setInsecureAndMark(usingInsecure: boolean, key: string, el: any) { + if (usingInsecure) { + el.port = 2375.0; + } + this.markElementReset(key, el); } - this.markElementReset(key, el); - } - deactivatePlugin(el: any, key: string) { - el.status = PluginStatus.LOADED; - this.markElementReset(key, el); - this.forceSubmit(() => el.status = PluginStatus.ACTIVE); - } - - activatePlugin(el: any, key: string) { - el.status = PluginStatus.ACTIVE; - this.markElementReset(key, el); - this.forceSubmit(() => el.status = PluginStatus.LOADED); + deactivatePlugin(el: any, key: string) { + el.status = PluginStatus.LOADED; + this.markElementReset(key, el); + this.forceSubmit(() => el.status = PluginStatus.ACTIVE); + } - } + activatePlugin(el: any, key: string) { + el.status = PluginStatus.ACTIVE; + this.markElementReset(key, el); + this.forceSubmit(() => el.status = PluginStatus.LOADED); - forceSubmit(func: () => {}) { - this.onSubmit(this.form, null, func); - } + } - comparePlugins(a: any, b: any) { - if (a.isSystemComponent) { - return -1; + forceSubmit(func: () => {}) { + this.onSubmit(this.form, null, func); } - if (b.isSystemComponent) { - return 1; + + comparePlugins(a: any, b: any) { + if (a.isSystemComponent) { + return -1; + } + if (b.isSystemComponent) { + return 1; + } + return 0; } - return 0; - } - filterPlugins(values): any[] { - return values.filter(p => p['isUiVisible']).sort(this.comparePlugins); - } + filterPlugins(values): any[] { + return values.filter(p => p['isUiVisible']).sort(this.comparePlugins); + } } export interface JavaUiPage { - id: number; - title: String; - description: String; - groups: Map; + id: number; + title: String; + description: String; + groups: Map; - fullWidth: boolean; + fullWidth: boolean; } export interface JavaUiGroup { - id: number; - pageId: number; - title: String; - description: String; - configs: Map; + id: number; + pageId: number; + title: String; + description: String; + configs: Map; } export interface JavaUiConfig { - key: String; - oldValue: any; - value: any; - values: String[];//enumList, clazzList, List - template: any; //List - requiresRestart: boolean; - webUiFormType: String; - webUiGroup: string; - webUiValidators: String[]; + key: String; + oldValue: any; + value: any; + values: String[];//enumList, clazzList, List + template: any; //List + requiresRestart: boolean; + webUiFormType: String; + webUiGroup: string; + webUiValidators: String[]; } diff --git a/src/app/views/forms/form-generator/form-generator.model.ts b/src/app/views/forms/form-generator/form-generator.model.ts index 8aec3d06..8b99a99f 100644 --- a/src/app/views/forms/form-generator/form-generator.model.ts +++ b/src/app/views/forms/form-generator/form-generator.model.ts @@ -1,5 +1,5 @@ export interface DockerStatus { - dockerId: number; - successful: boolean; - errorMessage: string; + dockerId: number; + successful: boolean; + errorMessage: string; } diff --git a/src/app/views/login/login.component.ts b/src/app/views/login/login.component.ts index 82a875b6..cd170a9e 100644 --- a/src/app/views/login/login.component.ts +++ b/src/app/views/login/login.component.ts @@ -3,71 +3,71 @@ import {UntypedFormControl, UntypedFormGroup, Validators} from '@angular/forms'; import {Router} from '@angular/router'; @Component({ - selector: 'app-dashboard', - templateUrl: 'login.component.html', - styleUrls: ['login.component.scss'] + selector: 'app-dashboard', + templateUrl: 'login.component.html', + styleUrls: ['login.component.scss'] }) export class LoginComponent implements OnInit { - form: UntypedFormGroup; - formObj; - history = [ - {server: '10.100.20.30', port: 4000}, - {server: '10.100.20.40', port: 4003}, - {server: '10.100.20.50', port: 4050} - ]; - favorites = [ - {server: '10.200.20.30', port: 5000}, - {server: '10.200.20.40', port: 5003}, - {server: '10.200.20.50', port: 5050} - ]; - submitted = false; + form: UntypedFormGroup; + formObj; + history = [ + {server: '10.100.20.30', port: 4000}, + {server: '10.100.20.40', port: 4003}, + {server: '10.100.20.50', port: 4050} + ]; + favorites = [ + {server: '10.200.20.30', port: 5000}, + {server: '10.200.20.40', port: 5003}, + {server: '10.200.20.50', port: 5050} + ]; + submitted = false; - loginModel: LoginModel = new LoginModel('10.100.1.2', 123); + loginModel: LoginModel = new LoginModel('10.100.1.2', 123); - constructor(private _router: Router) { - } + constructor(private _router: Router) { + } - ngOnInit() { + ngOnInit() { - this.formObj = [ - {key: 'server', value: '', validation: Validators.required}, - {key: 'port', value: '', validation: Validators.required} - ]; + this.formObj = [ + {key: 'server', value: '', validation: Validators.required}, + {key: 'port', value: '', validation: Validators.required} + ]; - const f = []; - const formGroup = {}; - for (let i of this.formObj) { - formGroup[i.key] = new UntypedFormControl(i.value || '', i.validation); + const f = []; + const formGroup = {}; + for (let i of this.formObj) { + formGroup[i.key] = new UntypedFormControl(i.value || '', i.validation); + } + this.form = new UntypedFormGroup(formGroup); } - this.form = new UntypedFormGroup(formGroup); - } - submit(form) { - this.submitted = true; - if (form.valid) { - this._router.navigateByUrl(''); + submit(form) { + this.submitted = true; + if (form.valid) { + this._router.navigateByUrl(''); + } } - } - applyHistory(h) { - this.loginModel.server = h.server; - this.loginModel.port = h.port; - } + applyHistory(h) { + this.loginModel.server = h.server; + this.loginModel.port = h.port; + } - testConnection() { - // todo - } + testConnection() { + // todo + } - saveNewFavorite(form) { - this.favorites.push({server: form.controls.server.value, port: form.controls.port.value}); - } + saveNewFavorite(form) { + this.favorites.push({server: form.controls.server.value, port: form.controls.port.value}); + } - deleteFavorite() { - // todo - } + deleteFavorite() { + // todo + } } export class LoginModel { - constructor(public server: string, public port: number) { - } + constructor(public server: string, public port: number) { + } } diff --git a/src/app/views/monitoring/monitoring.component.ts b/src/app/views/monitoring/monitoring.component.ts index 3a2ad98c..91386dd5 100644 --- a/src/app/views/monitoring/monitoring.component.ts +++ b/src/app/views/monitoring/monitoring.component.ts @@ -9,133 +9,133 @@ import {WebuiSettingsService} from '../../services/webui-settings.service'; import {Subscription} from 'rxjs'; @Component({ - selector: 'app-monitoring', - templateUrl: './monitoring.component.html', - styleUrls: ['./monitoring.component.scss'] + selector: 'app-monitoring', + templateUrl: './monitoring.component.html', + styleUrls: ['./monitoring.component.scss'] }) export class MonitoringComponent implements OnInit, OnDestroy { - private readonly _information = inject(InformationService); - private readonly _route = inject(ActivatedRoute); - public readonly _breadcrumb = inject(BreadcrumbService); - private readonly _sidebar = inject(LeftSidebarService); - private readonly _settings = inject(WebuiSettingsService); + private readonly _information = inject(InformationService); + private readonly _route = inject(ActivatedRoute); + public readonly _breadcrumb = inject(BreadcrumbService); + private readonly _sidebar = inject(LeftSidebarService); + private readonly _settings = inject(WebuiSettingsService); - data; - routerId; - pageList: InformationPage[]; - serverError; - pageNotFound = false; - private subscriptions = new Subscription(); + data; + routerId; + pageList: InformationPage[]; + serverError; + pageNotFound = false; + private subscriptions = new Subscription(); - constructor() { - } + constructor() { + } - ngOnInit() { - this.routerId = this._route.snapshot.paramMap.get('id'); - //todo set node active if routerId is set - this._route.params.subscribe(params => { - this.routerId = params['id']; - this.getServiceData(); - }); + ngOnInit() { + this.routerId = this._route.snapshot.paramMap.get('id'); + //todo set node active if routerId is set + this._route.params.subscribe(params => { + this.routerId = params['id']; + this.getServiceData(); + }); - this._sidebar.listInformationManagerPages(); - this._sidebar.open(); + this._sidebar.listInformationManagerPages(); + this._sidebar.open(); - this.initSocket(); - const sub = this._information.onReconnection().subscribe( - b => { - if (b) { - this.getServiceData(); - this._sidebar.listInformationManagerPages(); - } - } - ); - this.subscriptions.add(sub); - } + this.initSocket(); + const sub = this._information.onReconnection().subscribe( + b => { + if (b) { + this.getServiceData(); + this._sidebar.listInformationManagerPages(); + } + } + ); + this.subscriptions.add(sub); + } - initSocket() { - //this.subscription = this._information.onSocketEvent().subscribe( - const sub = this._information.onSocketEvent().subscribe( - update => { - const info: InformationObject = update; - if (this.data && this.data.groups[info.groupId] && this.data.groups[info.groupId].informationObjects[info.id]) { - switch (info.type) { - //case 'InformationGraph': - case 'InformationProgress': - // to enable the animation - Object.keys(info).forEach(key => { - this.data.groups[info.groupId].informationObjects[info.id][key] = info[key]; - }); - break; - default: - this.data.groups[info.groupId].informationObjects[info.id] = info; + initSocket() { + //this.subscription = this._information.onSocketEvent().subscribe( + const sub = this._information.onSocketEvent().subscribe( + update => { + const info: InformationObject = update; + if (this.data && this.data.groups[info.groupId] && this.data.groups[info.groupId].informationObjects[info.id]) { + switch (info.type) { + //case 'InformationGraph': + case 'InformationProgress': + // to enable the animation + Object.keys(info).forEach(key => { + this.data.groups[info.groupId].informationObjects[info.id][key] = info[key]; + }); + break; + default: + this.data.groups[info.groupId].informationObjects[info.id] = info; + } + } + }, + err => { + setTimeout(() => { + this.initSocket(); + }, +this._settings.getSetting('reconnection.timeout')); } - } - }, - err => { - setTimeout(() => { - this.initSocket(); - }, +this._settings.getSetting('reconnection.timeout')); - } - ); - this.subscriptions.add(sub); - } + ); + this.subscriptions.add(sub); + } - ngOnDestroy() { - this._breadcrumb.hide(); - this._sidebar.close(); - this.subscriptions.unsubscribe(); - } + ngOnDestroy() { + this._breadcrumb.hide(); + this._sidebar.close(); + this.subscriptions.unsubscribe(); + } - getServiceData() { - if (!this.routerId) { - this._information.getPageList().subscribe({ - next: res => { - this.pageList = res; - this._breadcrumb.setDashboardBreadcrumbs([new BreadcrumbItem('Dashboard')]); - this.serverError = null; - }, error: err => { - this.serverError = err; - } - }); - } else { - this._information.getPage(this.routerId).subscribe({ - next: res => { - if (res == null) { - this.onPageNotFound(); - this._breadcrumb.setDashboardBreadcrumbs([new BreadcrumbItem('Dashboard')]); + getServiceData() { + if (!this.routerId) { + this._information.getPageList().subscribe({ + next: res => { + this.pageList = res; + this._breadcrumb.setDashboardBreadcrumbs([new BreadcrumbItem('Dashboard')]); + this.serverError = null; + }, error: err => { + this.serverError = err; + } + }); + } else { + this._information.getPage(this.routerId).subscribe({ + next: res => { + if (res == null) { + this.onPageNotFound(); + this._breadcrumb.setDashboardBreadcrumbs([new BreadcrumbItem('Dashboard')]); - } else { - this.pageNotFound = false; - this.data = res; - this._breadcrumb.setBreadcrumbs([new BreadcrumbItem('Monitoring', '/views/monitoring/'), new BreadcrumbItem(this.data.name.toString())]); - if (this.data.fullWidth) { - this._breadcrumb.hideZoom(); - } - this.serverError = null; - } - }, error: err => { - this.onPageNotFound(); - this._breadcrumb.setBreadcrumbs([new BreadcrumbItem('Monitoring')]); - this.serverError = err; + } else { + this.pageNotFound = false; + this.data = res; + this._breadcrumb.setBreadcrumbs([new BreadcrumbItem('Monitoring', '/views/monitoring/'), new BreadcrumbItem(this.data.name.toString())]); + if (this.data.fullWidth) { + this._breadcrumb.hideZoom(); + } + this.serverError = null; + } + }, error: err => { + this.onPageNotFound(); + this._breadcrumb.setBreadcrumbs([new BreadcrumbItem('Monitoring')]); + this.serverError = err; + } + }); } - }); } - } - private onPageNotFound() { - this.pageNotFound = true; - this.data = null; - this.serverError = null; - this._information.getPageList().subscribe({ - next: res => { - this.pageList = res; - }, - error: err => { - this.serverError = err; - } - }); - } + private onPageNotFound() { + this.pageNotFound = true; + this.data = null; + this.serverError = null; + this._information.getPageList().subscribe({ + next: res => { + this.pageList = res; + }, + error: err => { + this.serverError = err; + } + }); + } } diff --git a/src/app/views/query-interfaces/query-interfaces.component.ts b/src/app/views/query-interfaces/query-interfaces.component.ts index f43bfbd4..d2d05a78 100644 --- a/src/app/views/query-interfaces/query-interfaces.component.ts +++ b/src/app/views/query-interfaces/query-interfaces.component.ts @@ -6,258 +6,263 @@ import {ModalDirective} from 'ngx-bootstrap/modal'; import {ActivatedRoute, Router} from '@angular/router'; import {ToasterService} from '../../components/toast-exposer/toaster.service'; import {RelationalResult} from '../../components/data-view/models/result-set.model'; -import {QueryInterface, QueryInterfaceInformation, QueryInterfaceInformationRequest, QueryInterfaceSetting} from './query-interfaces.model'; -import {LeftSidebarService} from "../../components/left-sidebar/left-sidebar.service"; +import { + QueryInterface, + QueryInterfaceInformation, + QueryInterfaceInformationRequest, + QueryInterfaceSetting +} from './query-interfaces.model'; +import {LeftSidebarService} from '../../components/left-sidebar/left-sidebar.service'; @Component({ - selector: 'app-query-interfaces', - templateUrl: './query-interfaces.component.html', - styleUrls: ['./query-interfaces.component.scss'] + selector: 'app-query-interfaces', + templateUrl: './query-interfaces.component.html', + styleUrls: ['./query-interfaces.component.scss'] }) export class QueryInterfacesComponent implements OnInit, OnDestroy { - private readonly _crud = inject(CrudService); - private readonly _route = inject(ActivatedRoute); - private readonly _router = inject(Router); - private readonly _toast = inject(ToasterService); - private readonly _sidebar = inject(LeftSidebarService); + private readonly _crud = inject(CrudService); + private readonly _route = inject(ActivatedRoute); + private readonly _router = inject(Router); + private readonly _toast = inject(ToasterService); + private readonly _sidebar = inject(LeftSidebarService); - queryInterfaces: QueryInterface[]; - availableQueryInterfaces: QueryInterfaceInformation[]; - route: String; - routeListener; - private subscriptions = new Subscription(); + queryInterfaces: QueryInterface[]; + availableQueryInterfaces: QueryInterfaceInformation[]; + route: String; + routeListener; + private subscriptions = new Subscription(); - editingQI: QueryInterface; - editingQIForm: UntypedFormGroup; - deletingQI; + editingQI: QueryInterface; + editingQIForm: UntypedFormGroup; + deletingQI; - editingAvailableQI: QueryInterfaceInformation; - editingAvailableQIForm: UntypedFormGroup; - availableQIUniqueNameForm: UntypedFormGroup; + editingAvailableQI: QueryInterfaceInformation; + editingAvailableQIForm: UntypedFormGroup; + availableQIUniqueNameForm: UntypedFormGroup; - @ViewChild('QISettingsModal', {static: false}) public QISettingsModal: ModalDirective; + @ViewChild('QISettingsModal', {static: false}) public QISettingsModal: ModalDirective; - constructor() { + constructor() { - } - - ngOnInit() { - this._sidebar.hide(); - this.getQueryInterfaces(); - this.getAvailableQueryInterfaces(); - this.route = this._route.snapshot.paramMap.get('action'); - this.routeListener = this._route.params.subscribe(params => { - this.route = params['action']; - }); - const sub = this._crud.onReconnection().subscribe( - b => { - if (b) { - this.getQueryInterfaces(); - this.getAvailableQueryInterfaces(); - } - } - ); - this.subscriptions.add(sub); - } - - ngOnDestroy() { - this.subscriptions.unsubscribe(); - } - - getQueryInterfaces() { - this._crud.getQueryInterfaces().subscribe({ - next: res => { - const queryInterfaces = res; - queryInterfaces.sort((a, b) => (a.uniqueName > b.uniqueName) ? 1 : -1); - this.queryInterfaces = queryInterfaces; - }, error: err => { - console.log(err); - } - }); - } + } - getAvailableQueryInterfaces() { - this._crud.getAvailableQueryInterfaces().subscribe({ - next: res => { - const availableQIs = res; - availableQIs.sort((a, b) => (a.name > b.name) ? 1 : -1); - this.availableQueryInterfaces = res; - }, error: err => { - console.log(err); - } - }); - } + ngOnInit() { + this._sidebar.hide(); + this.getQueryInterfaces(); + this.getAvailableQueryInterfaces(); + this.route = this._route.snapshot.paramMap.get('action'); + this.routeListener = this._route.params.subscribe(params => { + this.route = params['action']; + }); + const sub = this._crud.onReconnection().subscribe( + b => { + if (b) { + this.getQueryInterfaces(); + this.getAvailableQueryInterfaces(); + } + } + ); + this.subscriptions.add(sub); + } - onCloseModal() { - this.editingQI = undefined; - this.editingQIForm = undefined; - this.editingAvailableQI = undefined; - this.editingAvailableQIForm = undefined; - } + ngOnDestroy() { + this.subscriptions.unsubscribe(); + } - initQueryInterfaceSettings(queryInterface: QueryInterface) { - this.editingQI = queryInterface; - const fc = {}; - for (const [k, v] of Object.entries(this.editingQI.availableSettings)) { - const validators = []; - if (v.required) { - validators.push(Validators.required); - } - const val = queryInterface.currentSettings[v.name]; - fc[v.name] = new UntypedFormControl({value: val, disabled: !v.modifiable}, validators); + getQueryInterfaces() { + this._crud.getQueryInterfaces().subscribe({ + next: res => { + const queryInterfaces = res; + queryInterfaces.sort((a, b) => (a.uniqueName > b.uniqueName) ? 1 : -1); + this.queryInterfaces = queryInterfaces; + }, error: err => { + console.log(err); + } + }); } - this.editingQIForm = new UntypedFormGroup(fc); - this.QISettingsModal.show(); - } - saveQISettings() { - const queryInterface = this.editingQI; - queryInterface.availableSettings = null; - for (const [k, v] of Object.entries(this.editingQIForm.controls)) { - if (!v.disabled) { - queryInterface.currentSettings[k] = v.value; - } else { - //remove disabled (=non-modifiable) entries, else the backend will throw an exception - delete queryInterface.currentSettings[k]; - } + getAvailableQueryInterfaces() { + this._crud.getAvailableQueryInterfaces().subscribe({ + next: res => { + const availableQIs = res; + availableQIs.sort((a, b) => (a.name > b.name) ? 1 : -1); + this.availableQueryInterfaces = res; + }, error: err => { + console.log(err); + } + }); } - this._crud.updateQueryInterfaceSettings(queryInterface).subscribe({ - next: res => { - const result = res; - if (!result.error) { - this._toast.success('updated queryInterface settings'); - } else { - this._toast.exception(result); - } - this.QISettingsModal.hide(); - this.getQueryInterfaces(); - }, error: err => { - this._toast.error('could not update queryInterface settings'); - console.log(err); - } - }); - } - initAvailableQISettings(availableQI: QueryInterfaceInformation) { - this.editingAvailableQI = availableQI; - const fc = {}; - for (const [k, v] of Object.entries(this.editingAvailableQI.availableSettings)) { - const validators = []; - if (v.required) { - validators.push(Validators.required); - } - let val = v.defaultValue; - if (v.options) { - val = v.options[0]; - } - fc[v.name] = new UntypedFormControl(val, validators); + onCloseModal() { + this.editingQI = undefined; + this.editingQIForm = undefined; + this.editingAvailableQI = undefined; + this.editingAvailableQIForm = undefined; } - this.editingAvailableQIForm = new UntypedFormGroup(fc); - this.availableQIUniqueNameForm = new UntypedFormGroup({ - uniqueName: new UntypedFormControl(null, [Validators.required, Validators.pattern(this._crud.getValidationRegex()), validateUniqueQI(this.queryInterfaces)]) - }); - this.QISettingsModal.show(); - } - getFeedback() { - const errors = this.availableQIUniqueNameForm.controls['uniqueName'].errors; - if (errors) { - if (errors.required) { - return 'missing unique name'; - } else if (errors.pattern) { - return 'invalid unique name'; - } else if (errors.unique) { - return 'name is not unique'; - } + initQueryInterfaceSettings(queryInterface: QueryInterface) { + this.editingQI = queryInterface; + const fc = {}; + for (const [k, v] of Object.entries(this.editingQI.availableSettings)) { + const validators = []; + if (v.required) { + validators.push(Validators.required); + } + const val = queryInterface.currentSettings[v.name]; + fc[v.name] = new UntypedFormControl({value: val, disabled: !v.modifiable}, validators); + } + this.editingQIForm = new UntypedFormGroup(fc); + this.QISettingsModal.show(); } - return ''; - } - getAvailableQISetting(availableQI, key: string): QueryInterfaceSetting { - return availableQI.availableSettings.filter((a, i) => a.name === key)[0]; - } + saveQISettings() { + const queryInterface = this.editingQI; + queryInterface.availableSettings = null; + for (const [k, v] of Object.entries(this.editingQIForm.controls)) { + if (!v.disabled) { + queryInterface.currentSettings[k] = v.value; + } else { + //remove disabled (=non-modifiable) entries, else the backend will throw an exception + delete queryInterface.currentSettings[k]; + } + } + this._crud.updateQueryInterfaceSettings(queryInterface).subscribe({ + next: res => { + const result = res; + if (!result.error) { + this._toast.success('updated queryInterface settings'); + } else { + this._toast.exception(result); + } + this.QISettingsModal.hide(); + this.getQueryInterfaces(); + }, error: err => { + this._toast.error('could not update queryInterface settings'); + console.log(err); + } + }); + } - addQueryInterface() { - if (!this.editingAvailableQIForm.valid) { - return; + initAvailableQISettings(availableQI: QueryInterfaceInformation) { + this.editingAvailableQI = availableQI; + const fc = {}; + for (const [k, v] of Object.entries(this.editingAvailableQI.availableSettings)) { + const validators = []; + if (v.required) { + validators.push(Validators.required); + } + let val = v.defaultValue; + if (v.options) { + val = v.options[0]; + } + fc[v.name] = new UntypedFormControl(val, validators); + } + this.editingAvailableQIForm = new UntypedFormGroup(fc); + this.availableQIUniqueNameForm = new UntypedFormGroup({ + uniqueName: new UntypedFormControl(null, [Validators.required, Validators.pattern(this._crud.getValidationRegex()), validateUniqueQI(this.queryInterfaces)]) + }); + this.QISettingsModal.show(); } - if (!this.availableQIUniqueNameForm.valid) { - return; + + getFeedback() { + const errors = this.availableQIUniqueNameForm.controls['uniqueName'].errors; + if (errors) { + if (errors.required) { + return 'missing unique name'; + } else if (errors.pattern) { + return 'invalid unique name'; + } else if (errors.unique) { + return 'name is not unique'; + } + } + return ''; } - const deploy: QueryInterfaceInformationRequest = { - uniqueName: this.availableQIUniqueNameForm.controls['uniqueName'].value, - clazzName: this.editingAvailableQI.clazz, - currentSettings: {} - }; - for (const [k, v] of Object.entries(this.editingAvailableQIForm.controls)) { - deploy.currentSettings[k] = v.value; + + getAvailableQISetting(availableQI, key: string): QueryInterfaceSetting { + return availableQI.availableSettings.filter((a, i) => a.name === key)[0]; } - this._crud.createQueryInterface(deploy).subscribe({ - next: res => { - const result = res; - if (!result.error) { - this._toast.success('Added query interface: ' + deploy.uniqueName, result.query); - this._router.navigate(['./../'], {relativeTo: this._route}); - } else { - this._toast.exception(result); - } - this.QISettingsModal.hide(); - }, error: err => { - this._toast.error('Could not add query interface: ' + deploy.uniqueName); - console.log(err); - } - }); - } - removeQueryInterface(queryInterface: QueryInterface) { - if (this.deletingQI !== queryInterface) { - this.deletingQI = queryInterface; - } else { - this._crud.removeQueryInterface(queryInterface.uniqueName).subscribe({ - next: res => { - const result = res; - if (!result.error) { - this._toast.success('Removed query interface: ' + queryInterface.uniqueName, result.query); - this.getQueryInterfaces(); - } else { - this._toast.exception(result); - } - }, error: err => { - this._toast.error('Could not remove query interface: ' + queryInterface.uniqueName, 'server error'); - console.log(err); + addQueryInterface() { + if (!this.editingAvailableQIForm.valid) { + return; + } + if (!this.availableQIUniqueNameForm.valid) { + return; + } + const deploy: QueryInterfaceInformationRequest = { + uniqueName: this.availableQIUniqueNameForm.controls['uniqueName'].value, + clazzName: this.editingAvailableQI.clazz, + currentSettings: {} + }; + for (const [k, v] of Object.entries(this.editingAvailableQIForm.controls)) { + deploy.currentSettings[k] = v.value; } - }); + this._crud.createQueryInterface(deploy).subscribe({ + next: res => { + const result = res; + if (!result.error) { + this._toast.success('Added query interface: ' + deploy.uniqueName, result.query); + this._router.navigate(['./../'], {relativeTo: this._route}); + } else { + this._toast.exception(result); + } + this.QISettingsModal.hide(); + }, error: err => { + this._toast.error('Could not add query interface: ' + deploy.uniqueName); + console.log(err); + } + }); } - } - validate(form: UntypedFormGroup, key) { - if (form === undefined) { - return; - } - if (form.controls[key].status === 'DISABLED') { - return; + removeQueryInterface(queryInterface: QueryInterface) { + if (this.deletingQI !== queryInterface) { + this.deletingQI = queryInterface; + } else { + this._crud.removeQueryInterface(queryInterface.uniqueName).subscribe({ + next: res => { + const result = res; + if (!result.error) { + this._toast.success('Removed query interface: ' + queryInterface.uniqueName, result.query); + this.getQueryInterfaces(); + } else { + this._toast.exception(result); + } + }, error: err => { + this._toast.error('Could not remove query interface: ' + queryInterface.uniqueName, 'server error'); + console.log(err); + } + }); + } } - if (form.controls[key].valid) { - return 'is-valid'; - } else { - return 'is-invalid'; + + validate(form: UntypedFormGroup, key) { + if (form === undefined) { + return; + } + if (form.controls[key].status === 'DISABLED') { + return; + } + if (form.controls[key].valid) { + return 'is-valid'; + } else { + return 'is-invalid'; + } } - } } // see https://angular.io/guide/form-validation#custom-validators function validateUniqueQI(queryInterfaces: QueryInterface[]): ValidatorFn { - return (control: AbstractControl): { [key: string]: any } | null => { - if (!control.value) { - return null; - } - for (const s of queryInterfaces) { - if (s.uniqueName === control.value) { - return {unique: true}; - } - } - return null; - }; + return (control: AbstractControl): { [key: string]: any } | null => { + if (!control.value) { + return null; + } + for (const s of queryInterfaces) { + if (s.uniqueName === control.value) { + return {unique: true}; + } + } + return null; + }; } diff --git a/src/app/views/query-interfaces/query-interfaces.model.ts b/src/app/views/query-interfaces/query-interfaces.model.ts index 11d964d2..c6c0d6eb 100644 --- a/src/app/views/query-interfaces/query-interfaces.model.ts +++ b/src/app/views/query-interfaces/query-interfaces.model.ts @@ -1,30 +1,30 @@ export interface QueryInterface { - uniqueName: string; - supportsDdl: boolean; - supportsDml: boolean; - interfaceType: string; - availableSettings: QueryInterfaceSetting[]; - currentSettings: Map; + uniqueName: string; + supportsDdl: boolean; + supportsDml: boolean; + interfaceType: string; + availableSettings: QueryInterfaceSetting[]; + currentSettings: Map; } export interface QueryInterfaceInformation { - name: string; - description: string; - clazz: string; - availableSettings: QueryInterfaceSetting[]; + name: string; + description: string; + clazz: string; + availableSettings: QueryInterfaceSetting[]; } export interface QueryInterfaceSetting { - name: string; - canBeNull: boolean; - required: boolean; - modifiable: boolean; - defaultValue: string; - options: string[]; + name: string; + canBeNull: boolean; + required: boolean; + modifiable: boolean; + defaultValue: string; + options: string[]; } export interface QueryInterfaceInformationRequest { - clazzName: string; - uniqueName: string; - currentSettings: any;//Map + clazzName: string; + uniqueName: string; + currentSettings: any;//Map } diff --git a/src/app/views/querying/algebra/algebra.component.ts b/src/app/views/querying/algebra/algebra.component.ts index 228bd5cf..da32db29 100644 --- a/src/app/views/querying/algebra/algebra.component.ts +++ b/src/app/views/querying/algebra/algebra.component.ts @@ -1,4 +1,20 @@ -import {AfterViewInit, Component, computed, effect, ElementRef, HostBinding, inject, OnDestroy, OnInit, Signal, signal, untracked, ViewChild, ViewEncapsulation, WritableSignal} from '@angular/core'; +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'; @@ -19,7 +35,7 @@ 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"; +import {KeyValue} from '@angular/common'; @Component({ selector: 'app-algebra', @@ -79,7 +95,7 @@ export class AlgebraComponent implements OnInit, AfterViewInit, OnDestroy { } } return map; - }) + }); effect(() => { const catalog = this._catalog.listener(); @@ -113,8 +129,8 @@ export class AlgebraComponent implements OnInit, AfterViewInit, OnDestroy { this.sidebarNodes = nodes; this._leftSidebar.setNodes(nodes); this._leftSidebar.open(); - }) - }) + }); + }); } static toSidebarNode(nodeModel: AlgNodeModel): SidebarNode { @@ -384,18 +400,18 @@ export class AlgebraComponent implements OnInit, AfterViewInit, OnDestroy { 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); - }); + .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; @@ -592,7 +608,7 @@ export class AlgebraComponent implements OnInit, AfterViewInit, OnDestroy { * Set temporal values when dragging a node */ dragStart(e, node: Node) { - const $bg = $("#wrapper"); + const $bg = $('#wrapper'); this.scrollTop = $bg.scrollTop(); this.scrollLeft = $bg.scrollLeft(); this.nodes.get(node.id).dragging = true; @@ -602,7 +618,7 @@ export class AlgebraComponent implements OnInit, AfterViewInit, OnDestroy { * Set temporal values when dragging a node */ draggingNode(e, node: Node) { - const $bg = $("#wrapper"); + 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; @@ -612,14 +628,14 @@ export class AlgebraComponent implements OnInit, AfterViewInit, OnDestroy { * Save the position of a node when it was moved */ savePos(e, node: Node) { - const $bg = $("#drop"); + 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) + 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; diff --git a/src/app/views/querying/algebra/algebra.model.ts b/src/app/views/querying/algebra/algebra.model.ts index 7b7a3b57..51c2f33b 100644 --- a/src/app/views/querying/algebra/algebra.model.ts +++ b/src/app/views/querying/algebra/algebra.model.ts @@ -1,6 +1,6 @@ import {SortState} from '../../../components/data-view/models/sort-state.model'; import {SidebarNode} from '../../../models/sidebar-node.model'; -import {signal, WritableSignal} from "@angular/core"; +import {signal, WritableSignal} from '@angular/core'; export enum LogicalOperator { Scan = 'RelScan', diff --git a/src/app/views/querying/console/console.component.ts b/src/app/views/querying/console/console.component.ts index 97e1fcbd..9fea04bd 100644 --- a/src/app/views/querying/console/console.component.ts +++ b/src/app/views/querying/console/console.component.ts @@ -1,4 +1,14 @@ -import {Component, effect, inject, OnDestroy, OnInit, signal, untracked, ViewChild, WritableSignal} from '@angular/core'; +import { + Component, + effect, + inject, + OnDestroy, + OnInit, + signal, + untracked, + ViewChild, + WritableSignal +} from '@angular/core'; import {EntityConfig} from '../../../components/data-view/data-table/entity-config'; import {CrudService} from '../../../services/crud.service'; import {RelationalResult, Result} from '../../../components/data-view/models/result-set.model'; @@ -20,398 +30,398 @@ import {CatalogService} from '../../../services/catalog.service'; import {NamespaceModel} from '../../../models/catalog.model'; @Component({ - selector: 'app-console', - templateUrl: './console.component.html', - styleUrls: ['./console.component.scss'] + selector: 'app-console', + templateUrl: './console.component.html', + styleUrls: ['./console.component.scss'] }) export class ConsoleComponent implements OnInit, OnDestroy { - private readonly _crud = inject(CrudService); - private readonly _leftSidebar = inject(LeftSidebarService); - private readonly _breadcrumb = inject(BreadcrumbService); - private readonly _settings = inject(WebuiSettingsService); - public readonly _util = inject(UtilService); - public readonly _toast = inject(ToasterService); - public readonly _catalog = inject(CatalogService); - private readonly _sidebar = inject(LeftSidebarService); - - @ViewChild('editor', {static: false}) codeEditor; - @ViewChild('historySearchInput') historySearchInput; - - history: Map = new Map(); - readonly MAX_HISTORY = 50; //maximum items in history - private readonly LOCAL_STORAGE_HISTORY_KEY = 'query-history'; - private readonly LOCAL_STORAGE_NAMESPACE_KEY = 'polypheny-namespace'; - - results: WritableSignal[]> = signal([]); - collapsed: boolean[]; - queryAnalysis: InformationPage; - analyzeQuery = true; - useCache = true; - private originalCache: boolean = null; - showingAnalysis = false; - websocket: WebSocket; - private subscriptions = new Subscription(); - readonly loading: WritableSignal = signal(false); - readonly language: WritableSignal = signal('sql'); - saveInHistory = true; - showSearch = false; - historySearchQuery = ''; - confirmDeletingHistory; - readonly activeNamespace: WritableSignal = signal(null); - readonly namespaces: WritableSignal = signal([]); - - entityConfig: EntityConfig = { - create: false, - update: false, - delete: false, - sort: false, - search: false, - exploring: false - }; - showNamespaceConfig: boolean; - - constructor() { - this.websocket = new WebSocket(); - this._sidebar.close(); - // @ts-ignore - if (window.Cypress) { - (window).executeQuery = (query: string) => { - this.codeEditor.setCode(query); - this.submitQuery(); - }; - } + private readonly _crud = inject(CrudService); + private readonly _leftSidebar = inject(LeftSidebarService); + private readonly _breadcrumb = inject(BreadcrumbService); + private readonly _settings = inject(WebuiSettingsService); + public readonly _util = inject(UtilService); + public readonly _toast = inject(ToasterService); + public readonly _catalog = inject(CatalogService); + private readonly _sidebar = inject(LeftSidebarService); + + @ViewChild('editor', {static: false}) codeEditor; + @ViewChild('historySearchInput') historySearchInput; + + history: Map = new Map(); + readonly MAX_HISTORY = 50; //maximum items in history + private readonly LOCAL_STORAGE_HISTORY_KEY = 'query-history'; + private readonly LOCAL_STORAGE_NAMESPACE_KEY = 'polypheny-namespace'; + + results: WritableSignal[]> = signal([]); + collapsed: boolean[]; + queryAnalysis: InformationPage; + analyzeQuery = true; + useCache = true; + private originalCache: boolean = null; + showingAnalysis = false; + websocket: WebSocket; + private subscriptions = new Subscription(); + readonly loading: WritableSignal = signal(false); + readonly language: WritableSignal = signal('sql'); + saveInHistory = true; + showSearch = false; + historySearchQuery = ''; + confirmDeletingHistory; + readonly activeNamespace: WritableSignal = signal(null); + readonly namespaces: WritableSignal = signal([]); + + entityConfig: EntityConfig = { + create: false, + update: false, + delete: false, + sort: false, + search: false, + exploring: false + }; + showNamespaceConfig: boolean; + + constructor() { + this.websocket = new WebSocket(); + this._sidebar.close(); + // @ts-ignore + if (window.Cypress) { + (window).executeQuery = (query: string) => { + this.codeEditor.setCode(query); + this.submitQuery(); + }; + } - this.initWebsocket(); + this.initWebsocket(); - effect(() => { - const namespace = this._catalog.namespaces(); - untracked(() => { - this.namespaces.set(Array.from(namespace.values())); - this.loadAndSetNamespaceDB(); - }); - }); - } + effect(() => { + const namespace = this._catalog.namespaces(); + untracked(() => { + this.namespaces.set(Array.from(namespace.values())); + this.loadAndSetNamespaceDB(); + }); + }); + } - ngOnInit() { - QueryHistory.fromJson(localStorage.getItem(this.LOCAL_STORAGE_HISTORY_KEY), this.history); - this._breadcrumb.hide(); + ngOnInit() { + QueryHistory.fromJson(localStorage.getItem(this.LOCAL_STORAGE_HISTORY_KEY), this.history); + this._breadcrumb.hide(); - this.loadAndSetNamespaceDB(); - } + this.loadAndSetNamespaceDB(); + } - private loadAndSetNamespaceDB() { - let namespaceName = localStorage.getItem(this.LOCAL_STORAGE_NAMESPACE_KEY); + private loadAndSetNamespaceDB() { + let namespaceName = localStorage.getItem(this.LOCAL_STORAGE_NAMESPACE_KEY); - if (namespaceName === null || (this.namespaces && this.namespaces.length > 0 && (this.namespaces().filter(n => n.name === namespaceName).length === 0))) { - if (this.namespaces() && this.namespaces().length > 0) { - namespaceName = this.namespaces()[0].name; - } else { - namespaceName = 'public'; - } + if (namespaceName === null || (this.namespaces && this.namespaces.length > 0 && (this.namespaces().filter(n => n.name === namespaceName).length === 0))) { + if (this.namespaces() && this.namespaces().length > 0) { + namespaceName = this.namespaces()[0].name; + } else { + namespaceName = 'public'; + } + } + if (!namespaceName) { + return; + } + this.activeNamespace.set(namespaceName); + + this.storeNamespace(namespaceName); } - if (!namespaceName) { - return; + + ngOnDestroy() { + this._leftSidebar.close(); + this.subscriptions.unsubscribe(); + this.websocket.close(); + this._breadcrumb.hide(); + window.onbeforeunload = null; + window.onkeydown = null; } - this.activeNamespace.set(namespaceName); - this.storeNamespace(namespaceName); - } - ngOnDestroy() { - this._leftSidebar.close(); - this.subscriptions.unsubscribe(); - this.websocket.close(); - this._breadcrumb.hide(); - window.onbeforeunload = null; - window.onkeydown = null; - } + submitQuery() { + const code = this.codeEditor.getCode(); + if (!code) { + return; + } + if (this.saveInHistory) { + this.addToHistory(code, this.language()); + } + if (this.usesAdvancedConsole(this.language())) { // maybe adjust + const matchGraph = code.toLowerCase().match('use graph [a-zA-Z][a-zA-Z0-1]*'); + if (matchGraph !== null && matchGraph.length >= 0) { + const namespace = matchGraph[matchGraph.length - 1].replace('use ', ''); + this.activeNamespace.set(namespace); + } + const match = code.toLowerCase().match('use [a-zA-Z][a-zA-Z0-1]*'); + if (match !== null && match.length >= 0) { + const namespace = match[match.length - 1].replace('use ', ''); + if (namespace !== 'placement') { + this.activeNamespace.set(namespace); + } + } + + + if (code.match('show db')) { + this._catalog.updateIfNecessary().subscribe(catalog => { + this.loading.set(false); + }); + return; + } - submitQuery() { - const code = this.codeEditor.getCode(); - if (!code) { - return; - } - if (this.saveInHistory) { - this.addToHistory(code, this.language()); - } - if (this.usesAdvancedConsole(this.language())) { // maybe adjust - const matchGraph = code.toLowerCase().match('use graph [a-zA-Z][a-zA-Z0-1]*'); - if (matchGraph !== null && matchGraph.length >= 0) { - const namespace = matchGraph[matchGraph.length - 1].replace('use ', ''); - this.activeNamespace.set(namespace); - } - - const match = code.toLowerCase().match('use [a-zA-Z][a-zA-Z0-1]*'); - if (match !== null && match.length >= 0) { - const namespace = match[match.length - 1].replace('use ', ''); - if (namespace !== 'placement') { - this.activeNamespace.set(namespace); } - } + this._leftSidebar.setNodes([]); + if (this.analyzeQuery) { + this._leftSidebar.open(); + } else { + this._leftSidebar.close(); + } + this.queryAnalysis = null; - if (code.match('show db')) { - this._catalog.updateIfNecessary().subscribe(catalog => { - this.loading.set(false); - }); - return; - } + this.loading.set(true); + if (!this._crud.anyQuery(this.websocket, new QueryRequest(code, this.analyzeQuery, this.useCache, this.language(), this.activeNamespace()))) { + this.loading.set(false); + this.results.set([new RelationalResult('Could not establish a connection with the server.')]); + } + } + collapseAll(collapse: boolean) { + this.collapsed.fill(collapse); } - this._leftSidebar.setNodes([]); - if (this.analyzeQuery) { - this._leftSidebar.open(); - } else { - this._leftSidebar.close(); + addToHistory(query: string, lang: string): void { + if (this.history.size >= this.MAX_HISTORY) { + let h: QueryHistory = new QueryHistory(''); + this.history.forEach((val, key) => { + if (val.time < h.time) { + h = val; + } + }); + this.history.delete(h.query); + } + const newHistory = new QueryHistory(query, null, lang); + this.history.set(newHistory.query, newHistory); + + localStorage.setItem(this.LOCAL_STORAGE_HISTORY_KEY, JSON.stringify(Array.from(this.history.values()))); } - this.queryAnalysis = null; - this.loading.set(true); - if (!this._crud.anyQuery(this.websocket, new QueryRequest(code, this.analyzeQuery, this.useCache, this.language(), this.activeNamespace()))) { - this.loading.set(false); - this.results.set([new RelationalResult('Could not establish a connection with the server.')]); + applyHistory(query: string, lang: string, run: boolean) { + this.language.set(lang); + this.codeEditor.setCode(query); + if (run) { + this.submitQuery(); + } } - } - - collapseAll(collapse: boolean) { - this.collapsed.fill(collapse); - } - - addToHistory(query: string, lang: string): void { - if (this.history.size >= this.MAX_HISTORY) { - let h: QueryHistory = new QueryHistory(''); - this.history.forEach((val, key) => { - if (val.time < h.time) { - h = val; + + deleteHistoryItem(key, e) { + if (this.confirmDeletingHistory === key) { + this.history.delete(key); + localStorage.setItem(this.LOCAL_STORAGE_HISTORY_KEY, JSON.stringify(Array.from(this.history.values()))); + } else { + this.confirmDeletingHistory = key; } - }); - this.history.delete(h.query); + e.stopPropagation(); } - const newHistory = new QueryHistory(query, null, lang); - this.history.set(newHistory.query, newHistory); - localStorage.setItem(this.LOCAL_STORAGE_HISTORY_KEY, JSON.stringify(Array.from(this.history.values()))); - } + //from: https://stackoverflow.com/questions/52793944/angular-keyvalue-pipe-sort-properties-iterate-in-order + orderHistory(a: KeyValue, b: KeyValue) { + return a.value.time > b.value.time ? -1 : (b.value.time > a.value.time ? 1 : 0); + } - applyHistory(query: string, lang: string, run: boolean) { - this.language.set(lang); - this.codeEditor.setCode(query); - if (run) { - this.submitQuery(); + openHistorySearch() { + this.showSearch = true; + setTimeout( + () => this.historySearchInput.nativeElement.focus(), + 1 + ); } - } - - deleteHistoryItem(key, e) { - if (this.confirmDeletingHistory === key) { - this.history.delete(key); - localStorage.setItem(this.LOCAL_STORAGE_HISTORY_KEY, JSON.stringify(Array.from(this.history.values()))); - } else { - this.confirmDeletingHistory = key; + + closeHistorySearch() { + this.showSearch = false; + this.historySearchQuery = ''; } - e.stopPropagation(); - } - - //from: https://stackoverflow.com/questions/52793944/angular-keyvalue-pipe-sort-properties-iterate-in-order - orderHistory(a: KeyValue, b: KeyValue) { - return a.value.time > b.value.time ? -1 : (b.value.time > a.value.time ? 1 : 0); - } - - openHistorySearch() { - this.showSearch = true; - setTimeout( - () => this.historySearchInput.nativeElement.focus(), - 1 - ); - } - - closeHistorySearch() { - this.showSearch = false; - this.historySearchQuery = ''; - } - - - initWebsocket() { - //function to define behavior when clicking on a page link - const nodeBehavior = (tree, node, $event) => { - if (node.data.id === 'console') { - //this.queryAnalysis = null; - this.showingAnalysis = false; - this._breadcrumb.hide(); - node.setIsActive(true); - return; - } - const split = node.data.routerLink.split('/'); - const analyzerId = split[0]; - const analyzerPage = split[1]; - if (analyzerId && analyzerPage) { - this._crud.getAnalyzerPage(analyzerId, analyzerPage).subscribe({ - next: res => { - console.log(res); - this.queryAnalysis = res; - this.showingAnalysis = true; - this._breadcrumb.setBreadcrumbs([new BreadcrumbItem(node.data.name)]); - if (this.queryAnalysis.fullWidth) { - this._breadcrumb.hideZoom(); + + + initWebsocket() { + //function to define behavior when clicking on a page link + const nodeBehavior = (tree, node, $event) => { + if (node.data.id === 'console') { + //this.queryAnalysis = null; + this.showingAnalysis = false; + this._breadcrumb.hide(); + node.setIsActive(true); + return; + } + const split = node.data.routerLink.split('/'); + const analyzerId = split[0]; + const analyzerPage = split[1]; + if (analyzerId && analyzerPage) { + this._crud.getAnalyzerPage(analyzerId, analyzerPage).subscribe({ + next: res => { + console.log(res); + this.queryAnalysis = res; + this.showingAnalysis = true; + this._breadcrumb.setBreadcrumbs([new BreadcrumbItem(node.data.name)]); + if (this.queryAnalysis.fullWidth) { + this._breadcrumb.hideZoom(); + } + node.setIsActive(true); + }, error: err => { + console.log(err); + } + }); + } + }; + + const sub = this.websocket.onMessage().subscribe({ + next: msg => { + //if msg contains nodes of the sidebar + if (Array.isArray(msg) && msg[0].hasOwnProperty('routerLink')) { + const sidebarNodesTemp: SidebarNode[] = msg; + const sidebarNodes: SidebarNode[] = []; + const labels = new Set(); + sidebarNodesTemp.sort(this._leftSidebar.sortNodes).forEach((s) => { + if (s.label) { + labels.add(s.label); + } else { + sidebarNodes.push(SidebarNode.fromJson(s, {allowRouting: false, action: nodeBehavior})); + } + }); + for (const l of [...labels].sort()) { + sidebarNodes.push(new SidebarNode(l, l).asSeparator()); + sidebarNodesTemp.filter((n) => n.label === l).sort(this._leftSidebar.sortNodes).forEach((n) => { + sidebarNodes.push(SidebarNode.fromJson(n, {allowRouting: false, action: nodeBehavior})); + }); + } + + sidebarNodes.unshift(new SidebarNode('console', 'console', 'fa fa-keyboard-o').setAction(nodeBehavior)); + + this._leftSidebar.setNodes(sidebarNodes); + if (sidebarNodes.length > 0) { + this._leftSidebar.open(); + } else { + this._leftSidebar.close(); + } + + } else if (Array.isArray(msg) && ((msg[0].hasOwnProperty('data') || msg[0].hasOwnProperty('affectedTuples') || msg[0].hasOwnProperty('error')))) { // array of ResultSets + this.loading.set(false); + this.results.set([]>msg); + this.collapsed = new Array(this.results.length); + this.collapsed.fill(false); + + } 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')); } - node.setIsActive(true); - }, error: err => { - console.log(err); - } }); - } - }; + this.subscriptions.add(sub); + } - 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})); + createView(info: ViewInformation) { + this.codeEditor.setCode(info.fullQuery); + } + + executeView(info: ViewInformation) { + this.codeEditor.setCode(info.fullQuery); + this.submitQuery(); + } + + formatQuery() { + let code = this.codeEditor.getCode(); + if (!code) { + return; + } + let before = ''; + const after = ')'; + + // here we replace the Json incompatible types with placeholders + const temp = code.match(/NumberDecimal\([^)]*\)/g); + + if (temp !== null) { + for (let i = 0; i < temp.length; i++) { + code = code.replace(temp[i], '"___' + i + '"'); } - }); - 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('console', 'console', 'fa fa-keyboard-o').setAction(nodeBehavior)); - this._leftSidebar.setNodes(sidebarNodes); - if (sidebarNodes.length > 0) { - this._leftSidebar.open(); - } else { - this._leftSidebar.close(); - } - - } else if (Array.isArray(msg) && ((msg[0].hasOwnProperty('data') || msg[0].hasOwnProperty('affectedTuples') || msg[0].hasOwnProperty('error')))) { // array of ResultSets - this.loading.set(false); - this.results.set([]>msg); - this.collapsed = new Array(this.results.length); - this.collapsed.fill(false); - - } 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; + const splits = code.split('('); + before = splits.shift() + '('; + + try { + let json = this.parse(splits.join('(').slice(0, -1)); + // we have to translate them back + if (temp !== null) { + for (let i = 0; i < temp.length; i++) { + json = json.replace('"___' + i + '"', temp[i]); + } } - } + + this.codeEditor.setCode(before + json + after); + } catch (e) { + this._toast.warn(e); } - }, - error: err => { - //this._leftSidebar.setError('Lost connection with the server.'); - setTimeout(() => { - this.initWebsocket(); - }, +this._settings.getSetting('reconnection.timeout')); - } - }); - this.subscriptions.add(sub); - } - - createView(info: ViewInformation) { - this.codeEditor.setCode(info.fullQuery); - } - - executeView(info: ViewInformation) { - this.codeEditor.setCode(info.fullQuery); - this.submitQuery(); - } - - formatQuery() { - let code = this.codeEditor.getCode(); - if (!code) { - return; } - let before = ''; - const after = ')'; - // here we replace the Json incompatible types with placeholders - const temp = code.match(/NumberDecimal\([^)]*\)/g); + parse(code: string) { + const formatted = JSON.stringify(JSON.parse('[' + code + ']'), null, 4); + return formatted.substring(1, formatted.length - 1); + } - if (temp !== null) { - for (let i = 0; i < temp.length; i++) { - code = code.replace(temp[i], '"___' + i + '"'); - } + private storeNamespace(name: string) { + localStorage.setItem(this.LOCAL_STORAGE_NAMESPACE_KEY, name); } + toggleCollapsed(i: number) { + if (this.collapsed !== undefined && this.collapsed[i] !== undefined) { + this.collapsed[i] = !this.collapsed[i]; + } + } - const splits = code.split('('); - before = splits.shift() + '('; + clearConsole() { + this.codeEditor.setCode(''); + } - try { - let json = this.parse(splits.join('(').slice(0, -1)); - // we have to translate them back - if (temp !== null) { - for (let i = 0; i < temp.length; i++) { - json = json.replace('"___' + i + '"', temp[i]); + toggleCache(b: boolean) { + if (this.originalCache === null) { + this.originalCache = this.useCache; } - } - - this.codeEditor.setCode(before + json + after); - } catch (e) { - this._toast.warn(e); + this.useCache = b; } - } - parse(code: string) { - const formatted = JSON.stringify(JSON.parse('[' + code + ']'), null, 4); - return formatted.substring(1, formatted.length - 1); - } + revertCache() { + console.log('revert'); + this.useCache = this.originalCache; + this.originalCache = null; + } - private storeNamespace(name: string) { - localStorage.setItem(this.LOCAL_STORAGE_NAMESPACE_KEY, name); - } + usesAdvancedConsole(lang: string) { + return lang === 'mql' || lang === 'cypher'; + } - toggleCollapsed(i: number) { - if (this.collapsed !== undefined && this.collapsed[i] !== undefined) { - this.collapsed[i] = !this.collapsed[i]; + toggleNamespaceField() { + this.showNamespaceConfig = !this.showNamespaceConfig; } - } - clearConsole() { - this.codeEditor.setCode(''); - } + changedDefaultDB(n) { + console.log(n); + this.activeNamespace.set(n); + } - toggleCache(b: boolean) { - if (this.originalCache === null) { - this.originalCache = this.useCache; + setLanguage(language) { + this.language.set(language); } - this.useCache = b; - } - - revertCache() { - console.log('revert'); - this.useCache = this.originalCache; - this.originalCache = null; - } - - usesAdvancedConsole(lang: string) { - return lang === 'mql' || lang === 'cypher'; - } - - toggleNamespaceField() { - this.showNamespaceConfig = !this.showNamespaceConfig; - } - - changedDefaultDB(n) { - console.log(n); - this.activeNamespace.set(n); - } - - setLanguage(language) { - this.language.set(language); - } } diff --git a/src/app/views/querying/console/query-history.model.ts b/src/app/views/querying/console/query-history.model.ts index e81d26fe..2a708fb0 100644 --- a/src/app/views/querying/console/query-history.model.ts +++ b/src/app/views/querying/console/query-history.model.ts @@ -2,59 +2,59 @@ import * as moment from 'moment'; export class QueryHistory { - query: string; - time: Date; - lang: string; - - constructor(query: string, time = null, lang = 'sql') { - this.query = query; - this.lang = lang; - if (time === null) { - this.time = new Date(); - } else { - this.time = new Date(time); + query: string; + time: Date; + lang: string; + + constructor(query: string, time = null, lang = 'sql') { + this.query = query; + this.lang = lang; + if (time === null) { + this.time = new Date(); + } else { + this.time = new Date(time); + } } - } - static fromJson(json: string, map: Map): void { - if (json === null) { - return; + static fromJson(json: string, map: Map): void { + if (json === null) { + return; + } + const obj = JSON.parse(json); + + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + map.set(obj[key].query, new QueryHistory(obj[key].query, obj[key].time, obj[key].lang)); + } + } } - const obj = JSON.parse(json); - for (const key in obj) { - if (obj.hasOwnProperty(key)) { - map.set(obj[key].query, new QueryHistory(obj[key].query, obj[key].time, obj[key].lang)); - } + displayTime() { + //padstart: from: https://stackoverflow.com/questions/8935414/getminutes-0-9-how-to-display-two-digit-numbers + return String(this.time.getHours()).padStart(2, '0') + ':' + + String(this.time.getMinutes()).padStart(2, '0') + ':' + + String(this.time.getSeconds()).padStart(2, '0'); + } + + displayDate() { + return String(this.time.getDate()).padStart(2, '0') + '.' + + String(this.time.getMonth()).padStart(2, '0') + '.' + + String(this.time.getFullYear()); + } + + fromNow() { + //see https://stackoverflow.com/questions/35441820/moment-js-tomorrow-today-and-yesterday + const self = this; + return moment(this.time).calendar(null, { + lastMonth: 'in mmmm', + lastWeek: '[on] ddd', + lastDay: '[yesterday]', + sameDay: '[today]', + //in all other cases + sameElse: function () { + return self.displayDate(); + } + }); } - } - - displayTime() { - //padstart: from: https://stackoverflow.com/questions/8935414/getminutes-0-9-how-to-display-two-digit-numbers - return String(this.time.getHours()).padStart(2, '0') + ':' + - String(this.time.getMinutes()).padStart(2, '0') + ':' + - String(this.time.getSeconds()).padStart(2, '0'); - } - - displayDate() { - return String(this.time.getDate()).padStart(2, '0') + '.' + - String(this.time.getMonth()).padStart(2, '0') + '.' + - String(this.time.getFullYear()); - } - - fromNow() { - //see https://stackoverflow.com/questions/35441820/moment-js-tomorrow-today-and-yesterday - const self = this; - return moment(this.time).calendar(null, { - lastMonth: 'in mmmm', - lastWeek: '[on] ddd', - lastDay: '[yesterday]', - sameDay: '[today]', - //in all other cases - sameElse: function () { - return self.displayDate(); - } - }); - } } diff --git a/src/app/views/querying/graphical-querying/graphical-querying.component.spec.ts b/src/app/views/querying/graphical-querying/graphical-querying.component.spec.ts index 8a200820..84e87cfe 100644 --- a/src/app/views/querying/graphical-querying/graphical-querying.component.spec.ts +++ b/src/app/views/querying/graphical-querying/graphical-querying.component.spec.ts @@ -3,23 +3,23 @@ import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {GraphicalQueryingComponent} from './graphical-querying.component'; describe('GraphicalQueryingComponent', () => { - let component: GraphicalQueryingComponent; - let fixture: ComponentFixture; + let component: GraphicalQueryingComponent; + let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [GraphicalQueryingComponent] - }) - .compileComponents(); - })); + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [GraphicalQueryingComponent] + }) + .compileComponents(); + })); - beforeEach(() => { - fixture = TestBed.createComponent(GraphicalQueryingComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(GraphicalQueryingComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); }); diff --git a/src/app/views/querying/graphical-querying/graphical-querying.component.ts b/src/app/views/querying/graphical-querying/graphical-querying.component.ts index 323a80bf..8a138585 100644 --- a/src/app/views/querying/graphical-querying/graphical-querying.component.ts +++ b/src/app/views/querying/graphical-querying/graphical-querying.component.ts @@ -1,4 +1,16 @@ -import {AfterViewInit, Component, effect, inject, OnDestroy, OnInit, signal, untracked, ViewChild, ViewEncapsulation, WritableSignal} from '@angular/core'; +import { + AfterViewInit, + Component, + effect, + inject, + OnDestroy, + OnInit, + signal, + untracked, + ViewChild, + ViewEncapsulation, + WritableSignal +} from '@angular/core'; import * as $ from 'jquery'; import 'jquery-ui/ui/widget'; import 'jquery-ui/ui/widgets/sortable'; @@ -17,497 +29,497 @@ import {ViewInformation} from '../../../components/data-view/data-view.component import {CatalogService} from '../../../services/catalog.service'; @Component({ - selector: 'app-graphical-querying', - templateUrl: './graphical-querying.component.html', - styleUrls: ['./graphical-querying.component.scss'], - encapsulation: ViewEncapsulation.None, // new elements in sortable should have margin as well + selector: 'app-graphical-querying', + templateUrl: './graphical-querying.component.html', + styleUrls: ['./graphical-querying.component.scss'], + encapsulation: ViewEncapsulation.None, // new elements in sortable should have margin as well }) export class GraphicalQueryingComponent implements OnInit, AfterViewInit, OnDestroy { - @ViewChild('editorGenerated', {static: false}) editorGenerated; - generatedSQL; - result: RelationalResult; - selectedColumn = {}; - loading: WritableSignal = signal(false); - modalRefCreateView: BsModalRef; - whereCounter = 0; - orderByCounter = 0; - andCounter = 0; - filteredUserSet: FilteredUserInput; - private subscriptions = new Subscription(); - private readonly webSocket: WebSocket; - - //fields for the graphical query generation - namespaces = new Map();//schemaName, schemaName - entities = new Map();//tableName, number of columns of this table - fields = new Map();//columnId, columnName - umlData = new Map();//schemaName, uml - joinConditions = new Map(); - readonly selects: WritableSignal<{ name: string, id: string }[]> = signal([]); - - private readonly _crud = inject(CrudService); - private readonly _leftSidebar = inject(LeftSidebarService); - private readonly _toast = inject(ToasterService); - private readonly _catalog = inject(CatalogService); - - constructor() { - this.webSocket = new WebSocket(); - this.initWebSocket(); - this.initSchema(); - } - - ngOnInit() { - this._leftSidebar.open(); - - this.initGraphicalQuerying(); - const sub = this._crud.onReconnection().subscribe( - b => { - if (b) { - - } - } - ); - this.subscriptions.add(sub); - } - - ngAfterViewInit() { - this.generateSQL(); - } - - ngOnDestroy() { - this._leftSidebar.close(); - // this._leftSidebar.reset(); - this.subscriptions.unsubscribe(); - this.webSocket.close(); - } - - initWebSocket() { - this.webSocket.onMessage().subscribe({ - next: res => { - const result = res; - this.result = result[0]; - this.loading.set(false); - }, error: err => { - this._toast.error('Unknown error on the server.'); - this.loading.set(false); - } - }); - } - - initSchema() { - effect(() => { - const catalog = this._catalog.listener(); - untracked(() => { - const nodeAction = (tree, node, $event) => { - if (!node.isActive && node.isLeaf) { - this.addCol(node.data); - node.setIsActive(true, true); - } else if (node.isActive && node.isLeaf) { - - node.setIsActive(false, true); - this.removeCol(node.data.id); - - //deletes the selection if nothing is choosen - if (this.selectedColumn['column'].toString() === node.data.id) { - this.selectedCol([]); + @ViewChild('editorGenerated', {static: false}) editorGenerated; + generatedSQL; + result: RelationalResult; + selectedColumn = {}; + loading: WritableSignal = signal(false); + modalRefCreateView: BsModalRef; + whereCounter = 0; + orderByCounter = 0; + andCounter = 0; + filteredUserSet: FilteredUserInput; + private subscriptions = new Subscription(); + private readonly webSocket: WebSocket; + + //fields for the graphical query generation + namespaces = new Map();//schemaName, schemaName + entities = new Map();//tableName, number of columns of this table + fields = new Map();//columnId, columnName + umlData = new Map();//schemaName, uml + joinConditions = new Map(); + readonly selects: WritableSignal<{ name: string, id: string }[]> = signal([]); + + private readonly _crud = inject(CrudService); + private readonly _leftSidebar = inject(LeftSidebarService); + private readonly _toast = inject(ToasterService); + private readonly _catalog = inject(CatalogService); + + constructor() { + this.webSocket = new WebSocket(); + this.initWebSocket(); + this.initSchema(); + } + + ngOnInit() { + this._leftSidebar.open(); + + this.initGraphicalQuerying(); + const sub = this._crud.onReconnection().subscribe( + b => { + if (b) { + + } } - } - }; + ); + this.subscriptions.add(sub); + } - const schema = []; - for (const s of catalog.getSchemaTree('views/graphical-querying/', true, 3, false, [DataModel.RELATIONAL])) { - const node = SidebarNode.fromJson(s, {allowRouting: false, autoActive: false, action: nodeAction}); - schema.push(node); - } + ngAfterViewInit() { + this.generateSQL(); + } - this._leftSidebar.setNodes(schema); - this._leftSidebar.open(); - }); - }); - } - - initGraphicalQuerying() { - const self = this; - - $('#selectBox').sortable({ - stop: function (e, ui) { - self.generateSQL(); - }, - cursor: 'grabbing', - containment: 'parent', - tolerance: 'pointer' - }); - } - - removeCol(colId: string) { - - const data = colId.split('.'); - const tableId = data[0] + '.' + data[1]; - - const tableCounter = this.entities.get(tableId); - if (tableCounter === 1) { - this.entities.delete(tableId); - } else { - this.entities.set(tableId, tableCounter - 1); + ngOnDestroy() { + this._leftSidebar.close(); + // this._leftSidebar.reset(); + this.subscriptions.unsubscribe(); + this.webSocket.close(); } - this.fields.delete(colId); - //$(`#selectBox [data-id="${colId}"]`).remove(); - this.selects.update(selects => selects.filter(s => s.id !== colId)); + initWebSocket() { + this.webSocket.onMessage().subscribe({ + next: res => { + const result = res; + this.result = result[0]; + this.loading.set(false); + }, error: err => { + this._toast.error('Unknown error on the server.'); + this.loading.set(false); + } + }); + } - this._leftSidebar.setInactive(colId); - this.generateJoinConditions(); // re-generate join conditions - this.generateSQL(); - } + initSchema() { + effect(() => { + const catalog = this._catalog.listener(); + untracked(() => { + const nodeAction = (tree, node, $event) => { + if (!node.isActive && node.isLeaf) { + this.addCol(node.data); + node.setIsActive(true, true); + } else if (node.isActive && node.isLeaf) { + + node.setIsActive(false, true); + this.removeCol(node.data.id); + + //deletes the selection if nothing is choosen + if (this.selectedColumn['column'].toString() === node.data.id) { + this.selectedCol([]); + } + } + }; + + const schema = []; + for (const s of catalog.getSchemaTree('views/graphical-querying/', true, 3, false, [DataModel.RELATIONAL])) { + const node = SidebarNode.fromJson(s, {allowRouting: false, autoActive: false, action: nodeAction}); + schema.push(node); + } - userInput(fSet: Object) { - if (fSet instanceof FilteredUserInput) { - this.filteredUserSet = fSet; + this._leftSidebar.setNodes(schema); + this._leftSidebar.open(); + }); + }); } - this.generateSQL(); - } - - checkboxMultiAlphabetic(col: string, checked: [string]) { - const checkbox = []; - checked.forEach(val => { - checkbox.push('\'' + val.replace('check', '') + '\''); - }); - if (checkbox.length > 1) { - return (this.connectWheres() + col + ' IN (' + checkbox + ')'); - } else { - return (this.connectWheres() + col + ' = ' + checkbox); + + initGraphicalQuerying() { + const self = this; + + $('#selectBox').sortable({ + stop: function (e, ui) { + self.generateSQL(); + }, + cursor: 'grabbing', + containment: 'parent', + tolerance: 'pointer' + }); } - } - - checkboxMultiNumeric(col: string, checked: [string]) { - const checkbox = []; - checked.forEach(val => { - checkbox.push(val.replace('check', '')); - }); - if (checkbox.length > 1) { - return (this.connectWheres() + col + ' IN (' + checkbox + ')'); - } else { - return (this.connectWheres() + col + ' = ' + checkbox); + removeCol(colId: string) { + + const data = colId.split('.'); + const tableId = data[0] + '.' + data[1]; + + const tableCounter = this.entities.get(tableId); + if (tableCounter === 1) { + this.entities.delete(tableId); + } else { + this.entities.set(tableId, tableCounter - 1); + } + this.fields.delete(colId); + + //$(`#selectBox [data-id="${colId}"]`).remove(); + this.selects.update(selects => selects.filter(s => s.id !== colId)); + + this._leftSidebar.setInactive(colId); + this.generateJoinConditions(); // re-generate join conditions + this.generateSQL(); } - } - minMax(col: string, minMax) { - return (this.connectWheres() + col + ' BETWEEN ' + minMax[0] + ' AND ' + minMax[1]); - } + userInput(fSet: Object) { + if (fSet instanceof FilteredUserInput) { + this.filteredUserSet = fSet; + } + this.generateSQL(); + } + + checkboxMultiAlphabetic(col: string, checked: [string]) { + const checkbox = []; + checked.forEach(val => { + checkbox.push('\'' + val.replace('check', '') + '\''); + }); + if (checkbox.length > 1) { + return (this.connectWheres() + col + ' IN (' + checkbox + ')'); + } else { + return (this.connectWheres() + col + ' = ' + checkbox); + } - startingWith(col: string, firstLetters: string) { - if (firstLetters.includes('*')) { - return (this.connectWheres() + col + ' LIKE ' + '\'' + firstLetters.replace(new RegExp('\\*', 'g'), '%') + '\''); - } else { - return (this.connectWheres() + col + ' LIKE ' + '\'' + firstLetters + '\''); } - } - - sorting(col: string, sort: string) { - return (this.connectOrderBy() + col + ' ' + sort); - } - - sortingAggregate(col: string, sort: string, aggregate: string) { - return (this.connectOrderBy() + aggregate + '(' + col + ') ' + sort); - } - - /** - * adds everything selected in the filterset to two arrays in order to add in the generated query - */ - processFilterSet() { - const whereSql = []; - const orderBySql = []; - const groupBy = []; - let flag = false; - const checkboxSQLAlphabetic = {}; - const checkboxSQLNumerical = {}; - if (this.filteredUserSet) { - Object.keys(this.filteredUserSet).forEach(col => { - const el = this.filteredUserSet[col]; - if (this.selectedColumn['column'].includes(col)) { - - if (el['minMax']) { - if (!(el['minMax'].toString() === el['startMinMax'].toString())) { - whereSql.push(this.minMax(this.wrapInParenthesis(col), el['minMax'])); - } - } + checkboxMultiNumeric(col: string, checked: [string]) { + const checkbox = []; + checked.forEach(val => { + checkbox.push(val.replace('check', '')); + }); + if (checkbox.length > 1) { + return (this.connectWheres() + col + ' IN (' + checkbox + ')'); + } else { + return (this.connectWheres() + col + ' = ' + checkbox); + } + } - if (el['startsWith']) { - whereSql.push(this.startingWith(this.wrapInParenthesis(col), el['startsWith'])); - } + minMax(col: string, minMax) { + return (this.connectWheres() + col + ' BETWEEN ' + minMax[0] + ' AND ' + minMax[1]); + } - if (el['sorting'] && (el['sorting'] === 'ASC' || el['sorting'] === 'DESC')) { - if (el['aggregate'] && !(el['aggregate'] === 'OFF')) { - orderBySql.push(this.sortingAggregate(this.wrapInParenthesis(col), el['sorting'], el['aggregate'])); - } else { - orderBySql.push(this.sorting(this.wrapInParenthesis(col), el['sorting'])); - } + startingWith(col: string, firstLetters: string) { + if (firstLetters.includes('*')) { + return (this.connectWheres() + col + ' LIKE ' + '\'' + firstLetters.replace(new RegExp('\\*', 'g'), '%') + '\''); + } else { + return (this.connectWheres() + col + ' LIKE ' + '\'' + firstLetters + '\''); + } - } + } - if (!el['aggregate'] || el['aggregate'] === 'OFF') { - if (!groupBy || !groupBy.length) { - groupBy.push('\nGROUP BY ' + this.wrapInParenthesis(col)); - } else { - groupBy.push(' , ' + this.wrapInParenthesis(col)); - } - } - - if (el['aggregate'] && !(el['aggregate'] === 'OFF')) { - flag = true; - } - - Object.keys(el).forEach(k => { - if (k.startsWith('check', 0) && el['columnType'] === 'alphabetic') { - //whereSql.push(this.checkboxAlphabetic(col, k, el[k])); - if (el[k]) { - if (checkboxSQLAlphabetic[col]) { - checkboxSQLAlphabetic[col].push(k); - } else { - checkboxSQLAlphabetic[col] = [k]; + sorting(col: string, sort: string) { + return (this.connectOrderBy() + col + ' ' + sort); + } + + sortingAggregate(col: string, sort: string, aggregate: string) { + return (this.connectOrderBy() + aggregate + '(' + col + ') ' + sort); + } + + /** + * adds everything selected in the filterset to two arrays in order to add in the generated query + */ + processFilterSet() { + const whereSql = []; + const orderBySql = []; + const groupBy = []; + let flag = false; + const checkboxSQLAlphabetic = {}; + const checkboxSQLNumerical = {}; + if (this.filteredUserSet) { + Object.keys(this.filteredUserSet).forEach(col => { + const el = this.filteredUserSet[col]; + if (this.selectedColumn['column'].includes(col)) { + + if (el['minMax']) { + if (!(el['minMax'].toString() === el['startMinMax'].toString())) { + whereSql.push(this.minMax(this.wrapInParenthesis(col), el['minMax'])); + } + } + + if (el['startsWith']) { + whereSql.push(this.startingWith(this.wrapInParenthesis(col), el['startsWith'])); + } + + if (el['sorting'] && (el['sorting'] === 'ASC' || el['sorting'] === 'DESC')) { + if (el['aggregate'] && !(el['aggregate'] === 'OFF')) { + orderBySql.push(this.sortingAggregate(this.wrapInParenthesis(col), el['sorting'], el['aggregate'])); + } else { + orderBySql.push(this.sorting(this.wrapInParenthesis(col), el['sorting'])); + } + + } + + if (!el['aggregate'] || el['aggregate'] === 'OFF') { + if (!groupBy || !groupBy.length) { + groupBy.push('\nGROUP BY ' + this.wrapInParenthesis(col)); + } else { + groupBy.push(' , ' + this.wrapInParenthesis(col)); + } + } + + if (el['aggregate'] && !(el['aggregate'] === 'OFF')) { + flag = true; + } + + Object.keys(el).forEach(k => { + if (k.startsWith('check', 0) && el['columnType'] === 'alphabetic') { + //whereSql.push(this.checkboxAlphabetic(col, k, el[k])); + if (el[k]) { + if (checkboxSQLAlphabetic[col]) { + checkboxSQLAlphabetic[col].push(k); + } else { + checkboxSQLAlphabetic[col] = [k]; + } + } + } + if (k.startsWith('check', 0) && el['columnType'] === 'numeric') { + //whereSql.push(this.checkboxNumeric(col, k, el[k])); + if (el[k]) { + if (checkboxSQLNumerical[col]) { + checkboxSQLNumerical[col].push(k); + } else { + checkboxSQLNumerical[col] = [k]; + } + } + } + + if (k.startsWith('check', 0) && el['columnType'] === 'temporal') { + //whereSql.push(this.checkboxNumeric(col, k, el[k])); + if (el[k]) { + if (checkboxSQLNumerical[col]) { + checkboxSQLNumerical[col].push(`'${k}'`); + } else { + checkboxSQLNumerical[col] = [`'${k}'`]; + } + } + } + + }); } - } + }); + if (checkboxSQLAlphabetic) { + Object.keys(checkboxSQLAlphabetic).forEach(col => { + whereSql.push(this.checkboxMultiAlphabetic(this.wrapInParenthesis(col), checkboxSQLAlphabetic[col])); + }); } - if (k.startsWith('check', 0) && el['columnType'] === 'numeric') { - //whereSql.push(this.checkboxNumeric(col, k, el[k])); - if (el[k]) { - if (checkboxSQLNumerical[col]) { - checkboxSQLNumerical[col].push(k); - } else { - checkboxSQLNumerical[col] = [k]; - } - } + if (checkboxSQLNumerical) { + Object.keys(checkboxSQLNumerical).forEach(col => { + whereSql.push(this.checkboxMultiNumeric(this.wrapInParenthesis(col), checkboxSQLNumerical[col])); + }); } - - if (k.startsWith('check', 0) && el['columnType'] === 'temporal') { - //whereSql.push(this.checkboxNumeric(col, k, el[k])); - if (el[k]) { - if (checkboxSQLNumerical[col]) { - checkboxSQLNumerical[col].push(`'${k}'`); - } else { - checkboxSQLNumerical[col] = [`'${k}'`]; - } - } + if (flag) { + return (whereSql.join('') + groupBy.join('') + orderBySql.join('')); + } else { + return (whereSql.join('') + orderBySql.join('')); } + } else { + return ''; + } + } + + wrapInParenthesis(k) { + return '"' + k.split('.').join('"."') + '"'; + } + + generateSQL() { + this.whereCounter = 0; + this.andCounter = 0; + this.orderByCounter = 0; + let filteredInfos = ''; + + if (this.fields.size === 0) { + this.editorGenerated.setCode(''); + return; + } - }); + let sql = 'SELECT '; + const cols = []; + const filterCols = []; + for (const select of this.selects()) { + const name = select.id; + let id = '"' + name.split('.').join('"."') + '"'; + if (this.filteredUserSet) { + Object.keys(this.filteredUserSet).forEach(col => { + const element = this.filteredUserSet[col]; + if (this.selectedColumn['column'].includes(col)) { + if (element['aggregate'] && !(element['aggregate'] === 'OFF')) { + if (col === name) { + id = element['aggregate'] + '(' + id + ')'; + } + } + } + }); + } + cols.push(id); + filterCols.push(name); } - }); - if (checkboxSQLAlphabetic) { - Object.keys(checkboxSQLAlphabetic).forEach(col => { - whereSql.push(this.checkboxMultiAlphabetic(this.wrapInParenthesis(col), checkboxSQLAlphabetic[col])); + sql += cols.join(', '); + sql += '\nFROM '; + const tables = []; + this.entities.forEach((v, k) => { + tables.push('"' + k.split('.').join('"."') + '"'); }); - } - if (checkboxSQLNumerical) { - Object.keys(checkboxSQLNumerical).forEach(col => { - whereSql.push(this.checkboxMultiNumeric(this.wrapInParenthesis(col), checkboxSQLNumerical[col])); + sql += tables.join(', '); + + //get join conditions + let counter = 0; + const joinConditions = []; + this.joinConditions.forEach((v, k) => { + if (v.active) { + counter++; + joinConditions.push(v.condition); + } }); - } - if (flag) { - return (whereSql.join('') + groupBy.join('') + orderBySql.join('')); - } else { - return (whereSql.join('') + orderBySql.join('')); - } - } else { - return ''; - } - } + if (counter > 0) { + sql += this.connectWheres() + joinConditions.join(' AND '); + } + + //to only show filters for selected tables/cols + this.selectedCol(filterCols); - wrapInParenthesis(k) { - return '"' + k.split('.').join('"."') + '"'; - } + filteredInfos = this.processFilterSet(); + let finalized: string; - generateSQL() { - this.whereCounter = 0; - this.andCounter = 0; - this.orderByCounter = 0; - let filteredInfos = ''; + finalized = sql + filteredInfos; - if (this.fields.size === 0) { - this.editorGenerated.setCode(''); - return; + this.generatedSQL = finalized; + this.editorGenerated.setCode(finalized); } - let sql = 'SELECT '; - const cols = []; - const filterCols = []; - for (const select of this.selects()) { - const name = select.id; - let id = '"' + name.split('.').join('"."') + '"'; - if (this.filteredUserSet) { - Object.keys(this.filteredUserSet).forEach(col => { - const element = this.filteredUserSet[col]; - if (this.selectedColumn['column'].includes(col)) { - if (element['aggregate'] && !(element['aggregate'] === 'OFF')) { - if (col === name) { - id = element['aggregate'] + '(' + id + ')'; - } - } - } - }); - } - cols.push(id); - filterCols.push(name); + selectedCol(col: {}) { + this.selectedColumn = { + column: col + }; } - sql += cols.join(', '); - sql += '\nFROM '; - const tables = []; - this.entities.forEach((v, k) => { - tables.push('"' + k.split('.').join('"."') + '"'); - }); - sql += tables.join(', '); - - //get join conditions - let counter = 0; - const joinConditions = []; - this.joinConditions.forEach((v, k) => { - if (v.active) { - counter++; - joinConditions.push(v.condition); - } - }); - if (counter > 0) { - sql += this.connectWheres() + joinConditions.join(' AND '); + + /* + * to select correct keyword ORDER BY Comma + */ + connectOrderBy() { + if (this.orderByCounter === 0) { + this.orderByCounter += 1; + return '\nORDER BY '; + } else { + return ', '; + } } - //to only show filters for selected tables/cols - this.selectedCol(filterCols); - - filteredInfos = this.processFilterSet(); - let finalized: string; - - finalized = sql + filteredInfos; - - this.generatedSQL = finalized; - this.editorGenerated.setCode(finalized); - } - - selectedCol(col: {}) { - this.selectedColumn = { - column: col - }; - } - - /* - * to select correct keyword ORDER BY Comma - */ - connectOrderBy() { - if (this.orderByCounter === 0) { - this.orderByCounter += 1; - return '\nORDER BY '; - } else { - return ', '; + /* + * to select correct keyword WHERE AND + */ + connectWheres() { + if (this.whereCounter === 0) { + this.whereCounter += 1; + return '\nWHERE '; + } else { + return '\nAND '; + } } - } - - /* - * to select correct keyword WHERE AND - */ - connectWheres() { - if (this.whereCounter === 0) { - this.whereCounter += 1; - return '\nWHERE '; - } else { - return '\nAND '; + + executeQuery() { + this.loading.set(true); + const code = this.editorGenerated.getCode(); + if (!this._crud.anyQuery(this.webSocket, new QueryRequest(code, false, true, 'sql', null))) { + this.loading.set(false); + this.result = new RelationalResult('Could not establish a connection with the server.'); + } } - } - - executeQuery() { - this.loading.set(true); - const code = this.editorGenerated.getCode(); - if (!this._crud.anyQuery(this.webSocket, new QueryRequest(code, false, true, 'sql', null))) { - this.loading.set(false); - this.result = new RelationalResult('Could not establish a connection with the server.'); + + + addCol(data) { + const treeElement = new SidebarNode(data.id, data.name, null, null); + + if (this.fields.get(treeElement.id)) { + //skip if already in select list + return; + } else { + this.fields.set(treeElement.id, treeElement); + } + + if (this.entities.get(treeElement.getEntity())) { + this.entities.set(treeElement.getEntity(), this.entities.get(treeElement.getEntity()) + 1); + } else { + this.entities.set(treeElement.getEntity(), 1); + } + + if (!this.namespaces.get(treeElement.getNamespace())) { + this.namespaces.set(treeElement.getNamespace(), treeElement.getNamespace()); + this._crud.getUml(new EditTableRequest(this._catalog.getNamespaceFromName(treeElement.getNamespace()).id)).subscribe({ + next: (uml: Uml) => { + this.umlData.set(treeElement.getNamespace(), uml); + this.generateJoinConditions(); + } + , + error: err => { + this._toast.error('Could not get foreign keys of the schema ' + treeElement.getNamespace()); + } + }); + } else { + this.generateJoinConditions(); + } + this.selects.update(selects => [...selects, {name: treeElement.getField(), id: treeElement.id}]); + + this.generateSQL(); } - } + toggleCondition(con: JoinCondition) { + con.toggle(); + this.generateSQL(); + } - addCol(data) { - const treeElement = new SidebarNode(data.id, data.name, null, null); + /** + * Generate the needed join conditions + */ + generateJoinConditions() { + this.joinConditions.clear(); + this.umlData.forEach((uml, key) => { + uml.foreignKeys.forEach((fk: ForeignKey, key2) => { + const fkId = fk.sourceSchema + '.' + fk.sourceTable + '.' + fk.sourceColumn; + const pkId = fk.targetSchema + '.' + fk.targetTable + '.' + fk.targetColumn; + if (this.entities.get(fk.targetSchema + '.' + fk.targetTable) !== undefined && + this.entities.get(fk.sourceSchema + '.' + fk.sourceTable) !== undefined) { + this.joinConditions.set(fkId + pkId, new JoinCondition(this.wrapInParenthesis(fkId) + ' = ' + this.wrapInParenthesis(pkId))); + } + }); + }); + } - if (this.fields.get(treeElement.id)) { - //skip if already in select list - return; - } else { - this.fields.set(treeElement.id, treeElement); + createView(info: ViewInformation) { + this.editorGenerated.setCode(info.fullQuery); } - if (this.entities.get(treeElement.getEntity())) { - this.entities.set(treeElement.getEntity(), this.entities.get(treeElement.getEntity()) + 1); - } else { - this.entities.set(treeElement.getEntity(), 1); + executeView(info: ViewInformation) { + this.editorGenerated.setCode(info.fullQuery); + this.executeQuery(); } - if (!this.namespaces.get(treeElement.getNamespace())) { - this.namespaces.set(treeElement.getNamespace(), treeElement.getNamespace()); - this._crud.getUml(new EditTableRequest(this._catalog.getNamespaceFromName(treeElement.getNamespace()).id)).subscribe({ - next: (uml: Uml) => { - this.umlData.set(treeElement.getNamespace(), uml); - this.generateJoinConditions(); - } - , - error: err => { - this._toast.error('Could not get foreign keys of the schema ' + treeElement.getNamespace()); - } - }); - } else { - this.generateJoinConditions(); + removeSelect(field: { name: string; id: string }) { + this.selects.update(selects => selects.filter(s => s.id !== field.id)); + this.removeCol(field.id); } - this.selects.update(selects => [...selects, {name: treeElement.getField(), id: treeElement.id}]); - - this.generateSQL(); - } - - toggleCondition(con: JoinCondition) { - con.toggle(); - this.generateSQL(); - } - - /** - * Generate the needed join conditions - */ - generateJoinConditions() { - this.joinConditions.clear(); - this.umlData.forEach((uml, key) => { - uml.foreignKeys.forEach((fk: ForeignKey, key2) => { - const fkId = fk.sourceSchema + '.' + fk.sourceTable + '.' + fk.sourceColumn; - const pkId = fk.targetSchema + '.' + fk.targetTable + '.' + fk.targetColumn; - if (this.entities.get(fk.targetSchema + '.' + fk.targetTable) !== undefined && - this.entities.get(fk.sourceSchema + '.' + fk.sourceTable) !== undefined) { - this.joinConditions.set(fkId + pkId, new JoinCondition(this.wrapInParenthesis(fkId) + ' = ' + this.wrapInParenthesis(pkId))); - } - }); - }); - } - - createView(info: ViewInformation) { - this.editorGenerated.setCode(info.fullQuery); - } - - executeView(info: ViewInformation) { - this.editorGenerated.setCode(info.fullQuery); - this.executeQuery(); - } - - removeSelect(field: { name: string; id: string }) { - this.selects.update(selects => selects.filter(s => s.id !== field.id)); - this.removeCol(field.id); - } } class JoinCondition { - condition: string; - active: boolean; + condition: string; + active: boolean; - constructor(condition: string) { - this.condition = condition; - this.active = true; - } + constructor(condition: string) { + this.condition = condition; + this.active = true; + } - toggle() { - this.active = !this.active; - } + toggle() { + this.active = !this.active; + } } diff --git a/src/app/views/querying/graphical-querying/refinement-options/refinement-options.component.ts b/src/app/views/querying/graphical-querying/refinement-options/refinement-options.component.ts index e9abebe3..10f717b7 100644 --- a/src/app/views/querying/graphical-querying/refinement-options/refinement-options.component.ts +++ b/src/app/views/querying/graphical-querying/refinement-options/refinement-options.component.ts @@ -5,266 +5,266 @@ import {CrudService} from '../../../../services/crud.service'; import {ToasterService} from '../../../../components/toast-exposer/toaster.service'; @Component({ - selector: 'app-refinement-options', - templateUrl: './refinement-options.component.html', - styleUrls: ['./refinement-options.component.scss'] + selector: 'app-refinement-options', + templateUrl: './refinement-options.component.html', + styleUrls: ['./refinement-options.component.scss'] }) export class RefinementOptionsComponent implements OnInit { - private readonly _crud = inject(CrudService); - private readonly _toast = inject(ToasterService); + private readonly _crud = inject(CrudService); + private readonly _toast = inject(ToasterService); - activeHeaders: {}; - statisticSet: StatisticSet; - filteredUserInput: FilteredUserInput; - stylingSet: {}; - _choosenTables = {}; - active: String; - @Output() filteredUserInputChange = new EventEmitter(); + activeHeaders: {}; + statisticSet: StatisticSet; + filteredUserInput: FilteredUserInput; + stylingSet: {}; + _choosenTables = {}; + active: String; + @Output() filteredUserInputChange = new EventEmitter(); - constructor() { - } + constructor() { + } - ngOnInit() { - this.getStatistic(); - } + ngOnInit() { + this.getStatistic(); + } - /** - * to only show the filter options for the chosen Tables - */ - @Input() - set choosenTables(choosenTables: {}) { - const oldChoosen = this._choosenTables; - this._choosenTables = choosenTables; + /** + * to only show the filter options for the chosen Tables + */ + @Input() + set choosenTables(choosenTables: {}) { + const oldChoosen = this._choosenTables; + this._choosenTables = choosenTables; - if (choosenTables && ((oldChoosen === null) || JSON.stringify(oldChoosen['column']) !== JSON.stringify(choosenTables['column']))) { - this.resetHeader(choosenTables); + if (choosenTables && ((oldChoosen === null) || JSON.stringify(oldChoosen['column']) !== JSON.stringify(choosenTables['column']))) { + this.resetHeader(choosenTables); + } } - } - resetHeader(choosenTables) { - if (!this.stylingSet || !choosenTables) { - return; - } - this.activeHeaders = {}; - Object.keys(this.stylingSet).forEach(s => { - let i = 0; - Object.keys(this.stylingSet[s]).forEach(t => { - if (choosenTables !== null && this.includesTable(choosenTables['column'], t) && i === 0) { - this.activeHeaders[s] = t; - i++; + resetHeader(choosenTables) { + if (!this.stylingSet || !choosenTables) { + return; } - }); - }); - } + this.activeHeaders = {}; + Object.keys(this.stylingSet).forEach(s => { + let i = 0; + Object.keys(this.stylingSet[s]).forEach(t => { + if (choosenTables !== null && this.includesTable(choosenTables['column'], t) && i === 0) { + this.activeHeaders[s] = t; + i++; + } + }); + }); + } - /** - * get filter statistics form data sets - */ - getStatistic() { - this._crud.allStatistics(new StatisticRequest()).subscribe({ - next: res => { - this.prepareStatisticSet(res); - this.stylingSet = res; - }, error: err => { - this._toast.error('Unknown error on the server.'); - } - }); - } + /** + * get filter statistics form data sets + */ + getStatistic() { + this._crud.allStatistics(new StatisticRequest()).subscribe({ + next: res => { + this.prepareStatisticSet(res); + this.stylingSet = res; + }, error: err => { + this._toast.error('Unknown error on the server.'); + } + }); + } - /** - * Checks if a column value is included in the chosen table - */ - includes(o: string[], name: string) { - return o.includes(name); - } + /** + * Checks if a column value is included in the chosen table + */ + includes(o: string[], name: string) { + return o.includes(name); + } - /** - * Checks if a schema is included in the chosen tables - */ - includesSchema(o, name: string) { - const schema = []; - if (!o || !o.length) { - return false; + /** + * Checks if a schema is included in the chosen tables + */ + includesSchema(o, name: string) { + const schema = []; + if (!o || !o.length) { + return false; + } + o.forEach(s => { + schema.push(s.split('.', 1)[0]); + }); + return this.includes(schema, name); } - o.forEach(s => { - schema.push(s.split('.', 1)[0]); - }); - return this.includes(schema, name); - } - /** - * Checks if a table is chosen - */ - includesTable(o, name: string) { - const schema = []; - if (!o || !o.length) { - return false; + /** + * Checks if a table is chosen + */ + includesTable(o, name: string) { + const schema = []; + if (!o || !o.length) { + return false; + } + o.forEach(s => { + schema.push(s.split('.')[1]); + }); + return this.includes(schema, name); } - o.forEach(s => { - schema.push(s.split('.')[1]); - }); - return this.includes(schema, name); - } - /** - * after changing the filter values emiting changes for graphical-querying component - */ - changeUserInput() { - const transmitSet = new FilteredUserInput(); - this._choosenTables['column'].forEach(el => { - if (this.filteredUserInput.hasOwnProperty(el)) { - if (this.statisticSet[el]['columnType'] === 'temporal') { - const {getLabel, getNumber, step} = this.getTemporal(this.statisticSet[el]['temporalType']); - const fel = this.filteredUserInput[el]; + /** + * after changing the filter values emiting changes for graphical-querying component + */ + changeUserInput() { + const transmitSet = new FilteredUserInput(); + this._choosenTables['column'].forEach(el => { + if (this.filteredUserInput.hasOwnProperty(el)) { + if (this.statisticSet[el]['columnType'] === 'temporal') { + const {getLabel, getNumber, step} = this.getTemporal(this.statisticSet[el]['temporalType']); + const fel = this.filteredUserInput[el]; - transmitSet[el] = { - ...fel, - minMax: fel['minMax'].map(getLabel).map(d => `'${d}'`), - startMinMax: fel['startMinMax'].map(getLabel).map(d => `'${d}'`) - }; - } else { - transmitSet[el] = this.filteredUserInput[el]; - } + transmitSet[el] = { + ...fel, + minMax: fel['minMax'].map(getLabel).map(d => `'${d}'`), + startMinMax: fel['startMinMax'].map(getLabel).map(d => `'${d}'`) + }; + } else { + transmitSet[el] = this.filteredUserInput[el]; + } + + } + }); + this.filteredUserInputChange.emit(transmitSet); + } - } - }); - this.filteredUserInputChange.emit(transmitSet); - } + /** + * initializing filteredUserInput for dynamic binding + */ + processUserInput(stat: StatisticSet) { + this.filteredUserInput = new FilteredUserInput(); + Object.keys(stat).forEach(key => { + this.filteredUserInput[key] = {}; + const el = this.statisticSet[key]; + if (el['min'] && el['max']) { + this.filteredUserInput[key]['minMax'] = [el['min'], el['max']]; + this.filteredUserInput[key]['startMinMax'] = [el['min'], el['max']]; + } + this.filteredUserInput[key]['sorting'] = 'OFF'; + this.filteredUserInput[key]['aggregate'] = 'OFF'; + this.filteredUserInput[key]['columnType'] = el['columnType']; - /** - * initializing filteredUserInput for dynamic binding - */ - processUserInput(stat: StatisticSet) { - this.filteredUserInput = new FilteredUserInput(); - Object.keys(stat).forEach(key => { - this.filteredUserInput[key] = {}; - const el = this.statisticSet[key]; - if (el['min'] && el['max']) { - this.filteredUserInput[key]['minMax'] = [el['min'], el['max']]; - this.filteredUserInput[key]['startMinMax'] = [el['min'], el['max']]; - } - this.filteredUserInput[key]['sorting'] = 'OFF'; - this.filteredUserInput[key]['aggregate'] = 'OFF'; - this.filteredUserInput[key]['columnType'] = el['columnType']; + }); + } - }); - } + /** + * prepares the statisticSet from Server + */ + prepareStatisticSet(res: StatisticSet) { + this.statisticSet = new StatisticSet(); + Object.keys(res).forEach(keySchema => { + Object.keys(res[keySchema]).forEach(keyTable => { + Object.keys(res[keySchema][keyTable]).forEach(key => { + this.statisticSet[res[keySchema][keyTable][key]['qualifiedColumnName']] = res[keySchema][keyTable][key]; + }); + }); + }); + this.processStatistics(this.statisticSet); + this.processUserInput(this.statisticSet); + } - /** - * prepares the statisticSet from Server - */ - prepareStatisticSet(res: StatisticSet) { - this.statisticSet = new StatisticSet(); - Object.keys(res).forEach(keySchema => { - Object.keys(res[keySchema]).forEach(keyTable => { - Object.keys(res[keySchema][keyTable]).forEach(key => { - this.statisticSet[res[keySchema][keyTable][key]['qualifiedColumnName']] = res[keySchema][keyTable][key]; + /** + * add additional information to the statistics for the components + */ + processStatistics(stat: StatisticSet) { + Object.keys(stat).forEach(key => { + const el = stat[key]; + if (el['min'] && el['max']) { + if (this.statisticSet[key]['type']) { + this.statisticSet[key]['type'].push('range'); + } else { + this.statisticSet[key]['type'] = ['range']; + } + if (el['columnType'] === 'temporal') { + let {getLabel, getNumber, step} = this.getTemporal(el['temporalType']); + el['min'] = getNumber(el['min']); + el['max'] = getNumber(el['max']); + this.statisticSet[key]['options'] = { + floor: el['min'], + ceil: el['max'], + translate: getLabel, + step, + uniqueValues: [] + }; + } else { + this.statisticSet[key]['options'] = { + floor: el['min'], + ceil: el['max'], + step: 1, + uniqueValues: [] + }; + } + } + if (this.statisticSet[key]['uniqueValues']) { + if (this.statisticSet[key]['type']) { + this.statisticSet[key]['type'].push('uniqueValues'); + } else { + this.statisticSet[key]['type'] = ['uniqueValues']; + } + } }); - }); - }); - this.processStatistics(this.statisticSet); - this.processUserInput(this.statisticSet); - } + } - /** - * add additional information to the statistics for the components - */ - processStatistics(stat: StatisticSet) { - Object.keys(stat).forEach(key => { - const el = stat[key]; - if (el['min'] && el['max']) { - if (this.statisticSet[key]['type']) { - this.statisticSet[key]['type'].push('range'); - } else { - this.statisticSet[key]['type'] = ['range']; - } - if (el['columnType'] === 'temporal') { - let {getLabel, getNumber, step} = this.getTemporal(el['temporalType']); - el['min'] = getNumber(el['min']); - el['max'] = getNumber(el['max']); - this.statisticSet[key]['options'] = { - floor: el['min'], - ceil: el['max'], - translate: getLabel, - step, - uniqueValues: [] - }; - } else { - this.statisticSet[key]['options'] = { - floor: el['min'], - ceil: el['max'], - step: 1, - uniqueValues: [] - }; - } - } - if (this.statisticSet[key]['uniqueValues']) { - if (this.statisticSet[key]['type']) { - this.statisticSet[key]['type'].push('uniqueValues'); - } else { - this.statisticSet[key]['type'] = ['uniqueValues']; + filterHeaders(stylingSet: {}, choosenTable: {}, schema: string) { + if (!choosenTable && !choosenTable['column']) { + return []; } - } - }); - } - - filterHeaders(stylingSet: {}, choosenTable: {}, schema: string) { - if (!choosenTable && !choosenTable['column']) { - return []; + const filtered = {}; + Object.keys(stylingSet).forEach((table, i) => { + if (this.includesTable(choosenTable['column'], table)) { + filtered[table] = stylingSet[table]; + } + }); + return filtered; } - const filtered = {}; - Object.keys(stylingSet).forEach((table, i) => { - if (this.includesTable(choosenTable['column'], table)) { - filtered[table] = stylingSet[table]; - } - }); - return filtered; - } - addToHeader(schema: string, table: string) { - if (!this.activeHeaders) { - this.activeHeaders = {}; + addToHeader(schema: string, table: string) { + if (!this.activeHeaders) { + this.activeHeaders = {}; + } + this.activeHeaders[schema] = table; } - this.activeHeaders[schema] = table; - } - hasMultipleSchemas() { - return Object.keys(this.stylingSet).length > 1; - } + hasMultipleSchemas() { + return Object.keys(this.stylingSet).length > 1; + } - filterSet(inputSet: {}) { - if (!inputSet || !this._choosenTables || !this._choosenTables['column']) { - return {}; + filterSet(inputSet: {}) { + if (!inputSet || !this._choosenTables || !this._choosenTables['column']) { + return {}; + } + const filtered = {}; + Object.keys(inputSet).forEach(e => { + if (this.includes(this._choosenTables['column'], inputSet[e]['qualifiedColumnName'])) { + filtered[e] = inputSet[e]; + } + }); + return filtered; } - const filtered = {}; - Object.keys(inputSet).forEach(e => { - if (this.includes(this._choosenTables['column'], inputSet[e]['qualifiedColumnName'])) { - filtered[e] = inputSet[e]; - } - }); - return filtered; - } - getTemporal(temporalType: string) { - let getNumber, getLabel, step; - if (temporalType == 'TIME') { - getNumber = (ts: string) => new Date('2020-01-01 ' + ts).getTime() / 1000; - getLabel = (time: number, type: any) => new Date(time * 1000).toISOString().slice(11, 19); - step = 1; // 1000ms = 1s - } else if (temporalType == 'DATE') { - getNumber = (ts: string) => { - console.log('date slider,getNumber', ts); - return new Date(ts).getTime() / 1000; - }; - getLabel = (time: number, type: any) => time ? new Date(time * 1000).toISOString().slice(0, 10) : ''; - step = 86400; // 1000ms * 60s * 60min * 24hrs = 1day - } else { - getNumber = (ts: string) => new Date(ts).getTime() / 1000; - getLabel = (time: number, type: any) => new Date(time * 1000).toISOString().slice(0, 19).replace('T', ' '); - step = 1; + getTemporal(temporalType: string) { + let getNumber, getLabel, step; + if (temporalType == 'TIME') { + getNumber = (ts: string) => new Date('2020-01-01 ' + ts).getTime() / 1000; + getLabel = (time: number, type: any) => new Date(time * 1000).toISOString().slice(11, 19); + step = 1; // 1000ms = 1s + } else if (temporalType == 'DATE') { + getNumber = (ts: string) => { + console.log('date slider,getNumber', ts); + return new Date(ts).getTime() / 1000; + }; + getLabel = (time: number, type: any) => time ? new Date(time * 1000).toISOString().slice(0, 10) : ''; + step = 86400; // 1000ms * 60s * 60min * 24hrs = 1day + } else { + getNumber = (ts: string) => new Date(ts).getTime() / 1000; + getLabel = (time: number, type: any) => new Date(time * 1000).toISOString().slice(0, 19).replace('T', ' '); + step = 1; + } + return {getLabel, getNumber, step}; } - return {getLabel, getNumber, step}; - } } diff --git a/src/app/views/querying/querying.component.ts b/src/app/views/querying/querying.component.ts index 70f4f446..8ea7bddd 100644 --- a/src/app/views/querying/querying.component.ts +++ b/src/app/views/querying/querying.component.ts @@ -2,27 +2,27 @@ import {Component, inject, OnInit} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; @Component({ - selector: 'app-querying', - templateUrl: './querying.component.html', - styleUrls: ['./querying.component.scss'], + selector: 'app-querying', + templateUrl: './querying.component.html', + styleUrls: ['./querying.component.scss'], }) export class QueryingComponent implements OnInit { - private readonly _route = inject(ActivatedRoute); + private readonly _route = inject(ActivatedRoute); - public route = 'console'; + public route = 'console'; - constructor() { - } + constructor() { + } - ngOnInit() { - this.getRoute(); - } + ngOnInit() { + this.getRoute(); + } - getRoute() { - this.route = this._route.snapshot.paramMap.get('route'); - this._route.params.subscribe(params => { - this.route = params['route']; - }); - } + getRoute() { + this.route = this._route.snapshot.paramMap.get('route'); + this._route.params.subscribe(params => { + this.route = params['route']; + }); + } } diff --git a/src/app/views/schema-editing/document-edit-collection/document-edit-collection.component.ts b/src/app/views/schema-editing/document-edit-collection/document-edit-collection.component.ts index f6f3aa3f..2b3679ce 100644 --- a/src/app/views/schema-editing/document-edit-collection/document-edit-collection.component.ts +++ b/src/app/views/schema-editing/document-edit-collection/document-edit-collection.component.ts @@ -10,128 +10,134 @@ import {AdapterModel} from '../../adapters/adapter.model'; import {ModalDirective} from 'ngx-bootstrap/modal'; import {Subscription} from 'rxjs'; import {CatalogService} from '../../../services/catalog.service'; -import {AllocationEntityModel, AllocationPartitionModel, AllocationPlacementModel, NamespaceModel, TableModel} from '../../../models/catalog.model'; +import { + AllocationEntityModel, + AllocationPartitionModel, + AllocationPlacementModel, + NamespaceModel, + TableModel +} from '../../../models/catalog.model'; @Component({ - selector: 'app-document-edit-collection', - templateUrl: './document-edit-collection.component.html', - styleUrls: ['./document-edit-collection.component.scss'] + selector: 'app-document-edit-collection', + templateUrl: './document-edit-collection.component.html', + styleUrls: ['./document-edit-collection.component.scss'] }) export class DocumentEditCollectionComponent implements OnInit, OnDestroy { - public readonly _crud = inject(CrudService); - public readonly _types = inject(DbmsTypesService); - public readonly _catalog = inject(CatalogService); - private readonly _toast = inject(ToasterService); + public readonly _crud = inject(CrudService); + public readonly _types = inject(DbmsTypesService); + public readonly _catalog = inject(CatalogService); + private readonly _toast = inject(ToasterService); - constructor() { + constructor() { - } - - @Input() - readonly entity: Signal; - @Input() - readonly namespace: Signal; - @Input() - readonly currentRoute: Signal; + } - @Input() - readonly placements: Signal; - @Input() - readonly partitions: Signal; - @Input() - readonly allocations: Signal; - @Input() - readonly stores: Signal; - @Input() - readonly addableStores: Signal; + @Input() + readonly entity: Signal; + @Input() + readonly namespace: Signal; + @Input() + readonly currentRoute: Signal; + @Input() + readonly placements: Signal; + @Input() + readonly partitions: Signal; + @Input() + readonly allocations: Signal; + @Input() + readonly stores: Signal; + @Input() + readonly addableStores: Signal; - types: PolyType[] = []; - editColumn = -1; - createColumn = new UiColumnDefinition(-1, '', false, true, 'text', '', null, null, null); - confirm = -1; - updateColumn = new UntypedFormGroup({name: new UntypedFormControl('')}); + types: PolyType[] = []; + editColumn = -1; + createColumn = new UiColumnDefinition(-1, '', false, true, 'text', '', null, null, null); + confirm = -1; + updateColumn = new UntypedFormGroup({name: new UntypedFormControl('')}); - //data placement handling - selectedStore: AdapterModel; - placementMethod: Method; - isAddingPlacement = false; + //data placement handling - subscriptions = new Subscription(); + selectedStore: AdapterModel; + placementMethod: Method; + isAddingPlacement = false; - @ViewChild('placementModal', {static: false}) public placementModal: ModalDirective; - @ViewChild('partitioningModal', {static: false}) public partitioningModal: ModalDirective; - @ViewChild('partitionFunctionModal', {static: false}) public partitionFunctionModal: ModalDirective; + subscriptions = new Subscription(); - protected readonly Method = Method; + @ViewChild('placementModal', {static: false}) public placementModal: ModalDirective; + @ViewChild('partitioningModal', {static: false}) public partitioningModal: ModalDirective; + @ViewChild('partitionFunctionModal', {static: false}) public partitionFunctionModal: ModalDirective; - ngOnInit() { + protected readonly Method = Method; - this.getFixedFields(); - } + ngOnInit() { - ngOnDestroy() { - $(document).off('click'); - this.subscriptions.unsubscribe(); - } + this.getFixedFields(); + } - //see https://medium.com/claritydesignsystem/1b66d45b3e3d - @HostListener('window:click', ['$event.target']) - onClick(targetElement: string) { - const self = this; - if ($(targetElement).parents('.editing').length === 0) { - self.editColumn = -1; + ngOnDestroy() { + $(document).off('click'); + this.subscriptions.unsubscribe(); } - } + //see https://medium.com/claritydesignsystem/1b66d45b3e3d + @HostListener('window:click', ['$event.target']) + onClick(targetElement: string) { + const self = this; + if ($(targetElement).parents('.editing').length === 0) { + self.editColumn = -1; + } + } - getFixedFields() { - return []; - } - modifyPlacement(method: Method, storeId: number = null) { - this.placementMethod = method; - if (storeId != null) { - this.selectedStore = this._catalog.getAdapter(storeId); - } - if (!this.stores) { - return; + getFixedFields() { + return []; } - this.isAddingPlacement = true; - this._crud.addDropCollectionPlacement(this.namespace().name, this.entity().name, this.selectedStore.name, this.placementMethod).subscribe({ - next: (result: RelationalResult) => { - if (result.error) { - this._toast.exception(result); - } else { - if (this.placementMethod === Method.ADD) { - this._toast.success('Added placement on store ' + this.selectedStore.name, result.query, 'Added placement'); - } else if (this.placementMethod === Method.MODIFY) { - this._toast.success('Modified placement on store ' + this.selectedStore.name, result.query, 'Modified placement'); - } - //this._catalog.updateIfNecessary(); + + modifyPlacement(method: Method, storeId: number = null) { + this.placementMethod = method; + if (storeId != null) { + this.selectedStore = this._catalog.getAdapter(storeId); + } + if (!this.stores) { + return; + } + this.isAddingPlacement = true; + this._crud.addDropCollectionPlacement(this.namespace().name, this.entity().name, this.selectedStore.name, this.placementMethod).subscribe({ + next: (result: RelationalResult) => { + if (result.error) { + this._toast.exception(result); + } else { + if (this.placementMethod === Method.ADD) { + this._toast.success('Added placement on store ' + this.selectedStore.name, result.query, 'Added placement'); + } else if (this.placementMethod === Method.MODIFY) { + this._toast.success('Modified placement on store ' + this.selectedStore.name, result.query, 'Modified placement'); + } + //this._catalog.updateIfNecessary(); + } + this.selectedStore = null; + }, error: err => { + this._toast.error('Could not ' + this.placementMethod.toLowerCase() + ' placement on store ' + this.selectedStore.name); + } } - this.selectedStore = null; - }, error: err => { - this._toast.error('Could not ' + this.placementMethod.toLowerCase() + ' placement on store ' + this.selectedStore.name); - } + ).add(() => { + this.isAddingPlacement = false; + }); + } + + + validate(defaultValue) { + if (defaultValue === null) { + return ''; + } else if (isNaN(defaultValue) || defaultValue === '') { + return 'is-invalid'; + } else { + return 'is-valid'; } - ).add(() => { - this.isAddingPlacement = false; - }); - } - - - validate(defaultValue) { - if (defaultValue === null) { - return ''; - } else if (isNaN(defaultValue) || defaultValue === '') { - return 'is-invalid'; - } else { - return 'is-valid'; } - } } diff --git a/src/app/views/schema-editing/document-edit-collections/document-edit-collections.component.ts b/src/app/views/schema-editing/document-edit-collections/document-edit-collections.component.ts index 9ab7b4de..19791b92 100644 --- a/src/app/views/schema-editing/document-edit-collections/document-edit-collections.component.ts +++ b/src/app/views/schema-editing/document-edit-collections/document-edit-collections.component.ts @@ -1,4 +1,16 @@ -import {Component, computed, ElementRef, inject, Input, OnDestroy, OnInit, QueryList, Renderer2, Signal, ViewChildren} from '@angular/core'; +import { + Component, + computed, + ElementRef, + inject, + Input, + OnDestroy, + OnInit, + QueryList, + Renderer2, + Signal, + ViewChildren +} from '@angular/core'; import {CrudService} from '../../../services/crud.service'; import {Method, QueryRequest} from '../../../models/ui-request.model'; import {Router} from '@angular/router'; @@ -8,270 +20,278 @@ import {LeftSidebarService} from '../../../components/left-sidebar/left-sidebar. import {DbmsTypesService} from '../../../services/dbms-types.service'; import {Subscription} from 'rxjs'; import {DbTable} from '../../uml/uml.model'; -import {AllocationEntityModel, AllocationPartitionModel, AllocationPlacementModel, CollectionModel, EntityType, NamespaceModel, TableModel} from '../../../models/catalog.model'; +import { + AllocationEntityModel, + AllocationPartitionModel, + AllocationPlacementModel, + CollectionModel, + EntityType, + NamespaceModel, + TableModel +} from '../../../models/catalog.model'; import {CatalogService} from '../../../services/catalog.service'; import {AdapterModel} from '../../adapters/adapter.model'; @Component({ - selector: 'app-document-edit-collections', - templateUrl: './document-edit-collections.component.html', - styleUrls: ['./document-edit-collections.component.scss'] + selector: 'app-document-edit-collections', + templateUrl: './document-edit-collections.component.html', + styleUrls: ['./document-edit-collections.component.scss'] }) export class DocumentEditCollectionsComponent implements OnInit, OnDestroy { - public readonly _crud = inject(CrudService); - public readonly _types = inject(DbmsTypesService); - public readonly _catalog = inject(CatalogService); - private readonly _toast = inject(ToasterService); - private readonly _router = inject(Router); - private readonly _leftSidebar = inject(LeftSidebarService); - private readonly _render = inject(Renderer2); - - constructor() { - this._render.listen('document', 'click', (e: Event) => { - if (!this.inputGroup || this.inputGroup.length === 0) { - return; - } - if (this.editOpen && !this.inputGroup.get(0).nativeElement.contains(e.target)) { - this.collections().map(t => { - t.editing = false; - return t; + public readonly _crud = inject(CrudService); + public readonly _types = inject(DbmsTypesService); + public readonly _catalog = inject(CatalogService); + private readonly _toast = inject(ToasterService); + private readonly _router = inject(Router); + private readonly _leftSidebar = inject(LeftSidebarService); + private readonly _render = inject(Renderer2); + + constructor() { + this._render.listen('document', 'click', (e: Event) => { + if (!this.inputGroup || this.inputGroup.length === 0) { + return; + } + if (this.editOpen && !this.inputGroup.get(0).nativeElement.contains(e.target)) { + this.collections().map(t => { + t.editing = false; + return t; + }); + this.editOpen = false; + } else { + this.editOpen = true; + } + }); + + this.collections = computed(() => { + const catalog = this._catalog.listener(); + if (!this.namespace) { + return; + } + const namespace = this.namespace(); + if (!namespace) { + return; + } + const collections = this._catalog.getEntities(namespace.id); + return collections.map(c => Collection.fromModel(c)).sort((a, b) => a.name.localeCompare(b.name)); + }); + + this.stores = computed(() => { + const catalog = this._catalog.listener(); + return this._catalog.getStores(); }); - this.editOpen = false; - } else { - this.editOpen = true; - } - }); - - this.collections = computed(() => { - const catalog = this._catalog.listener(); - if (!this.namespace) { - return; - } - const namespace = this.namespace(); - if (!namespace) { - return; - } - const collections = this._catalog.getEntities(namespace.id); - return collections.map(c => Collection.fromModel(c)).sort((a, b) => a.name.localeCompare(b.name)); - }); - - this.stores = computed(() => { - const catalog = this._catalog.listener(); - return this._catalog.getStores(); - }); - } - - @ViewChildren('editing', {read: ElementRef}) inputGroup: QueryList; - - @Input() - readonly entity: Signal; - @Input() - readonly namespace: Signal; - @Input() - readonly currentRoute: Signal; - - @Input() - readonly placements: Signal; - @Input() - readonly partitions: Signal; - @Input() - readonly allocations: Signal; - @Input() - readonly stores: Signal; - @Input() - readonly addableStores: Signal; - - readonly collections: Signal; - - newCollectionName: string; - selectedStore; - creatingCollection = false; - - private subscriptions = new Subscription(); - - private editOpen = false; - - protected readonly Method = Method; - - ngOnInit() { - - const sub2 = this._crud.onReconnection().subscribe((b) => { - if (b) { - this.onReconnect(); - } - }); - this.subscriptions.add(sub2); - } - - ngOnDestroy() { - this.subscriptions.unsubscribe(); - } - - onReconnect() { - //this._catalog.updateIfNecessary(); - this._leftSidebar.setSchema(this._router, '/views/schema-editing/', false, 2, true); - } - - /** - * get the right class for the 'drop' and 'truncate' buttons - * enable the button if the confirm-text is equal to the table-name or to 'drop table-name' respectively 'truncate table-name' - */ - dropTruncateClass(action: Method, table: Collection) { - if (action === Method.DROP && (table.drop === table.name || table.drop === 'drop ' + table.name)) { - return 'btn-danger'; - } else if (action === Method.TRUNCATE && (table.truncate === table.name || table.truncate === 'truncate ' + table.name)) { - return 'btn-danger'; } - return 'btn-light disabled'; - } - - /** - * send a request to either drop or truncate a table - */ - sendRequest(action: Method, collection: Collection) { - console.log('trunc'); - if (this.dropTruncateClass(action, collection) !== 'btn-danger') { - return; + + @ViewChildren('editing', {read: ElementRef}) inputGroup: QueryList; + + @Input() + readonly entity: Signal; + @Input() + readonly namespace: Signal; + @Input() + readonly currentRoute: Signal; + + @Input() + readonly placements: Signal; + @Input() + readonly partitions: Signal; + @Input() + readonly allocations: Signal; + @Input() + readonly stores: Signal; + @Input() + readonly addableStores: Signal; + + readonly collections: Signal; + + newCollectionName: string; + selectedStore; + creatingCollection = false; + + private subscriptions = new Subscription(); + + private editOpen = false; + + protected readonly Method = Method; + + ngOnInit() { + + const sub2 = this._crud.onReconnection().subscribe((b) => { + if (b) { + this.onReconnect(); + } + }); + this.subscriptions.add(sub2); + } + + ngOnDestroy() { + this.subscriptions.unsubscribe(); } - let query; - switch (action) { - case Method.DROP: - query = `db.${collection.name}.drop()`; - break; - case Method.TRUNCATE: - query = `db.${collection.name}.remove({})`; - break; - default: - return; + onReconnect() { + //this._catalog.updateIfNecessary(); + this._leftSidebar.setSchema(this._router, '/views/schema-editing/', false, 2, true); } - const request = new QueryRequest(query, false, true, 'mql', this.namespace().name); + /** + * get the right class for the 'drop' and 'truncate' buttons + * enable the button if the confirm-text is equal to the table-name or to 'drop table-name' respectively 'truncate table-name' + */ + dropTruncateClass(action: Method, table: Collection) { + if (action === Method.DROP && (table.drop === table.name || table.drop === 'drop ' + table.name)) { + return 'btn-danger'; + } else if (action === Method.TRUNCATE && (table.truncate === table.name || table.truncate === 'truncate ' + table.name)) { + return 'btn-danger'; + } + return 'btn-light disabled'; + } - this._crud.anyQueryBlocking(request).subscribe({ - next: (result: Result) => { - if (result.error) { - this._toast.exception(result, 'Could not ' + action + ' the table ' + collection + ':'); - } else { - //this._catalog.updateIfNecessary(); - let toastAction = 'Truncated'; - if (action === Method.DROP) { - toastAction = 'Dropped'; - this._leftSidebar.setSchema(this._router, '/views/schema-editing/', true, 2, false); - } - this._toast.success(toastAction + ' collection ' + collection.name); + /** + * send a request to either drop or truncate a table + */ + sendRequest(action: Method, collection: Collection) { + console.log('trunc'); + if (this.dropTruncateClass(action, collection) !== 'btn-danger') { + return; } - }, error: err => { - this._toast.error('Could not ' + action + ' the table ' + collection + ' due to an unknown error'); - console.log(err); - } - }); - } - - createCollection() { - if (this.newCollectionName === '') { - this._toast.warn('Please provide a name for the new collection. The new collection was not created.', 'missing table name', ToastDuration.INFINITE); - return; + let query; + switch (action) { + case Method.DROP: + query = `db.${collection.name}.drop()`; + break; + case Method.TRUNCATE: + query = `db.${collection.name}.remove({})`; + break; + default: + return; + } + + const request = new QueryRequest(query, false, true, 'mql', this.namespace().name); + + this._crud.anyQueryBlocking(request).subscribe({ + next: (result: Result) => { + if (result.error) { + this._toast.exception(result, 'Could not ' + action + ' the table ' + collection + ':'); + } else { + //this._catalog.updateIfNecessary(); + let toastAction = 'Truncated'; + if (action === Method.DROP) { + toastAction = 'Dropped'; + this._leftSidebar.setSchema(this._router, '/views/schema-editing/', true, 2, false); + } + this._toast.success(toastAction + ' collection ' + collection.name); + } + + }, error: err => { + this._toast.error('Could not ' + action + ' the table ' + collection + ' due to an unknown error'); + console.log(err); + } + }); } - if (!this._crud.nameIsValid(this.newCollectionName)) { - this._toast.warn('Please provide a valid name for the new collection. The new collection was not created.', 'invalid table name', ToastDuration.INFINITE); - return; + + createCollection() { + if (this.newCollectionName === '') { + this._toast.warn('Please provide a name for the new collection. The new collection was not created.', 'missing table name', ToastDuration.INFINITE); + return; + } + if (!this._crud.nameIsValid(this.newCollectionName)) { + this._toast.warn('Please provide a valid name for the new collection. The new collection was not created.', 'invalid table name', ToastDuration.INFINITE); + return; + } + if (this.collections().filter((t) => t.name === this.newCollectionName).length > 0) { + //if (this.tables.indexOf(this.newTableName) !== -1) { + this._toast.warn('A collection with this name already exists. Please choose another name.', 'invalid collection name', ToastDuration.INFINITE); + return; + } + const query = 'db.createCollection(' + this.newCollectionName + ')'; + const entityName = this.newCollectionName; + //const request = new EditCollectionRequest(this.namespace.value.id, this.newCollectionName, null, 'create', this.selectedStore); + this.creatingCollection = true; + this._crud.anyQueryBlocking(new QueryRequest(query, false, true, 'mql', this.namespace().name)).subscribe({ + next: (result: Result) => { + if (result.error) { + this._toast.exception(result, 'Could not generate collection:'); + } else { + this._toast.success('Generated collection ' + entityName, result.query); + this.newCollectionName = ''; + this.selectedStore = null; + this._leftSidebar.setSchema(this._router, '/views/schema-editing/', true, 2, false); + } + //this._catalog.updateIfNecessary(); + }, error: err => { + this._toast.error('Could not generate collection'); + console.log(err); + } + }).add(() => this.creatingCollection = false); } - if (this.collections().filter((t) => t.name === this.newCollectionName).length > 0) { - //if (this.tables.indexOf(this.newTableName) !== -1) { - this._toast.warn('A collection with this name already exists. Please choose another name.', 'invalid collection name', ToastDuration.INFINITE); - return; + + rename(table: Collection) { + const t = new EntityMeta(this.namespace().id, table.id, table.newName, []); + this._crud.renameTable(t).subscribe({ + next: res => { + const r = res; + if (r.exception) { + this._toast.exception(r); + } else { + this._toast.success('Renamed table ' + table.name + ' to ' + table.newName); + //this._catalog.updateIfNecessary(); + this._leftSidebar.setSchema(this._router, '/views/schema-editing/', false, 2, true); + } + }, error: err => { + this._toast.error('Could not rename the collection ' + table.name); + console.log(err); + } + + }); } - const query = 'db.createCollection(' + this.newCollectionName + ')'; - const entityName = this.newCollectionName; - //const request = new EditCollectionRequest(this.namespace.value.id, this.newCollectionName, null, 'create', this.selectedStore); - this.creatingCollection = true; - this._crud.anyQueryBlocking(new QueryRequest(query, false, true, 'mql', this.namespace().name)).subscribe({ - next: (result: Result) => { - if (result.error) { - this._toast.exception(result, 'Could not generate collection:'); - } else { - this._toast.success('Generated collection ' + entityName, result.query); - this.newCollectionName = ''; - this.selectedStore = null; - this._leftSidebar.setSchema(this._router, '/views/schema-editing/', true, 2, false); - } - //this._catalog.updateIfNecessary(); - }, error: err => { - this._toast.error('Could not generate collection'); - console.log(err); - } - }).add(() => this.creatingCollection = false); - } - - rename(table: Collection) { - const t = new EntityMeta(this.namespace().id, table.id, table.newName, []); - this._crud.renameTable(t).subscribe({ - next: res => { - const r = res; - if (r.exception) { - this._toast.exception(r); + + /** + * Check if the new table name is valid + */ + canRename(table: Collection) { + //table.name !== table.newName not necessary, since the filter will catch it as well + return this.collections().filter((t) => t.name === table.newName).length === 0 && + this._crud.nameIsValid(table.newName); + } + + createTableValidation(name: string) { + const regex = this._crud.getValidationRegex(); + if (name === '') { + return ''; + //} else if (regex.test(name) && name.length <= 100 && this.tables.indexOf(name) === -1) { + } else if (regex.test(name) && name.length <= 100 && this.collections().filter((t) => t.name === name).length === 0) { + return 'is-valid'; } else { - this._toast.success('Renamed table ' + table.name + ' to ' + table.newName); - //this._catalog.updateIfNecessary(); - this._leftSidebar.setSchema(this._router, '/views/schema-editing/', false, 2, true); + return 'is-invalid'; } - }, error: err => { - this._toast.error('Could not rename the collection ' + table.name); - console.log(err); - } - - }); - } - - /** - * Check if the new table name is valid - */ - canRename(table: Collection) { - //table.name !== table.newName not necessary, since the filter will catch it as well - return this.collections().filter((t) => t.name === table.newName).length === 0 && - this._crud.nameIsValid(table.newName); - } - - createTableValidation(name: string) { - const regex = this._crud.getValidationRegex(); - if (name === '') { - return ''; - //} else if (regex.test(name) && name.length <= 100 && this.tables.indexOf(name) === -1) { - } else if (regex.test(name) && name.length <= 100 && this.collections().filter((t) => t.name === name).length === 0) { - return 'is-valid'; - } else { - return 'is-invalid'; } - } } class Collection { - id: number; - name: string; - truncate = ''; - drop = ''; - export = false; - editing = false; - newName: string; - modifiable: boolean; - tableType: EntityType; - - constructor(name: string, newName: string, modifiable: boolean, entityType: EntityType) { - this.name = name; - this.newName = newName; - this.modifiable = modifiable; - this.tableType = entityType; - } - - static fromDB(collection: DbTable) { - return new Collection(collection.tableName, collection.tableName, collection.modifiable, collection.tableType); - } - - static fromModel(collection: CollectionModel) { - return new Collection(collection.name, collection.name, collection.modifiable, collection.entityType); - } + id: number; + name: string; + truncate = ''; + drop = ''; + export = false; + editing = false; + newName: string; + modifiable: boolean; + tableType: EntityType; + + constructor(name: string, newName: string, modifiable: boolean, entityType: EntityType) { + this.name = name; + this.newName = newName; + this.modifiable = modifiable; + this.tableType = entityType; + } + + static fromDB(collection: DbTable) { + return new Collection(collection.tableName, collection.tableName, collection.modifiable, collection.tableType); + } + + static fromModel(collection: CollectionModel) { + return new Collection(collection.name, collection.name, collection.modifiable, collection.entityType); + } } diff --git a/src/app/views/schema-editing/edit-columns/edit-columns.component.ts b/src/app/views/schema-editing/edit-columns/edit-columns.component.ts index baceea95..e216e9fb 100644 --- a/src/app/views/schema-editing/edit-columns/edit-columns.component.ts +++ b/src/app/views/schema-editing/edit-columns/edit-columns.component.ts @@ -1,994 +1,1034 @@ -import {Component, computed, effect, HostListener, inject, Injector, Input, OnDestroy, OnInit, signal, Signal, WritableSignal} from '@angular/core'; +import { + Component, + computed, + effect, + HostListener, + inject, + Injector, + Input, + OnDestroy, + OnInit, + signal, + Signal, + WritableSignal +} from '@angular/core'; import * as $ from 'jquery'; import {CrudService} from '../../../services/crud.service'; -import {FieldType, IndexMethodModel, IndexModel, ModifyPartitionRequest, PartitionFunctionModel, PartitioningRequest, PolyType, RelationalResult, TableConstraint, UiColumnDefinition} from '../../../components/data-view/models/result-set.model'; +import { + FieldType, + IndexMethodModel, + IndexModel, + ModifyPartitionRequest, + PartitionFunctionModel, + PartitioningRequest, + PolyType, + RelationalResult, + TableConstraint, + UiColumnDefinition +} from '../../../components/data-view/models/result-set.model'; import {ToastDuration, ToasterService} from '../../../components/toast-exposer/toaster.service'; import {UntypedFormControl, UntypedFormGroup, Validators} from '@angular/forms'; -import {ColumnRequest, ConstraintRequest, EditTableRequest, MaterializedRequest, Method} from '../../../models/ui-request.model'; +import { + ColumnRequest, + ConstraintRequest, + EditTableRequest, + MaterializedRequest, + Method +} from '../../../models/ui-request.model'; import {DbmsTypesService} from '../../../services/dbms-types.service'; import {AdapterModel, AdapterType, PlacementType} from '../../adapters/adapter.model'; import {Subscription} from 'rxjs'; import {ForeignKey, Uml} from '../../uml/uml.model'; import {CatalogService} from '../../../services/catalog.service'; -import {AllocationEntityModel, AllocationPartitionModel, AllocationPlacementModel, ConstraintModel, DeployMode, EntityModel, EntityType, NamespaceModel, TableModel} from '../../../models/catalog.model'; +import { + AllocationEntityModel, + AllocationPartitionModel, + AllocationPlacementModel, + ConstraintModel, + DeployMode, + EntityModel, + EntityType, + NamespaceModel, + TableModel +} from '../../../models/catalog.model'; import {toSignal} from '@angular/core/rxjs-interop'; -import {map} from "rxjs/operators"; +import {map} from 'rxjs/operators'; const INITIAL_TYPE = 'BIGINT'; @Component({ - selector: 'app-edit-columns', - templateUrl: './edit-columns.component.html', - styleUrls: ['./edit-columns.component.scss'] + selector: 'app-edit-columns', + templateUrl: './edit-columns.component.html', + styleUrls: ['./edit-columns.component.scss'] }) export class EditColumnsComponent implements OnInit, OnDestroy { - public readonly _crud = inject(CrudService); - public readonly _types = inject(DbmsTypesService); - public readonly _catalog = inject(CatalogService); - private readonly _toast = inject(ToasterService); - - constructor( - private injector: Injector - ) { - this.newIndexForm = new UntypedFormGroup({ - name: new UntypedFormControl('', this._crud.getNameValidator()), - method: new UntypedFormControl('') - }); - - - this.oldColumns = computed(() => { - const catalog = this._catalog.listener(); - if (!this.entity) { - return new Map(); - } - - const entity = this.entity(); - if (!entity) { - return new Map(); - } - const columns = this._catalog.getColumns(entity.id); - if (!columns) { - return new Map(); - } - - return new Map(columns.map(c => { - const columnIds = this._catalog.getPrimaryKey(c.entityId)?.columnIds || []; - return UiColumnDefinition.fromModel(c, columnIds); - }).map(c => [c.name, c])); - }); - - effect(() => { - const catalog = this._catalog.listener(); - const oldColumns = this.oldColumns(); - if (!oldColumns) { - return; - } + public readonly _crud = inject(CrudService); + public readonly _types = inject(DbmsTypesService); + public readonly _catalog = inject(CatalogService); + private readonly _toast = inject(ToasterService); + + constructor( + private injector: Injector + ) { + this.newIndexForm = new UntypedFormGroup({ + name: new UntypedFormControl('', this._crud.getNameValidator()), + method: new UntypedFormControl('') + }); - if (!this.entity || !this.entity()) { - return; - } - const entity = this.entity(); - const colValues = Array.from(oldColumns.values()); - - this.newIndexCols = new Map(colValues.map(c => [c.name, false])); - this.partitioningRequest.column = colValues.length > 0 ? colValues[0].name : ''; - this.newPrimaryKey = Array.from(oldColumns.values()).map(x => Object.assign({}, x)); - }); - this.constraints = computed(() => { - const catalog = this._catalog.listener(); + this.oldColumns = computed(() => { + const catalog = this._catalog.listener(); + if (!this.entity) { + return new Map(); + } - if (!this.entity || !this.entity()) { - return []; - } - - - return catalog.getConstraints(this.entity().id) || []; - }); - - this._types.getTypes().subscribe( - (type: PolyType[]) => { - this.types = type; - this.createColumn.dataType = INITIAL_TYPE; - } - ); - - this.availableStoresForIndexes = computed(() => { + const entity = this.entity(); + if (!entity) { + return new Map(); + } + const columns = this._catalog.getColumns(entity.id); + if (!columns) { + return new Map(); + } - if (!this.stores || !this.stores()) { - return []; - } - const stores = this.stores(); - const placements = this.placements()?.map(p => p.adapterId); - if (!placements) { - return []; - } - let locations = Array.from(stores).filter(store => placements.includes(store.id)); - let adapterModel = new AdapterModel("Polypheny-DB", "POLYPHENY", new Map(), false, AdapterType.SOURCE, DeployMode.ALL); - adapterModel.indexMethods = [new IndexMethodModel('hash', 'Hash')]; - locations = [adapterModel, ...locations]; - return locations; - }); - - effect(() => { - if (!this.currentRoute || !this.currentRoute()) { - return; - } + return new Map(columns.map(c => { + const columnIds = this._catalog.getPrimaryKey(c.entityId)?.columnIds || []; + return UiColumnDefinition.fromModel(c, columnIds); + }).map(c => [c.name, c])); + }); - this.getAvailableStoresForIndexes(); - this.getUml(); + effect(() => { + const catalog = this._catalog.listener(); + const oldColumns = this.oldColumns(); + if (!oldColumns) { + return; + } - const entity = this.entity(); - if (!!entity) { - if (entity.entityType === EntityType.MATERIALIZED_VIEW) { - this.subscribeMaterializedInfo(); - } - } + if (!this.entity || !this.entity()) { + return; + } + const entity = this.entity(); + const colValues = Array.from(oldColumns.values()); + + this.newIndexCols = new Map(colValues.map(c => [c.name, false])); + this.partitioningRequest.column = colValues.length > 0 ? colValues[0].name : ''; + this.newPrimaryKey = Array.from(oldColumns.values()).map(x => Object.assign({}, x)); + }); + + this.constraints = computed(() => { + const catalog = this._catalog.listener(); + + if (!this.entity || !this.entity()) { + return []; + } + + + return catalog.getConstraints(this.entity().id) || []; + }); - this.subscribeIndexes(); + this._types.getTypes().subscribe( + (type: PolyType[]) => { + this.types = type; + this.createColumn.dataType = INITIAL_TYPE; + } + ); + + this.availableStoresForIndexes = computed(() => { + + if (!this.stores || !this.stores()) { + return []; + } + const stores = this.stores(); + const placements = this.placements()?.map(p => p.adapterId); + if (!placements) { + return []; + } + let locations = Array.from(stores).filter(store => placements.includes(store.id)); + let adapterModel = new AdapterModel('Polypheny-DB', 'POLYPHENY', new Map(), false, AdapterType.SOURCE, DeployMode.ALL); + adapterModel.indexMethods = [new IndexMethodModel('hash', 'Hash')]; + locations = [adapterModel, ...locations]; + return locations; + }); - this.initNewIndexValues(); - }); + effect(() => { + if (!this.currentRoute || !this.currentRoute()) { + return; + } - } + this.getAvailableStoresForIndexes(); + this.getUml(); - readonly entity: WritableSignal = signal(null); + const entity = this.entity(); + if (!!entity) { + if (entity.entityType === EntityType.MATERIALIZED_VIEW) { + this.subscribeMaterializedInfo(); + } + } - @Input() set entityIn(entity: EntityModel) { - this.entity.set(entity as TableModel); - } + this.subscribeIndexes(); - @Input() - readonly namespace: Signal; + this.initNewIndexValues(); + }); - readonly currentRoute: WritableSignal = signal(""); + } - @Input() set route(currentRoute: string) { - this.currentRoute.set(currentRoute); - } + readonly entity: WritableSignal = signal(null); - @Input() - readonly placements: Signal; + @Input() set entityIn(entity: EntityModel) { + this.entity.set(entity as TableModel); + } - @Input() - readonly partitions: Signal; + @Input() + readonly namespace: Signal; - @Input() - readonly allocations: Signal; + readonly currentRoute: WritableSignal = signal(''); - @Input() - readonly stores: Signal; + @Input() set route(currentRoute: string) { + this.currentRoute.set(currentRoute); + } - @Input() - readonly addableStores: Signal; + @Input() + readonly placements: Signal; - foreignKeys: ForeignKey[] = []; + @Input() + readonly partitions: Signal; - types: PolyType[] = []; - editColumn = -1; - createColumn = new UiColumnDefinition(-1, '', false, true, 'text', '', null, null, null); - confirm = -1; - readonly oldColumns: Signal>; - updateColumn = new UntypedFormGroup({name: new UntypedFormControl('')}); + @Input() + readonly allocations: Signal; - readonly constraints: Signal; - confirmConstraint = -1; - newPrimaryKey: UiColumnDefinition[]; + @Input() + readonly stores: Signal; - uniqueConstraintName = ''; - proposedConstraintName = 'constraintName'; + @Input() + readonly addableStores: Signal; - readonly indexHeaders: WritableSignal = signal([]); - readonly indexDefinitions: WritableSignal = signal([]); - newIndexCols = new Map(); - selectedStoreForIndex: AdapterModel; - newIndexForm: UntypedFormGroup; - indexSubmitted = false; - proposedIndexName = 'indexName'; - addingIndex = false; - - materializedInfo: Signal<{}>; - - //data placement handling - readonly availableStoresForIndexes: Signal; - selectedStore: AdapterModel; - - - columnPlacement: UntypedFormGroup; - placementMethod: Method; - isAddingPlacement = false; - - //partition handling - partitionTypes: string[]; - partitioningRequest: PartitioningRequest = new PartitioningRequest(); - isMergingPartitions = false; - partitionsToModify: { partitionName: string, selected: boolean }[]; - partitionFunctionParams: PartitionFunctionModel; - fieldTypes: typeof FieldType = FieldType; - - subscriptions = new Subscription(); + foreignKeys: ForeignKey[] = []; - placementModal = false; - partitioningModal = false; - partitionFunctionModal = false; + types: PolyType[] = []; + editColumn = -1; + createColumn = new UiColumnDefinition(-1, '', false, true, 'text', '', null, null, null); + confirm = -1; + readonly oldColumns: Signal>; + updateColumn = new UntypedFormGroup({name: new UntypedFormControl('')}); + readonly constraints: Signal; + confirmConstraint = -1; + newPrimaryKey: UiColumnDefinition[]; - public readonly EntityType = EntityType; + uniqueConstraintName = ''; + proposedConstraintName = 'constraintName'; - protected readonly Method = Method; + readonly indexHeaders: WritableSignal = signal([]); + readonly indexDefinitions: WritableSignal = signal([]); + newIndexCols = new Map(); + selectedStoreForIndex: AdapterModel; + newIndexForm: UntypedFormGroup; + indexSubmitted = false; + proposedIndexName = 'indexName'; + addingIndex = false; - protected readonly PlacementType = PlacementType; - - ngOnInit() { - this.getPartitionTypes(); - this.getGeneratedNames(); - } + materializedInfo: Signal<{}>; - ngOnDestroy() { - $(document).off('click'); - this.subscriptions.unsubscribe(); - } - - //see https://medium.com/claritydesignsystem/1b66d45b3e3d - @HostListener('window:click', ['$event.target']) - onClick(targetElement: string) { - const self = this; - if ($(targetElement).parents('.editing').length === 0) { - self.editColumn = -1; - } - } - - isEntityType(type: EntityType): Signal { - return computed(() => this.entity()?.entityType === type); - } - - - columnValidation(columnName: string, editing: string = null) { - if (editing) { - if (Array.from(this.oldColumns().values()).filter((h) => h.name === columnName && h.name !== editing).length > 0) { - return 'is-invalid'; - } - } else { - if (Array.from(this.oldColumns().values()).filter((h) => h.name === columnName).length > 0) { - return 'is-invalid'; - } - } - return this._crud.getValidationClass(columnName); - } - - editCol(i: number, col: UiColumnDefinition, e = null) { - if (e.target.id === 'delete') { - return; - } - if (this.editColumn !== i) { - if (col.defaultValue === undefined) { - col.defaultValue = null; - } - this.updateColumn = new UntypedFormGroup({ - name: new UntypedFormControl(col.name, Validators.required), - oldName: new UntypedFormControl(col.name), - nullable: new UntypedFormControl(col.nullable), - dataType: new UntypedFormControl(col.dataType), - collectionsType: new UntypedFormControl(col.collectionsType), - precision: new UntypedFormControl(col.precision), - scale: new UntypedFormControl(col.scale), - dimension: new UntypedFormControl(col.dimension), - cardinality: new UntypedFormControl(col.cardinality), - defaultValue: new UntypedFormControl({value: col.defaultValue, disabled: col.defaultValue === null}) - }); - this.editColumn = i; - } - } - - subscribeMaterializedInfo() { - this.materializedInfo = computed(() => { - const entity = this.entity; - if (!entity) { - return this.materializedInfo(); - } - return toSignal(this._crud.getMaterializedInfo(new EditTableRequest(this.namespace().id, this.entity().id)))(); - - }); - } - - updateMaterialized() { - const req = new MaterializedRequest(this.entity().id); - this._crud.updateMaterialized(req).subscribe({ - next: (res: RelationalResult) => { - //this.subscribeMaterializedInfo(); - if (res.error) { - this._toast.exception(res, 'Could not update materialized view:'); - } else { - this._toast.success('Materialized view was updated', res.query, 'Updated'); - } - }, error: err => { - this._toast.error('Could not update materialized view due to an error on the server.', null, ToastDuration.INFINITE); - console.log(err); - } - }); - } - - - updateMaterializedColumn(oldCol: UiColumnDefinition, newName) { - const newCol = Object.assign({}, oldCol); - newCol.name = newName; - const req = new ColumnRequest(this.entity().id, oldCol, newCol, true, 'MATERIALIZED'); - - this._crud.updateColumn(req).subscribe({ - next: (res: RelationalResult) => { - this.editColumn = -1; - //this._catalog.updateIfNecessary(); - if (res.error) { - this._toast.exception(res, 'Could not update column:'); - } else { - this._toast.success('The column was renamed.', res.query, 'column saved'); - } - }, error: err => { - this._toast.error('Could not save column due to an error on the server.', null, ToastDuration.INFINITE); - console.log(err); - } - }); - } - - - saveCol() { - console.log(this.updateColumn); - if (!this._crud.nameIsValid(this.updateColumn.controls['name'].value)) { - this._toast.warn(this._crud.invalidNameMessage('column'), 'invalid column name'); - return; - } - if (Array.from(this.oldColumns().values()).filter((h) => h.name === this.updateColumn.controls['name'].value && h.name !== this.updateColumn.controls['oldName'].value).length > 0) { - this._toast.warn('This column name already exists', 'invalid column name'); - return; - } - const oldColumn = this.oldColumns().get(this.updateColumn.controls['oldName'].value); - console.log(oldColumn); - const newColumn = new UiColumnDefinition( - null, - this.updateColumn.controls['name'].value, - oldColumn.primary, - this.updateColumn.controls['nullable'].value, - this.updateColumn.controls['dataType'].value, - this.updateColumn.controls['collectionsType'].value, - this.updateColumn.controls['precision'].value, - this.updateColumn.controls['scale'].value, - this.updateColumn.controls['defaultValue'].value, - this.updateColumn.controls['dimension'].value || -1, - this.updateColumn.controls['cardinality'].value || -1 - ); - if (!this._types.supportsPrecision(newColumn.dataType) && newColumn.precision !== null) { - newColumn.precision = null; - } - if (!this._types.supportsScale(newColumn.dataType) && newColumn.scale !== null) { - newColumn.scale = null; - } - const req = new ColumnRequest(this.entity().id, oldColumn, newColumn); - this._crud.updateColumn(req).subscribe({ - next: (res: RelationalResult) => { - this.editColumn = -1; - //this._catalog.updateIfNecessary(); - if (res.error) { - this._toast.exception(res, 'Could not update column:'); - console.log(res); - } else { - this._toast.success('The new column was saved.', res.query, 'column saved'); - } - }, error: err => { - this._toast.error('Could not save column due to an error on the server.', null, ToastDuration.INFINITE); - console.log(err); - } - }); - } - - addColumn() { - if (this.createColumn.name === '') { - this._toast.warn('Please provide a name for the new column.', 'missing column name'); - return; - } - if (!this._crud.nameIsValid(this.createColumn.name)) { - this._toast.warn(this._crud.invalidNameMessage('column'), 'invalid column name'); - return; - } - if (Array.from(this.oldColumns().values()).filter((h) => h.name === this.createColumn.name).length > 0) { - this._toast.warn('There already exists a column with this name', 'invalid column name'); - return; - } - if (!this._types.supportsPrecision(this.createColumn.dataType) && this.createColumn.precision !== null) { - this.createColumn.precision = null; - } - if (!this._types.supportsScale(this.createColumn.dataType) && this.createColumn.scale !== null) { - this.createColumn.scale = null; - } - const req = new ColumnRequest(this.entity().id, null, this.createColumn); - this._crud.createColumn(req).subscribe({ - next: (res: RelationalResult) => { - if (res.error === undefined) { - //this._catalog.updateIfNecessary(); - this.createColumn.name = ''; - this.createColumn.nullable = true; - this.createColumn.dataType = INITIAL_TYPE; - this.createColumn.collectionsType = ''; - this.createColumn.precision = null; - this.createColumn.scale = null; - this.createColumn.defaultValue = null; + //data placement handling + readonly availableStoresForIndexes: Signal; + selectedStore: AdapterModel; + + + columnPlacement: UntypedFormGroup; + placementMethod: Method; + isAddingPlacement = false; + + //partition handling + partitionTypes: string[]; + partitioningRequest: PartitioningRequest = new PartitioningRequest(); + isMergingPartitions = false; + partitionsToModify: { partitionName: string, selected: boolean }[]; + partitionFunctionParams: PartitionFunctionModel; + fieldTypes: typeof FieldType = FieldType; + + subscriptions = new Subscription(); + + placementModal = false; + partitioningModal = false; + partitionFunctionModal = false; + + + public readonly EntityType = EntityType; + + protected readonly Method = Method; + + protected readonly PlacementType = PlacementType; + + ngOnInit() { + this.getPartitionTypes(); + this.getGeneratedNames(); + } + + ngOnDestroy() { + $(document).off('click'); + this.subscriptions.unsubscribe(); + } + + //see https://medium.com/claritydesignsystem/1b66d45b3e3d + @HostListener('window:click', ['$event.target']) + onClick(targetElement: string) { + const self = this; + if ($(targetElement).parents('.editing').length === 0) { + self.editColumn = -1; + } + } + + isEntityType(type: EntityType): Signal { + return computed(() => this.entity()?.entityType === type); + } + + + columnValidation(columnName: string, editing: string = null) { + if (editing) { + if (Array.from(this.oldColumns().values()).filter((h) => h.name === columnName && h.name !== editing).length > 0) { + return 'is-invalid'; + } } else { - this._toast.exception(res, null, 'server error', ToastDuration.INFINITE); - } - }, error: err => { - this._toast.error('An error occurred on the server.', null, ToastDuration.INFINITE); - console.log(err); - } - }); - } - - dropColumn(col: UiColumnDefinition) { - this._crud.dropColumn(new ColumnRequest(this.entity().id, col)).subscribe({ - next: (result: RelationalResult) => { - //this._catalog.updateIfNecessary(); - //this.getPlacementsAndPartitions(); - this.confirm = -1; - if (result.error) { - this._toast.exception(result, 'Could not delete column:', 'server error', ToastDuration.INFINITE); - } - }, error: err => { - this._toast.error('Could not delete column.', null, ToastDuration.INFINITE); - console.log(err); - } - }); - } - - getUml() { - this.foreignKeys = []; - if (!this.namespace) { - this.foreignKeys = null; - return; - } - this._crud.getUml(new EditTableRequest(this.namespace().id)).subscribe({ - next: (uml: Uml) => { - - const fks = new Map(); - - uml.foreignKeys.forEach((v, k) => { - if ((v.sourceSchema + '.' + v.sourceTable) === this._catalog.getFullEntityName(this.entity().id)) { - if (fks.has(v.fkName)) { - const fk = fks.get(v.fkName); - fk.targetColumn = fk.targetColumn + ', ' + v.targetColumn; - fk.sourceColumn = fk.sourceColumn + ', ' + v.sourceColumn; + if (Array.from(this.oldColumns().values()).filter((h) => h.name === columnName).length > 0) { + return 'is-invalid'; + } + } + return this._crud.getValidationClass(columnName); + } + + editCol(i: number, col: UiColumnDefinition, e = null) { + if (e.target.id === 'delete') { + return; + } + if (this.editColumn !== i) { + if (col.defaultValue === undefined) { + col.defaultValue = null; + } + this.updateColumn = new UntypedFormGroup({ + name: new UntypedFormControl(col.name, Validators.required), + oldName: new UntypedFormControl(col.name), + nullable: new UntypedFormControl(col.nullable), + dataType: new UntypedFormControl(col.dataType), + collectionsType: new UntypedFormControl(col.collectionsType), + precision: new UntypedFormControl(col.precision), + scale: new UntypedFormControl(col.scale), + dimension: new UntypedFormControl(col.dimension), + cardinality: new UntypedFormControl(col.cardinality), + defaultValue: new UntypedFormControl({value: col.defaultValue, disabled: col.defaultValue === null}) + }); + this.editColumn = i; + } + } + + subscribeMaterializedInfo() { + this.materializedInfo = computed(() => { + const entity = this.entity; + if (!entity) { + return this.materializedInfo(); + } + return toSignal(this._crud.getMaterializedInfo(new EditTableRequest(this.namespace().id, this.entity().id)))(); + + }); + } + + updateMaterialized() { + const req = new MaterializedRequest(this.entity().id); + this._crud.updateMaterialized(req).subscribe({ + next: (res: RelationalResult) => { + //this.subscribeMaterializedInfo(); + if (res.error) { + this._toast.exception(res, 'Could not update materialized view:'); + } else { + this._toast.success('Materialized view was updated', res.query, 'Updated'); + } + }, error: err => { + this._toast.error('Could not update materialized view due to an error on the server.', null, ToastDuration.INFINITE); + console.log(err); + } + }); + } + + + updateMaterializedColumn(oldCol: UiColumnDefinition, newName) { + const newCol = Object.assign({}, oldCol); + newCol.name = newName; + const req = new ColumnRequest(this.entity().id, oldCol, newCol, true, 'MATERIALIZED'); + + this._crud.updateColumn(req).subscribe({ + next: (res: RelationalResult) => { + this.editColumn = -1; + //this._catalog.updateIfNecessary(); + if (res.error) { + this._toast.exception(res, 'Could not update column:'); + } else { + this._toast.success('The column was renamed.', res.query, 'column saved'); + } + }, error: err => { + this._toast.error('Could not save column due to an error on the server.', null, ToastDuration.INFINITE); + console.log(err); + } + }); + } + + + saveCol() { + console.log(this.updateColumn); + if (!this._crud.nameIsValid(this.updateColumn.controls['name'].value)) { + this._toast.warn(this._crud.invalidNameMessage('column'), 'invalid column name'); + return; + } + if (Array.from(this.oldColumns().values()).filter((h) => h.name === this.updateColumn.controls['name'].value && h.name !== this.updateColumn.controls['oldName'].value).length > 0) { + this._toast.warn('This column name already exists', 'invalid column name'); + return; + } + const oldColumn = this.oldColumns().get(this.updateColumn.controls['oldName'].value); + console.log(oldColumn); + const newColumn = new UiColumnDefinition( + null, + this.updateColumn.controls['name'].value, + oldColumn.primary, + this.updateColumn.controls['nullable'].value, + this.updateColumn.controls['dataType'].value, + this.updateColumn.controls['collectionsType'].value, + this.updateColumn.controls['precision'].value, + this.updateColumn.controls['scale'].value, + this.updateColumn.controls['defaultValue'].value, + this.updateColumn.controls['dimension'].value || -1, + this.updateColumn.controls['cardinality'].value || -1 + ); + if (!this._types.supportsPrecision(newColumn.dataType) && newColumn.precision !== null) { + newColumn.precision = null; + } + if (!this._types.supportsScale(newColumn.dataType) && newColumn.scale !== null) { + newColumn.scale = null; + } + const req = new ColumnRequest(this.entity().id, oldColumn, newColumn); + this._crud.updateColumn(req).subscribe({ + next: (res: RelationalResult) => { + this.editColumn = -1; + //this._catalog.updateIfNecessary(); + if (res.error) { + this._toast.exception(res, 'Could not update column:'); + console.log(res); + } else { + this._toast.success('The new column was saved.', res.query, 'column saved'); + } + }, error: err => { + this._toast.error('Could not save column due to an error on the server.', null, ToastDuration.INFINITE); + console.log(err); + } + }); + } + + addColumn() { + if (this.createColumn.name === '') { + this._toast.warn('Please provide a name for the new column.', 'missing column name'); + return; + } + if (!this._crud.nameIsValid(this.createColumn.name)) { + this._toast.warn(this._crud.invalidNameMessage('column'), 'invalid column name'); + return; + } + if (Array.from(this.oldColumns().values()).filter((h) => h.name === this.createColumn.name).length > 0) { + this._toast.warn('There already exists a column with this name', 'invalid column name'); + return; + } + if (!this._types.supportsPrecision(this.createColumn.dataType) && this.createColumn.precision !== null) { + this.createColumn.precision = null; + } + if (!this._types.supportsScale(this.createColumn.dataType) && this.createColumn.scale !== null) { + this.createColumn.scale = null; + } + const req = new ColumnRequest(this.entity().id, null, this.createColumn); + this._crud.createColumn(req).subscribe({ + next: (res: RelationalResult) => { + if (res.error === undefined) { + //this._catalog.updateIfNecessary(); + this.createColumn.name = ''; + this.createColumn.nullable = true; + this.createColumn.dataType = INITIAL_TYPE; + this.createColumn.collectionsType = ''; + this.createColumn.precision = null; + this.createColumn.scale = null; + this.createColumn.defaultValue = null; + } else { + this._toast.exception(res, null, 'server error', ToastDuration.INFINITE); + } + }, error: err => { + this._toast.error('An error occurred on the server.', null, ToastDuration.INFINITE); + console.log(err); + } + }); + } + + dropColumn(col: UiColumnDefinition) { + this._crud.dropColumn(new ColumnRequest(this.entity().id, col)).subscribe({ + next: (result: RelationalResult) => { + //this._catalog.updateIfNecessary(); + //this.getPlacementsAndPartitions(); + this.confirm = -1; + if (result.error) { + this._toast.exception(result, 'Could not delete column:', 'server error', ToastDuration.INFINITE); + } + }, error: err => { + this._toast.error('Could not delete column.', null, ToastDuration.INFINITE); + console.log(err); + } + }); + } + + getUml() { + this.foreignKeys = []; + if (!this.namespace) { + this.foreignKeys = null; + return; + } + this._crud.getUml(new EditTableRequest(this.namespace().id)).subscribe({ + next: (uml: Uml) => { + + const fks = new Map(); + + uml.foreignKeys.forEach((v, k) => { + if ((v.sourceSchema + '.' + v.sourceTable) === this._catalog.getFullEntityName(this.entity().id)) { + if (fks.has(v.fkName)) { + const fk = fks.get(v.fkName); + fk.targetColumn = fk.targetColumn + ', ' + v.targetColumn; + fk.sourceColumn = fk.sourceColumn + ', ' + v.sourceColumn; + } else { + fks.set(v.fkName, v); + } + this.foreignKeys = [...fks.values()]; + } + }); + }, error: err => { + console.log(err); + } + }); + } + + + dropConstraint(constraintId: number) { + this._crud.dropConstraint(new ConstraintRequest(this.entity().id, new TableConstraint(constraintId))).subscribe({ + next: (result: RelationalResult) => { + if (result.error) { + this._toast.exception(result, null, 'constraint error'); + } else { + //this._catalog.updateIfNecessary(); + this.getUml(); + } + }, error: err => { + console.log(err); + } + }); + } + + updatePrimaryKey() { + const pk = new TableConstraint(-1, 'PRIMARY KEY'); + this.newPrimaryKey.forEach((v, k) => { + if (v.primary) { + pk.addColumn(v.name); + } + }); + const constraintRequest = new ConstraintRequest(this.entity().id, pk); + this._crud.createPrimaryKey(constraintRequest).subscribe({ + next: (res: RelationalResult) => { + if (!res.error) { + this._toast.success('The primary key was updated.', res.query, 'updated primary key'); + //this._catalog.updateIfNecessary(); + //this.getPlacementsAndPartitions(); + } else { + this._toast.exception(res, null, 'primary key error', ToastDuration.INFINITE); + } + }, error: err => { + this._toast.error('Could not update primary key.', null, ToastDuration.INFINITE); + console.log(err); + } + }); + } + + addUniqueConstraint() { + if (this.uniqueConstraintName === '') { + if (!this.proposedConstraintName) { + this._toast.warn('Please provide a name for the unique constraint.', 'constraint name'); + return; } else { - fks.set(v.fkName, v); + this.uniqueConstraintName = this.proposedConstraintName; + } + } + if (!this._crud.nameIsValid(this.uniqueConstraintName)) { + this._toast.warn(this._crud.invalidNameMessage('unique constraint'), 'invalid constraint name'); + return; + } + const constraint = new TableConstraint(-1, this.uniqueConstraintName, 'UNIQUE'); + let counter = 0; + this.oldColumns().forEach((v, k) => { + if (v.unique) { + constraint.addColumn(v.name); + counter++; } - this.foreignKeys = [...fks.values()]; - } }); - }, error: err => { - console.log(err); - } - }); - } - - - dropConstraint(constraintId: number) { - this._crud.dropConstraint(new ConstraintRequest(this.entity().id, new TableConstraint(constraintId))).subscribe({ - next: (result: RelationalResult) => { - if (result.error) { - this._toast.exception(result, null, 'constraint error'); - } else { - //this._catalog.updateIfNecessary(); - this.getUml(); - } - }, error: err => { - console.log(err); - } - }); - } - - updatePrimaryKey() { - const pk = new TableConstraint(-1, 'PRIMARY KEY'); - this.newPrimaryKey.forEach((v, k) => { - if (v.primary) { - pk.addColumn(v.name); - } - }); - const constraintRequest = new ConstraintRequest(this.entity().id, pk); - this._crud.createPrimaryKey(constraintRequest).subscribe({ - next: (res: RelationalResult) => { - if (!res.error) { - this._toast.success('The primary key was updated.', res.query, 'updated primary key'); - //this._catalog.updateIfNecessary(); - //this.getPlacementsAndPartitions(); - } else { - this._toast.exception(res, null, 'primary key error', ToastDuration.INFINITE); - } - }, error: err => { - this._toast.error('Could not update primary key.', null, ToastDuration.INFINITE); - console.log(err); - } - }); - } - - addUniqueConstraint() { - if (this.uniqueConstraintName === '') { - if (!this.proposedConstraintName) { - this._toast.warn('Please provide a name for the unique constraint.', 'constraint name'); - return; - } else { - this.uniqueConstraintName = this.proposedConstraintName; - } - } - if (!this._crud.nameIsValid(this.uniqueConstraintName)) { - this._toast.warn(this._crud.invalidNameMessage('unique constraint'), 'invalid constraint name'); - return; - } - const constraint = new TableConstraint(-1, this.uniqueConstraintName, 'UNIQUE'); - let counter = 0; - this.oldColumns().forEach((v, k) => { - if (v.unique) { - constraint.addColumn(v.name); - counter++; - } - }); - if (counter === 0) { - this._toast.warn('Please select at least one column that should be part of the unique constraint.', 'unique constraint'); - return; - } - const constraintRequest = new ConstraintRequest(this.entity().id, constraint); - this._crud.createUniqueConstraint(constraintRequest).subscribe({ - next: (res: RelationalResult) => { - if (!res.error) { - //this._catalog.updateIfNecessary(); - this._toast.success('The unique constraint was successfully created', res.query, 'added constraint'); - this.uniqueConstraintName = ''; - this.getGeneratedNames(); - this.oldColumns().forEach((v, k) => { - v.unique = false; - }); + if (counter === 0) { + this._toast.warn('Please select at least one column that should be part of the unique constraint.', 'unique constraint'); + return; + } + const constraintRequest = new ConstraintRequest(this.entity().id, constraint); + this._crud.createUniqueConstraint(constraintRequest).subscribe({ + next: (res: RelationalResult) => { + if (!res.error) { + //this._catalog.updateIfNecessary(); + this._toast.success('The unique constraint was successfully created', res.query, 'added constraint'); + this.uniqueConstraintName = ''; + this.getGeneratedNames(); + this.oldColumns().forEach((v, k) => { + v.unique = false; + }); + } else { + this._toast.exception(res, null, 'unique constraint error', ToastDuration.INFINITE); + } + }, error: err => { + this._toast.error('Could not add unique constraint.', null, ToastDuration.INFINITE); + console.log(err); + } + }); + } + + assignDefault(col: any, isFormGroup: boolean) { + if (isFormGroup) { + if (this._types.isNumeric(col.controls['dataType'].value)) { + col.controls['defaultValue'].setValue(0); + } else if (this._types.isBoolean(col.controls['dataType'].value)) { + col.controls['defaultValue'].setValue(false); + } else { + col.controls['defaultValue'].setValue(''); + } } else { - this._toast.exception(res, null, 'unique constraint error', ToastDuration.INFINITE); - } - }, error: err => { - this._toast.error('Could not add unique constraint.', null, ToastDuration.INFINITE); - console.log(err); - } - }); - } - - assignDefault(col: any, isFormGroup: boolean) { - if (isFormGroup) { - if (this._types.isNumeric(col.controls['dataType'].value)) { - col.controls['defaultValue'].setValue(0); - } else if (this._types.isBoolean(col.controls['dataType'].value)) { - col.controls['defaultValue'].setValue(false); - } else { - col.controls['defaultValue'].setValue(''); - } - } else { - if (this._types.isNumeric(col.dataType)) { - col.defaultValue = 0; - } else if (this._types.isBoolean(col.dataType)) { - col.defaultValue = false; - } else { - col.defaultValue = ''; - } - } - } - - triggerDefaultNull(col: UiColumnDefinition = null) { - if (col === null) {//when updating a column - if (this.updateColumn.controls['defaultValue'].value === null) { - this.updateColumn.controls['defaultValue'].enable(); - this.assignDefault(this.updateColumn, true); - } else { - this.updateColumn.controls['defaultValue'].setValue(null); - this.updateColumn.controls['defaultValue'].disable(); - } - } else {//if col !== null: when inserting a new column - if (col.defaultValue === null) { - this.assignDefault(col, false); - } else { - col.defaultValue = null; - } - } - } - - changeNullable(col: UiColumnDefinition = null) { - if (col === null) {//when updating a column - if (this.updateColumn.controls['defaultValue'].value === null && this.updateColumn.controls['nullable'].value === false) { - this.updateColumn.controls['defaultValue'].enable(); - this.assignDefault(this.updateColumn, true); - } - } else {//if col !== null: when inserting a new column - if (col.defaultValue === null && col.nullable === false) { - this.assignDefault(col, false); - } - } - } - - subscribeIndexes() { - effect(() => { - const entity = this.entity(); - const namespace = this.namespace(); - if (!entity || !namespace) { - return; - } - this._crud.getIndexes(new EditTableRequest(namespace.id, entity.id)).pipe(map(r => r as RelationalResult)).subscribe( - res => { - this.indexHeaders.set(res.header.map(h => h?.name)); - this.indexDefinitions.set(res.data); - } - ); - }, {injector: this.injector}); - } - - - initNewIndexValues() { - const availableStores = this.availableStoresForIndexes; - if (availableStores()?.length > 0) { - this.selectedStoreForIndex = availableStores()[0]; - if (this.selectedStoreForIndex.indexMethods && this.selectedStoreForIndex.indexMethods.length > 0) { - this.newIndexForm.controls['method'].setValue(this.selectedStoreForIndex.indexMethods[0].name); - } - } else { - this.selectedStoreForIndex = null; - this.newIndexForm.controls['method'].setValue(''); - } - } - - onSelectingIndexStore(store: AdapterModel) { - this.selectedStoreForIndex = store; - this.newIndexForm.controls['method'].setValue(store.indexMethods[0]); - } - - getAvailableStoresForIndexes() { - effect(() => { - if (!this.availableStoresForIndexes || !this.availableStoresForIndexes()) { - return; - } - const stores = this.availableStoresForIndexes(); - - if (stores?.length > 0) { - this.selectedStoreForIndex = stores[0]; - this.newIndexForm.controls['method'].setValue(this.selectedStoreForIndex.indexMethods[0]); - } else { - this.selectedStoreForIndex = null; - } - }, {injector: this.injector}); - } - - initPlacementModal(method: Method, placement: AllocationPlacementModel) { - let store; - const preselect = this._catalog.getAllocColumns(placement.id); - this.placementMethod = method; - - store = this._catalog.getAdapter(placement.adapterId); - this.selectedStore = store; - - if (!this.selectedStore) { - return; - } - this.columnPlacement = new UntypedFormGroup({}); - this.oldColumns().forEach((v, k) => { - let state = true; - if (preselect.length > 0 && !preselect.some(e => e.name === v.name && e.placementType === PlacementType.MANUAL)) { - state = false; - } - this.columnPlacement.addControl(v.name, new UntypedFormControl(state)); - }); - this.placementModal = true; - } - - clearPlacementModal() { - this.selectedStore = null; - } - - selectAllColumns(selectAll: boolean) { - this.oldColumns().forEach((v, k) => { - this.columnPlacement.get(v.name).setValue(selectAll); - }); - } - - addPlacement() { - const cols = []; - for (const [k, v] of Object.entries(this.columnPlacement.value)) { - //const v = this.columnPlacement.value[k]; - if (v) { - cols.push(k); - } - } - this.isAddingPlacement = true; - this._crud.addDropPlacement(this.namespace().id, this.entity().id, this.selectedStore.id, this.placementMethod, cols).subscribe({ - next: (res: RelationalResult) => { - if (res.error) { - this._toast.exception(res); + if (this._types.isNumeric(col.dataType)) { + col.defaultValue = 0; + } else if (this._types.isBoolean(col.dataType)) { + col.defaultValue = false; + } else { + col.defaultValue = ''; + } + } + } + + triggerDefaultNull(col: UiColumnDefinition = null) { + if (col === null) {//when updating a column + if (this.updateColumn.controls['defaultValue'].value === null) { + this.updateColumn.controls['defaultValue'].enable(); + this.assignDefault(this.updateColumn, true); + } else { + this.updateColumn.controls['defaultValue'].setValue(null); + this.updateColumn.controls['defaultValue'].disable(); + } + } else {//if col !== null: when inserting a new column + if (col.defaultValue === null) { + this.assignDefault(col, false); + } else { + col.defaultValue = null; + } + } + } + + changeNullable(col: UiColumnDefinition = null) { + if (col === null) {//when updating a column + if (this.updateColumn.controls['defaultValue'].value === null && this.updateColumn.controls['nullable'].value === false) { + this.updateColumn.controls['defaultValue'].enable(); + this.assignDefault(this.updateColumn, true); + } + } else {//if col !== null: when inserting a new column + if (col.defaultValue === null && col.nullable === false) { + this.assignDefault(col, false); + } + } + } + + subscribeIndexes() { + effect(() => { + const entity = this.entity(); + const namespace = this.namespace(); + if (!entity || !namespace) { + return; + } + this._crud.getIndexes(new EditTableRequest(namespace.id, entity.id)).pipe(map(r => r as RelationalResult)).subscribe( + res => { + this.indexHeaders.set(res.header.map(h => h?.name)); + this.indexDefinitions.set(res.data); + } + ); + }, {injector: this.injector}); + } + + + initNewIndexValues() { + const availableStores = this.availableStoresForIndexes; + if (availableStores()?.length > 0) { + this.selectedStoreForIndex = availableStores()[0]; + if (this.selectedStoreForIndex.indexMethods && this.selectedStoreForIndex.indexMethods.length > 0) { + this.newIndexForm.controls['method'].setValue(this.selectedStoreForIndex.indexMethods[0].name); + } } else { - if (this.placementMethod === 'ADD') { - this._toast.success('Added placement on store ' + this.selectedStore.name, res.query, 'Added placement'); - } else if (this.placementMethod === 'MODIFY') { - this._toast.success('Modified placement on store ' + this.selectedStore.name, res.query, 'Modified placement'); - } - //this._catalog.updateIfNecessary(); + this.selectedStoreForIndex = null; + this.newIndexForm.controls['method'].setValue(''); } + } + + onSelectingIndexStore(store: AdapterModel) { + this.selectedStoreForIndex = store; + this.newIndexForm.controls['method'].setValue(store.indexMethods[0]); + } + + getAvailableStoresForIndexes() { + effect(() => { + if (!this.availableStoresForIndexes || !this.availableStoresForIndexes()) { + return; + } + const stores = this.availableStoresForIndexes(); + + if (stores?.length > 0) { + this.selectedStoreForIndex = stores[0]; + this.newIndexForm.controls['method'].setValue(this.selectedStoreForIndex.indexMethods[0]); + } else { + this.selectedStoreForIndex = null; + } + }, {injector: this.injector}); + } + + initPlacementModal(method: Method, placement: AllocationPlacementModel) { + let store; + const preselect = this._catalog.getAllocColumns(placement.id); + this.placementMethod = method; + + store = this._catalog.getAdapter(placement.adapterId); + this.selectedStore = store; + + if (!this.selectedStore) { + return; + } + this.columnPlacement = new UntypedFormGroup({}); + this.oldColumns().forEach((v, k) => { + let state = true; + if (preselect.length > 0 && !preselect.some(e => e.name === v.name && e.placementType === PlacementType.MANUAL)) { + state = false; + } + this.columnPlacement.addControl(v.name, new UntypedFormControl(state)); + }); + this.placementModal = true; + } + + clearPlacementModal() { this.selectedStore = null; - }, error: err => { - this._toast.error('Could not ' + this.placementMethod.toLowerCase() + ' placement on store ' + this.selectedStore.name); - } - }).add(() => { - this.isAddingPlacement = false; - this.placementModal = false; - }); - } - - dropPlacement(adapterId: number) { - const store = this._catalog.getAdapter(adapterId); - this._crud.addDropPlacement(this.namespace().id, this.entity().id, store.id, Method.DROP).subscribe({ - next: (res: RelationalResult) => { - if (res.error) { - this._toast.exception(res); - } else { - this._toast.success('Dropped placement on store ' + store.name, res.query, 'Dropped placement'); - //this.getPlacementsAndPartitions(); - this.getAvailableStoresForIndexes(); - } - }, error: err => { - this._toast.error('Could not drop placement on store ' + store.name, 'Error'); - } - }); - } - - getPartitionTypes() { - this._crud.getPartitionTypes().subscribe({ - next: (res: string[]) => { - this.partitionTypes = res; - }, - error: err => { - console.log(err); - } - }); - } - - /** - * Whether the table is partitioned - */ - isPartitioned() { - return this.placements()?.length > 1; - } - - getPartitionFunctionModel() { - if (this.partitioningRequest.method === 'NONE') { - this._toast.warn('Please select a partitioning method.'); - return; - } - this.partitioningRequest.schemaName = this.namespace().name; - this.partitioningRequest.tableName = this.entity().name; - this._crud.getPartitionFunctionModel(this.partitioningRequest).subscribe({ - next: (res: PartitionFunctionModel) => { - this.partitionFunctionParams = res; - if (this.partitionFunctionParams.error) { - this._toast.warn(this.partitionFunctionParams.error); - } else { - this.partitionFunctionModal = true; - } - }, error: err => { - this.partitionFunctionParams = null; - this._toast.error('Could not get partitionFunctionParams'); - console.log(err); - } - }); - } - - /** - * Horizontally partition a table - */ - partitionTable() { - this._crud.partitionTable(this.partitionFunctionParams).subscribe({ - next: (res: RelationalResult) => { - if (res.error) { - this._toast.exception(res); - console.log(res.query); - } else { - this._toast.success('Partitioned table', res.query); - //this.getPlacementsAndPartitions(); - } - this.partitionFunctionModal = false; - }, error: err => { - this._toast.error(err); - } - }); - } - - mergePartitions() { - //const split = this.tableId.split('\.'); - const request = new PartitioningRequest(this.namespace().name, this.entity().name); - this.isMergingPartitions = true; - this._crud.mergePartitions(request).subscribe({ - next: res => { - const result = res; - if (!result.error) { - this._toast.success('Merged partitions '); - //this.getPlacementsAndPartitions(); - } else { - this._toast.exception(result); - } - }, error: err => { - this._toast.error('Could not merge partitions'); - console.log(err); - } - }).add(() => { - this.isMergingPartitions = false; - }); - } - - modifyPartitioning() { - const partitions = []; - for (let i = 0; i < this.partitionsToModify.length; i++) { - if (this.partitionsToModify[i].selected) { - partitions.push(this.partitionsToModify[i].partitionName); - } - } - //const split = this.tableId.split('\.'); - const request = new ModifyPartitionRequest(this.namespace().name, this.entity().name, partitions, this.selectedStore.name); - this._crud.modifyPartitions(request).subscribe({ - next: (res: RelationalResult) => { - if (!res.error) { - this.partitioningModal = false; - this._toast.success('Modified partitions'); - //this.getPlacementsAndPartitions(); - console.log(res.query); - } else { - this._toast.exception(res); - console.log(res.query); + } + + selectAllColumns(selectAll: boolean) { + this.oldColumns().forEach((v, k) => { + this.columnPlacement.get(v.name).setValue(selectAll); + }); + } + + addPlacement() { + const cols = []; + for (const [k, v] of Object.entries(this.columnPlacement.value)) { + //const v = this.columnPlacement.value[k]; + if (v) { + cols.push(k); + } } - }, error: err => { - this._toast.error('Could not modify the partitioning'); - } - }); - } + this.isAddingPlacement = true; + this._crud.addDropPlacement(this.namespace().id, this.entity().id, this.selectedStore.id, this.placementMethod, cols).subscribe({ + next: (res: RelationalResult) => { + if (res.error) { + this._toast.exception(res); + } else { + if (this.placementMethod === 'ADD') { + this._toast.success('Added placement on store ' + this.selectedStore.name, res.query, 'Added placement'); + } else if (this.placementMethod === 'MODIFY') { + this._toast.success('Modified placement on store ' + this.selectedStore.name, res.query, 'Modified placement'); + } + //this._catalog.updateIfNecessary(); + } + this.selectedStore = null; + }, error: err => { + this._toast.error('Could not ' + this.placementMethod.toLowerCase() + ' placement on store ' + this.selectedStore.name); + } + }).add(() => { + this.isAddingPlacement = false; + this.placementModal = false; + }); + } - initPartitioningModal(adapterId: number, partitions: AllocationPartitionModel[]) { - const store = this._catalog.getAdapter(adapterId); - this.partitionsToModify = []; + dropPlacement(adapterId: number) { + const store = this._catalog.getAdapter(adapterId); + this._crud.addDropPlacement(this.namespace().id, this.entity().id, store.id, Method.DROP).subscribe({ + next: (res: RelationalResult) => { + if (res.error) { + this._toast.exception(res); + } else { + this._toast.success('Dropped placement on store ' + store.name, res.query, 'Dropped placement'); + //this.getPlacementsAndPartitions(); + this.getAvailableStoresForIndexes(); + } + }, error: err => { + this._toast.error('Could not drop placement on store ' + store.name, 'Error'); + } + }); + } - console.log(this.partitions()); + getPartitionTypes() { + this._crud.getPartitionTypes().subscribe({ + next: (res: string[]) => { + this.partitionTypes = res; + }, + error: err => { + console.log(err); + } + }); + } - const selectedPartId = partitions.map(p => p.id); - for (const [i, partition] of this.partitions().entries()) { - this.partitionsToModify.push({ - partitionName: partition.name, - selected: selectedPartId.includes(partition.id) - }); + /** + * Whether the table is partitioned + */ + isPartitioned() { + return this.placements()?.length > 1; } - this.selectedStore = store; - this.partitioningModal = true; - } + getPartitionFunctionModel() { + if (this.partitioningRequest.method === 'NONE') { + this._toast.warn('Please select a partitioning method.'); + return; + } + this.partitioningRequest.schemaName = this.namespace().name; + this.partitioningRequest.tableName = this.entity().name; + this._crud.getPartitionFunctionModel(this.partitioningRequest).subscribe({ + next: (res: PartitionFunctionModel) => { + this.partitionFunctionParams = res; + if (this.partitionFunctionParams.error) { + this._toast.warn(this.partitionFunctionParams.error); + } else { + this.partitionFunctionModal = true; + } + }, error: err => { + this.partitionFunctionParams = null; + this._toast.error('Could not get partitionFunctionParams'); + console.log(err); + } + }); + } + + /** + * Horizontally partition a table + */ + partitionTable() { + this._crud.partitionTable(this.partitionFunctionParams).subscribe({ + next: (res: RelationalResult) => { + if (res.error) { + this._toast.exception(res); + console.log(res.query); + } else { + this._toast.success('Partitioned table', res.query); + //this.getPlacementsAndPartitions(); + } + this.partitionFunctionModal = false; + }, error: err => { + this._toast.error(err); + } + }); + } + + mergePartitions() { + //const split = this.tableId.split('\.'); + const request = new PartitioningRequest(this.namespace().name, this.entity().name); + this.isMergingPartitions = true; + this._crud.mergePartitions(request).subscribe({ + next: res => { + const result = res; + if (!result.error) { + this._toast.success('Merged partitions '); + //this.getPlacementsAndPartitions(); + } else { + this._toast.exception(result); + } + }, error: err => { + this._toast.error('Could not merge partitions'); + console.log(err); + } + }).add(() => { + this.isMergingPartitions = false; + }); + } + + modifyPartitioning() { + const partitions = []; + for (let i = 0; i < this.partitionsToModify.length; i++) { + if (this.partitionsToModify[i].selected) { + partitions.push(this.partitionsToModify[i].partitionName); + } + } + //const split = this.tableId.split('\.'); + const request = new ModifyPartitionRequest(this.namespace().name, this.entity().name, partitions, this.selectedStore.name); + this._crud.modifyPartitions(request).subscribe({ + next: (res: RelationalResult) => { + if (!res.error) { + this.partitioningModal = false; + this._toast.success('Modified partitions'); + //this.getPlacementsAndPartitions(); + console.log(res.query); + } else { + this._toast.exception(res); + console.log(res.query); + } + }, error: err => { + this._toast.error('Could not modify the partitioning'); + } + }); + } + + initPartitioningModal(adapterId: number, partitions: AllocationPartitionModel[]) { + const store = this._catalog.getAdapter(adapterId); + this.partitionsToModify = []; + + console.log(this.partitions()); + + const selectedPartId = partitions.map(p => p.id); + for (const [i, partition] of this.partitions().entries()) { + this.partitionsToModify.push({ + partitionName: partition.name, + selected: selectedPartId.includes(partition.id) + }); + } + + this.selectedStore = store; + this.partitioningModal = true; + } + + clearPartitioningModal() { + this.selectedStore = null; + } + + selectAllPartitions(select: boolean) { + for (const p of this.partitionsToModify) { + p.selected = select; + } + } + + dropIndex(index: string) { + this._crud.dropIndex(new IndexModel(this.namespace().id, this.entity().id, index, null, null, null)).subscribe({ + next: (res: RelationalResult) => { + if (!res.error) { + //this._catalog.updateIfNecessary(); + } else { + this._toast.exception(res, 'Could not drop index:'); + } + }, error: err => { + console.log(err); + } + }); + } - clearPartitioningModal() { - this.selectedStore = null; - } + addIndex() { + this.indexSubmitted = true; + const newCols: number[] = []; + for (const [k, v] of Object.entries(this.newIndexCols)) { + if (v) { + newCols.push(this.oldColumns().get(k)?.id); + } + } + if (this.newIndexForm.controls['method'].valid && this.newIndexForm.controls['name'].errors && newCols.length > 0) { + this.newIndexForm.controls['name'].setValue(this.proposedIndexName); + } + if (this.newIndexForm.valid && newCols.length > 0 && this.selectedStoreForIndex != null) { + const i = this.newIndexForm.value; + console.log(i.method); + const index = new IndexModel(this.namespace().id, this.entity().id, i.name, this.selectedStoreForIndex.name, i.method, newCols); + this.addingIndex = true; + this._crud.createIndex(index).subscribe({ + next: (res: RelationalResult) => { + if (!res.error) { + //this._catalog.updateIfNecessary(); + this.getGeneratedNames(); + this.newIndexForm.reset({name: '', method: ''}); + this.initNewIndexValues(); + this.newIndexCols = new Map(Array.from(this.oldColumns().values()).map(c => [c.name, false])); + this.indexSubmitted = false; + } else { + this._toast.exception(res, 'Could not create index:'); + } + }, error: err => { + console.log(err); + } + }).add(() => this.addingIndex = false); + } + } - selectAllPartitions(select: boolean) { - for (const p of this.partitionsToModify) { - p.selected = select; + getGeneratedNames() { + this._crud.getGeneratedNames().subscribe({ + next: (names: RelationalResult) => { + if (names != null && !names.error) { + this.proposedConstraintName = names.data[0][0]; + this.proposedIndexName = names.data[0][2]; + } + }, error: err => { + console.log(err); + } + }); } - } - dropIndex(index: string) { - this._crud.dropIndex(new IndexModel(this.namespace().id, this.entity().id, index, null, null, null)).subscribe({ - next: (res: RelationalResult) => { - if (!res.error) { - //this._catalog.updateIfNecessary(); + inputValidation(key) { + if (this.newIndexForm.controls[key].value === '') { + return ''; + } else if (this.newIndexForm.controls[key].valid) { + return {'is-valid': true}; } else { - this._toast.exception(res, 'Could not drop index:'); - } - }, error: err => { - console.log(err); - } - }); - } - - addIndex() { - this.indexSubmitted = true; - const newCols: number[] = []; - for (const [k, v] of Object.entries(this.newIndexCols)) { - if (v) { - newCols.push(this.oldColumns().get(k)?.id); - } - } - if (this.newIndexForm.controls['method'].valid && this.newIndexForm.controls['name'].errors && newCols.length > 0) { - this.newIndexForm.controls['name'].setValue(this.proposedIndexName); - } - if (this.newIndexForm.valid && newCols.length > 0 && this.selectedStoreForIndex != null) { - const i = this.newIndexForm.value; - console.log(i.method) - const index = new IndexModel(this.namespace().id, this.entity().id, i.name, this.selectedStoreForIndex.name, i.method, newCols); - this.addingIndex = true; - this._crud.createIndex(index).subscribe({ - next: (res: RelationalResult) => { - if (!res.error) { - //this._catalog.updateIfNecessary(); - this.getGeneratedNames(); - this.newIndexForm.reset({name: '', method: ''}); - this.initNewIndexValues(); - this.newIndexCols = new Map(Array.from(this.oldColumns().values()).map(c => [c.name, false])); - this.indexSubmitted = false; - } else { - this._toast.exception(res, 'Could not create index:'); - } - }, error: err => { - console.log(err); - } - }).add(() => this.addingIndex = false); - } - } - - getGeneratedNames() { - this._crud.getGeneratedNames().subscribe({ - next: (names: RelationalResult) => { - if (names != null && !names.error) { - this.proposedConstraintName = names.data[0][0]; - this.proposedIndexName = names.data[0][2]; - } - }, error: err => { - console.log(err); - } - }); - } - - inputValidation(key) { - if (this.newIndexForm.controls[key].value === '') { - return ''; - } else if (this.newIndexForm.controls[key].valid) { - return {'is-valid': true}; - } else { - return {'is-invalid': true}; - } - } - - onTypeChange() { - this.updateColumn.controls['defaultValue'].setValue(null); - } - - onTypeChange2(col: UiColumnDefinition) { - if (col.defaultValue !== null) { - this.assignDefault(col, false); - } - col.precision = null; - col.scale = null; - } - - validate(defaultValue) { - if (defaultValue === null) { - return ''; - } else if (isNaN(defaultValue) || defaultValue === '') { - return 'is-invalid'; - } else { - return 'is-valid'; - } - } - - validatePartitionModification() { - if (this.partitionsToModify) { - for (const p of this.partitionsToModify) { - if (p.selected) { - return true; - } - } - } - return false; - } - - getOrNull(index: number) { - return this.materializedInfo()[index] || null; - } - - getColumnsOfKey(constraint: ConstraintModel): Signal { - return computed(() => this._catalog.getKey(constraint.keyId).columnIds); - - } - - getArray(values: IterableIterator) { - return Array.from(values); - } + return {'is-invalid': true}; + } + } + + onTypeChange() { + this.updateColumn.controls['defaultValue'].setValue(null); + } + + onTypeChange2(col: UiColumnDefinition) { + if (col.defaultValue !== null) { + this.assignDefault(col, false); + } + col.precision = null; + col.scale = null; + } + + validate(defaultValue) { + if (defaultValue === null) { + return ''; + } else if (isNaN(defaultValue) || defaultValue === '') { + return 'is-invalid'; + } else { + return 'is-valid'; + } + } + + validatePartitionModification() { + if (this.partitionsToModify) { + for (const p of this.partitionsToModify) { + if (p.selected) { + return true; + } + } + } + return false; + } + + getOrNull(index: number) { + return this.materializedInfo()[index] || null; + } + + getColumnsOfKey(constraint: ConstraintModel): Signal { + return computed(() => this._catalog.getKey(constraint.keyId).columnIds); + + } + + getArray(values: IterableIterator) { + return Array.from(values); + } } diff --git a/src/app/views/schema-editing/edit-entity/edit-entity.component.ts b/src/app/views/schema-editing/edit-entity/edit-entity.component.ts index 6a8cc0d6..09f31bb2 100644 --- a/src/app/views/schema-editing/edit-entity/edit-entity.component.ts +++ b/src/app/views/schema-editing/edit-entity/edit-entity.component.ts @@ -2,111 +2,118 @@ import {Component, computed, inject, Input, Signal} from '@angular/core'; import {Router} from '@angular/router'; import {DbmsTypesService} from '../../../services/dbms-types.service'; import {CatalogService} from '../../../services/catalog.service'; -import {AllocationEntityModel, AllocationPartitionModel, AllocationPlacementModel, EntityModel, EntityType, NamespaceModel} from '../../../models/catalog.model'; +import { + AllocationEntityModel, + AllocationPartitionModel, + AllocationPlacementModel, + EntityModel, + EntityType, + NamespaceModel +} from '../../../models/catalog.model'; import {DataModel} from '../../../models/ui-request.model'; import {AdapterModel} from '../../adapters/adapter.model'; @Component({ - selector: 'app-edit-entity', - templateUrl: './edit-entity.component.html', - styleUrls: ['./edit-entity.component.scss'] + selector: 'app-edit-entity', + templateUrl: './edit-entity.component.html', + styleUrls: ['./edit-entity.component.scss'] }) export class EditEntityComponent { - @Input() - readonly currentRoute: Signal; - - readonly entity: Signal; - readonly namespace: Signal; - readonly placements: Signal; - readonly partitions: Signal; - readonly allocations: Signal; - readonly stores: Signal; - readonly addableStores: Signal; - - - protected readonly NamespaceType = DataModel; - - protected readonly EntityType = EntityType; - - public readonly _router = inject(Router); - public readonly _types = inject(DbmsTypesService); - public readonly _catalog = inject(CatalogService); - - - constructor() { - this.namespace = computed(() => { - const catalog = this._catalog.listener(); - const route = this.currentRoute(); - if (!route) { - return null; - } - const splits = this.currentRoute().split('\.'); - return this._catalog.getNamespaceFromName(splits[0]); - }); - - this.entity = computed(() => { - const catalog = this._catalog.listener(); - const route = this.currentRoute(); - if (!route) { - return null; - } - - const splits = this.currentRoute().split('\.'); - - if (this.namespace && this.namespace() && this.namespace().dataModel === DataModel.GRAPH) { - return this._catalog.getEntityFromName(splits[0], splits[0]); - } - - return this._catalog.getEntityFromName(splits[0], splits[1]) as EntityModel; - }); - - this.stores = computed(() => { - const catalog = this._catalog.listener(); - return this._catalog.getStores(); - }); - - this.addableStores = computed(() => { - const catalog = this._catalog.listener(); - const stores = this.stores(); - const placements = this.placements(); - if (!stores || !placements) { - return []; - } - const adapterIds = placements.map(p => p.adapterId); - return stores.filter(s => !adapterIds.includes(s.id)); - }); - - this.placements = computed(() => { - const catalog = this._catalog.listener(); - const entity = this.entity(); - if (!entity) { - return; - } - - return this._catalog.getPlacements(entity.id); - }); - - this.partitions = computed(() => { - const catalog = this._catalog.listener(); - if (!this.entity || !this.entity()) { - return []; - } - return this._catalog.getPartitions(this.entity().id); - }); - - this.allocations = computed(() => { - const catalog = this._catalog.listener(); - if (!this.entity || !this.entity()) { - return []; - } - return this._catalog.getAllocations(this.entity().id); - }); - } - - - isStatistic() { - return this._router.url.includes('statistics'); - } + @Input() + readonly currentRoute: Signal; + + readonly entity: Signal; + readonly namespace: Signal; + readonly placements: Signal; + readonly partitions: Signal; + readonly allocations: Signal; + readonly stores: Signal; + readonly addableStores: Signal; + + + protected readonly NamespaceType = DataModel; + + protected readonly EntityType = EntityType; + + public readonly _router = inject(Router); + public readonly _types = inject(DbmsTypesService); + public readonly _catalog = inject(CatalogService); + + + constructor() { + this.namespace = computed(() => { + const catalog = this._catalog.listener(); + const route = this.currentRoute(); + if (!route) { + return null; + } + const splits = this.currentRoute().split('\.'); + return this._catalog.getNamespaceFromName(splits[0]); + }); + + this.entity = computed(() => { + const catalog = this._catalog.listener(); + const route = this.currentRoute(); + if (!route) { + return null; + } + + const splits = this.currentRoute().split('\.'); + + if (this.namespace && this.namespace() && this.namespace().dataModel === DataModel.GRAPH) { + return this._catalog.getEntityFromName(splits[0], splits[0]); + } + + return this._catalog.getEntityFromName(splits[0], splits[1]) as EntityModel; + }); + + this.stores = computed(() => { + const catalog = this._catalog.listener(); + return this._catalog.getStores(); + }); + + this.addableStores = computed(() => { + const catalog = this._catalog.listener(); + const stores = this.stores(); + const placements = this.placements(); + if (!stores || !placements) { + return []; + } + const adapterIds = placements.map(p => p.adapterId); + return stores.filter(s => !adapterIds.includes(s.id)); + }); + + this.placements = computed(() => { + const catalog = this._catalog.listener(); + const entity = this.entity(); + if (!entity) { + return; + } + + return this._catalog.getPlacements(entity.id); + }); + + this.partitions = computed(() => { + const catalog = this._catalog.listener(); + if (!this.entity || !this.entity()) { + return []; + } + return this._catalog.getPartitions(this.entity().id); + }); + + this.allocations = computed(() => { + const catalog = this._catalog.listener(); + if (!this.entity || !this.entity()) { + return []; + } + return this._catalog.getAllocations(this.entity().id); + }); + } + + + isStatistic() { + return this._router.url.includes('statistics'); + } } diff --git a/src/app/views/schema-editing/edit-source-columns/edit-source-columns.component.ts b/src/app/views/schema-editing/edit-source-columns/edit-source-columns.component.ts index 8a4e2eb2..1c54dbc0 100644 --- a/src/app/views/schema-editing/edit-source-columns/edit-source-columns.component.ts +++ b/src/app/views/schema-editing/edit-source-columns/edit-source-columns.component.ts @@ -9,194 +9,202 @@ import {BehaviorSubject, Observable, Subscription} from 'rxjs'; import {DbmsTypesService} from '../../../services/dbms-types.service'; import {ForeignKey} from '../../../views/uml/uml.model'; import {CatalogService} from '../../../services/catalog.service'; -import {AllocationEntityModel, AllocationPartitionModel, AllocationPlacementModel, EntityType, ForeignKeyModel, NamespaceModel, TableModel} from '../../../models/catalog.model'; +import { + AllocationEntityModel, + AllocationPartitionModel, + AllocationPlacementModel, + EntityType, + ForeignKeyModel, + NamespaceModel, + TableModel +} from '../../../models/catalog.model'; import {AdapterModel} from '../../adapters/adapter.model'; @Component({ - selector: 'app-edit-source-columns', - templateUrl: './edit-source-columns.component.html', - styleUrls: ['./edit-source-columns.component.scss'] + selector: 'app-edit-source-columns', + templateUrl: './edit-source-columns.component.html', + styleUrls: ['./edit-source-columns.component.scss'] }) export class EditSourceColumnsComponent implements OnInit, OnDestroy { - private readonly _crud = inject(CrudService); - private readonly _route = inject(ActivatedRoute); - private readonly _toast = inject(ToasterService); - public readonly _types = inject(DbmsTypesService); - public readonly _catalog = inject(CatalogService); - - constructor() { - - this.foreignKeys = computed(() => { - const catalog = this._catalog.listener(); - const namespace = this.namespace(); - const entity = this.entity(); - if (!namespace || !entity) { - return this.foreignKeys(); - } - - const fks = new Map(); - this._catalog.getKeys(entity.id).filter(k => !k.isPrimary).map(k => k).forEach(k => { - fks.set(catalog.getConstraintName(k.id), k); - return [...fks.values()]; - }); - }); - - - this.columns = computed(() => { - const catalog = this._catalog.listener(); - if (!this.entity) { - return []; - } - - const entity = this.entity(); - if (!entity) { - return this.columns(); - } - const columns = this._catalog.getColumns(entity.id); - - return columns.map(c => { - const primaries: number[] = this._catalog.getPrimaryKey(c.entityId)?.columnIds || []; - return UiColumnDefinition.fromModel(c, primaries); - }); - }); - } - - @Input() - readonly entity: Signal; - @Input() - readonly namespace: Signal; - @Input() - readonly currentRoute: Signal; - - @Input() - readonly placements: Signal; - @Input() - readonly partitions: Signal; - @Input() - readonly allocations: Signal; - @Input() - readonly stores: Signal; - @Input() - readonly addableStores: Signal; - - readonly columns: Signal; - readonly foreignKeys: Signal; - errorMsg: string; - editingCol: string; - subscriptions = new Subscription(); - - underlyingTables: WritableSignal<{}> = signal(null); - - public readonly EntityType = EntityType; - - ngOnInit(): void { - //this.getPlacements(); - const self = this; - $(document).on('click', function (e) { - if ($(e.target).hasClass('rename') || $(e.target).hasClass('add-col')) { - return; - } - if ($(e.target).parents('.editing').length === 0) { - self.editingCol = undefined; - } - }); - } - - ngOnDestroy() { - $(document).off('click'); - this.subscriptions.unsubscribe(); - } - - - getAddableColumns(): Observable { - const cols: UiColumnDefinition[] = []; - - for (const col of this.columns()) { - if (!this._catalog.getColumns(this.entity().id).find(h => h.name === col.name)) { - cols.push(col); - } + private readonly _crud = inject(CrudService); + private readonly _route = inject(ActivatedRoute); + private readonly _toast = inject(ToasterService); + public readonly _types = inject(DbmsTypesService); + public readonly _catalog = inject(CatalogService); + + constructor() { + + this.foreignKeys = computed(() => { + const catalog = this._catalog.listener(); + const namespace = this.namespace(); + const entity = this.entity(); + if (!namespace || !entity) { + return this.foreignKeys(); + } + + const fks = new Map(); + this._catalog.getKeys(entity.id).filter(k => !k.isPrimary).map(k => k).forEach(k => { + fks.set(catalog.getConstraintName(k.id), k); + return [...fks.values()]; + }); + }); + + + this.columns = computed(() => { + const catalog = this._catalog.listener(); + if (!this.entity) { + return []; + } + + const entity = this.entity(); + if (!entity) { + return this.columns(); + } + const columns = this._catalog.getColumns(entity.id); + + return columns.map(c => { + const primaries: number[] = this._catalog.getPrimaryKey(c.entityId)?.columnIds || []; + return UiColumnDefinition.fromModel(c, primaries); + }); + }); } - return new BehaviorSubject(cols); - } - - dropColumn(col: UiColumnDefinition) { - console.log(col); - const oldColumn = new ColumnRequest(this.entity().id, col); - console.log(oldColumn); - this._crud.dropColumn(oldColumn).subscribe({ - next: (res: RelationalResult) => { - if (res.error) { - this._toast.exception(res); - } else { - this._toast.success('The source column was dropped'); + @Input() + readonly entity: Signal; + @Input() + readonly namespace: Signal; + @Input() + readonly currentRoute: Signal; + + @Input() + readonly placements: Signal; + @Input() + readonly partitions: Signal; + @Input() + readonly allocations: Signal; + @Input() + readonly stores: Signal; + @Input() + readonly addableStores: Signal; + + readonly columns: Signal; + readonly foreignKeys: Signal; + errorMsg: string; + editingCol: string; + subscriptions = new Subscription(); + + underlyingTables: WritableSignal<{}> = signal(null); + + public readonly EntityType = EntityType; + + ngOnInit(): void { + //this.getPlacements(); + const self = this; + $(document).on('click', function (e) { + if ($(e.target).hasClass('rename') || $(e.target).hasClass('add-col')) { + return; + } + if ($(e.target).parents('.editing').length === 0) { + self.editingCol = undefined; + } + }); + } + + ngOnDestroy() { + $(document).off('click'); + this.subscriptions.unsubscribe(); + } + + + getAddableColumns(): Observable { + const cols: UiColumnDefinition[] = []; + + for (const col of this.columns()) { + if (!this._catalog.getColumns(this.entity().id).find(h => h.name === col.name)) { + cols.push(col); + } } - //this._catalog.updateIfNecessary(); - }, error: err => { - console.log(err); - } - }); - } - - renameColumn(input: HTMLInputElement, oldCol: UiColumnDefinition, newName: string, tableType: string) { - if (newName.trim() === '') { - this._toast.error('Name can not be empty.'); - return; + + return new BehaviorSubject(cols); + } + + dropColumn(col: UiColumnDefinition) { + console.log(col); + const oldColumn = new ColumnRequest(this.entity().id, col); + console.log(oldColumn); + this._crud.dropColumn(oldColumn).subscribe({ + next: (res: RelationalResult) => { + if (res.error) { + this._toast.exception(res); + } else { + this._toast.success('The source column was dropped'); + } + //this._catalog.updateIfNecessary(); + }, error: err => { + console.log(err); + } + }); } - const newCol = Object.assign({}, oldCol); - newCol.name = newName; - console.log(newCol) - const request = new ColumnRequest(this.entity().id, oldCol, newCol, true, tableType); - this._crud.updateColumn(request).subscribe({ - next: (res: RelationalResult) => { - if (res.error) { - this._toast.exception(res); - } else { - this._toast.success('Renamed column "' + oldCol.name + '" to "' + newName + '"'); + + renameColumn(input: HTMLInputElement, oldCol: UiColumnDefinition, newName: string, tableType: string) { + if (newName.trim() === '') { + this._toast.error('Name can not be empty.'); + return; } - this.editingCol = undefined; - input.value = ''; - //this._catalog.updateIfNecessary(); - }, error: err => { - this._toast.error('Could not rename the column "' + oldCol.name + '" to "' + newName + '"'); - console.log(err); - } - }); - } - - addColumn(col: UiColumnDefinition, newName: string, newDefault: string) { - const request = new ColumnRequest(this.entity().id, null, new UiColumnDefinition(-1, col.name, null, null, col.dataType, '', null, null, newDefault, -1, -1, newName)); - this._crud.createColumn(request).subscribe({ - next: res => { - const result = res; - if (result.error) { - this._toast.exception(result); - } else { - this._toast.success('Added column "' + newName + '"'); + const newCol = Object.assign({}, oldCol); + newCol.name = newName; + console.log(newCol); + const request = new ColumnRequest(this.entity().id, oldCol, newCol, true, tableType); + this._crud.updateColumn(request).subscribe({ + next: (res: RelationalResult) => { + if (res.error) { + this._toast.exception(res); + } else { + this._toast.success('Renamed column "' + oldCol.name + '" to "' + newName + '"'); + } + this.editingCol = undefined; + input.value = ''; + //this._catalog.updateIfNecessary(); + }, error: err => { + this._toast.error('Could not rename the column "' + oldCol.name + '" to "' + newName + '"'); + console.log(err); + } + }); + } + + addColumn(col: UiColumnDefinition, newName: string, newDefault: string) { + const request = new ColumnRequest(this.entity().id, null, new UiColumnDefinition(-1, col.name, null, null, col.dataType, '', null, null, newDefault, -1, -1, newName)); + this._crud.createColumn(request).subscribe({ + next: res => { + const result = res; + if (result.error) { + this._toast.exception(result); + } else { + this._toast.success('Added column "' + newName + '"'); + } + //this._catalog.updateIfNecessary(); + this.editingCol = undefined; + }, error: err => { + this._toast.error('Could not add the column "' + newName + '"'); + console.log(err); + } + }); + } + + + validTableName(name: string): boolean { + if (name.trim() === '') { + return false; } - //this._catalog.updateIfNecessary(); - this.editingCol = undefined; - }, error: err => { - this._toast.error('Could not add the column "' + newName + '"'); - console.log(err); - } - }); - } - - - validTableName(name: string): boolean { - if (name.trim() === '') { - return false; + return true; } - return true; - } - getTitle() { - return this._route.params['id']; - } + getTitle() { + return this._route.params['id']; + } - getAdapters(): Signal { - return computed(() => this.placements()?.map(a => this._catalog.getAdapter(a.adapterId)).filter(a => a)); - } + getAdapters(): Signal { + return computed(() => this.placements()?.map(a => this._catalog.getAdapter(a.adapterId)).filter(a => a)); + } } diff --git a/src/app/views/schema-editing/edit-tables/edit-tables.component.ts b/src/app/views/schema-editing/edit-tables/edit-tables.component.ts index a199a45f..b2de8ebd 100644 --- a/src/app/views/schema-editing/edit-tables/edit-tables.component.ts +++ b/src/app/views/schema-editing/edit-tables/edit-tables.component.ts @@ -1,8 +1,26 @@ -import {Component, computed, ElementRef, inject, Input, OnDestroy, OnInit, QueryList, Renderer2, Signal, ViewChildren} from '@angular/core'; +import { + Component, + computed, + ElementRef, + inject, + Input, + OnDestroy, + OnInit, + QueryList, + Renderer2, + Signal, + ViewChildren +} from '@angular/core'; import {CrudService} from '../../../services/crud.service'; import {EditTableRequest} from '../../../models/ui-request.model'; import {Router} from '@angular/router'; -import {EntityMeta, PolyType, RelationalResult, Status, UiColumnDefinition} from '../../../components/data-view/models/result-set.model'; +import { + EntityMeta, + PolyType, + RelationalResult, + Status, + UiColumnDefinition +} from '../../../components/data-view/models/result-set.model'; import {ToastDuration, ToasterService} from '../../../components/toast-exposer/toaster.service'; import {LeftSidebarService} from '../../../components/left-sidebar/left-sidebar.service'; import {DbmsTypesService} from '../../../services/dbms-types.service'; @@ -11,364 +29,371 @@ import {Subscription} from 'rxjs'; import {DbTable} from '../../uml/uml.model'; import {BreadcrumbService} from '../../../components/breadcrumb/breadcrumb.service'; import {CatalogService} from '../../../services/catalog.service'; -import {AllocationEntityModel, AllocationPartitionModel, AllocationPlacementModel, EntityType, NamespaceModel, TableModel} from '../../../models/catalog.model'; +import { + AllocationEntityModel, + AllocationPartitionModel, + AllocationPlacementModel, + EntityType, + NamespaceModel, + TableModel +} from '../../../models/catalog.model'; import {AdapterModel} from '../../adapters/adapter.model'; const INITIAL_TYPE = 'BIGINT'; @Component({ - selector: 'app-edit-tables', - templateUrl: './edit-tables.component.html', - styleUrls: ['./edit-tables.component.scss'] + selector: 'app-edit-tables', + templateUrl: './edit-tables.component.html', + styleUrls: ['./edit-tables.component.scss'] }) export class EditTablesComponent implements OnInit, OnDestroy { - public readonly _crud = inject(CrudService); - public readonly _types = inject(DbmsTypesService); - public readonly _catalog = inject(CatalogService); - public readonly _breadcrumb = inject(BreadcrumbService); - private readonly _toast = inject(ToasterService); - private readonly _router = inject(Router); - private readonly _leftSidebar = inject(LeftSidebarService); - private readonly _settings = inject(WebuiSettingsService); - private readonly _render = inject(Renderer2); - - @ViewChildren('editing', {read: ElementRef}) inputGroup: QueryList; - types: PolyType[] = []; - - @Input() - readonly entity: Signal; - @Input() - readonly namespace: Signal; - @Input() - readonly currentRoute: Signal; - - @Input() - readonly placements: Signal; - @Input() - readonly partitions: Signal; - @Input() - readonly allocations: Signal; - @Input() - readonly stores: Signal; - @Input() - readonly addableStores: Signal; - - readonly tables: Signal; //= signal([]); - - counter = 0; - newColumns = new Map(); - newTableName = ''; - selectedStore: AdapterModel; - creatingTable = false; - - editOpen = false; - - //export table - exportProgress = 0.0; - private subscriptions = new Subscription(); - - constructor() { - this._render.listen('document', 'click', (e: Event) => { - if (!this.inputGroup || this.inputGroup.length === 0) { - return; - } - if (this.editOpen && !this.inputGroup.get(0).nativeElement.contains(e.target)) { - this.tables().map(t => { - t.editing = false; - return t; + public readonly _crud = inject(CrudService); + public readonly _types = inject(DbmsTypesService); + public readonly _catalog = inject(CatalogService); + public readonly _breadcrumb = inject(BreadcrumbService); + private readonly _toast = inject(ToasterService); + private readonly _router = inject(Router); + private readonly _leftSidebar = inject(LeftSidebarService); + private readonly _settings = inject(WebuiSettingsService); + private readonly _render = inject(Renderer2); + + @ViewChildren('editing', {read: ElementRef}) inputGroup: QueryList; + types: PolyType[] = []; + + @Input() + readonly entity: Signal; + @Input() + readonly namespace: Signal; + @Input() + readonly currentRoute: Signal; + + @Input() + readonly placements: Signal; + @Input() + readonly partitions: Signal; + @Input() + readonly allocations: Signal; + @Input() + readonly stores: Signal; + @Input() + readonly addableStores: Signal; + + readonly tables: Signal; //= signal([]); + + counter = 0; + newColumns = new Map(); + newTableName = ''; + selectedStore: AdapterModel; + creatingTable = false; + + editOpen = false; + + //export table + exportProgress = 0.0; + private subscriptions = new Subscription(); + + constructor() { + this._render.listen('document', 'click', (e: Event) => { + if (!this.inputGroup || this.inputGroup.length === 0) { + return; + } + if (this.editOpen && !this.inputGroup.get(0).nativeElement.contains(e.target)) { + this.tables().map(t => { + t.editing = false; + return t; + }); + this.editOpen = false; + } else { + this.editOpen = true; + } }); - this.editOpen = false; - } else { - this.editOpen = true; - } - }); - - this.tables = computed(() => { - const catalog = this._catalog.listener(); - if (!this.namespace) { - return; - } - const namespace = this.namespace(); - if (!namespace) { - return; - } - const entities = this._catalog.getEntities(namespace.id); - return entities.map(e => Table.fromModel(e)).sort((a, b) => a.name.localeCompare(b.name)); - }); - } - - ngOnInit() { - this.newColumns.set(this.counter++, new UiColumnDefinition(-1, '', true, false, INITIAL_TYPE, '', null, null)); - this.getTypeInfo(); - - this.initSocket(); - const sub2 = this._crud.onReconnection().subscribe((b) => { - if (b) { - this.onReconnect(); - } - }); - this.subscriptions.add(sub2); - } - - ngOnDestroy() { - this.subscriptions.unsubscribe(); - } - - onReconnect() { - //this._catalog.updateIfNecessary(); - this.getTypeInfo(); - this._leftSidebar.setSchema(this._router, '/views/schema-editing/', true, 2, false); - } - - - /** - * get the right class for the 'drop' and 'truncate' buttons - * enable the button if the confirm-text is equal to the table-name or to 'drop table-name' respectively 'truncate table-name' - */ - dropTruncateClass(action: string, table: Table) { - if (action === 'drop' && (table.drop === table.name || table.drop === 'drop ' + table.name)) { - return 'btn-danger'; - } else if (action === 'truncate' && (table.truncate === table.name || table.truncate === 'truncate ' + table.name)) { - return 'btn-danger'; + + this.tables = computed(() => { + const catalog = this._catalog.listener(); + if (!this.namespace) { + return; + } + const namespace = this.namespace(); + if (!namespace) { + return; + } + const entities = this._catalog.getEntities(namespace.id); + return entities.map(e => Table.fromModel(e)).sort((a, b) => a.name.localeCompare(b.name)); + }); + } + + ngOnInit() { + this.newColumns.set(this.counter++, new UiColumnDefinition(-1, '', true, false, INITIAL_TYPE, '', null, null)); + this.getTypeInfo(); + + this.initSocket(); + const sub2 = this._crud.onReconnection().subscribe((b) => { + if (b) { + this.onReconnect(); + } + }); + this.subscriptions.add(sub2); } - return 'btn-light disabled'; - } - - /** - * send a request to either drop or truncate a table - */ - sendDropTruncateRequest(action, table: Table) { - let request; - let type: string; - if (this.dropTruncateClass(action, table) === 'btn-danger') { - if (table.tableType !== EntityType.VIEW) { - request = new EditTableRequest(this.namespace().id, table.id, null, action); - console.log(request); - type = ' the Table '; - } else { - request = new EditTableRequest(this.namespace().id, table.id, null, action, null, null, EntityType.VIEW); - type = ' the View '; - } - - } else { - return; + + ngOnDestroy() { + this.subscriptions.unsubscribe(); + } + + onReconnect() { + //this._catalog.updateIfNecessary(); + this.getTypeInfo(); + this._leftSidebar.setSchema(this._router, '/views/schema-editing/', true, 2, false); } - this._crud.dropTruncateTable(request).subscribe({ - next: (result: RelationalResult) => { - if (result.error) { - this._toast.exception(result, 'Could not ' + action + type + table + ':'); + + /** + * get the right class for the 'drop' and 'truncate' buttons + * enable the button if the confirm-text is equal to the table-name or to 'drop table-name' respectively 'truncate table-name' + */ + dropTruncateClass(action: string, table: Table) { + if (action === 'drop' && (table.drop === table.name || table.drop === 'drop ' + table.name)) { + return 'btn-danger'; + } else if (action === 'truncate' && (table.truncate === table.name || table.truncate === 'truncate ' + table.name)) { + return 'btn-danger'; + } + return 'btn-light disabled'; + } + + /** + * send a request to either drop or truncate a table + */ + sendDropTruncateRequest(action, table: Table) { + let request; + let type: string; + if (this.dropTruncateClass(action, table) === 'btn-danger') { + if (table.tableType !== EntityType.VIEW) { + request = new EditTableRequest(this.namespace().id, table.id, null, action); + console.log(request); + type = ' the Table '; + } else { + request = new EditTableRequest(this.namespace().id, table.id, null, action, null, null, EntityType.VIEW); + type = ' the View '; + } + } else { - let toastAction = 'Truncated'; - if (request.getAction() === 'drop') { - toastAction = 'Dropped'; - this._leftSidebar.setSchema(this._router, '/views/schema-editing/', true, 2, false); - } - this._toast.success(toastAction + type + request.table); - //this._catalog.updateIfNecessary(); + return; } - }, error: err => { - this._toast.error('Could not ' + action + type + table + ' due to an unknown error'); - console.log(err); - } - }); - } - - createTable() { - if (this.newTableName === '') { - this._toast.warn('Please provide a name for the new table. The new table was not created.', 'missing table name', ToastDuration.INFINITE); - return; + this._crud.dropTruncateTable(request).subscribe({ + next: (result: RelationalResult) => { + + if (result.error) { + this._toast.exception(result, 'Could not ' + action + type + table + ':'); + } else { + let toastAction = 'Truncated'; + if (request.getAction() === 'drop') { + toastAction = 'Dropped'; + this._leftSidebar.setSchema(this._router, '/views/schema-editing/', true, 2, false); + } + this._toast.success(toastAction + type + request.table); + //this._catalog.updateIfNecessary(); + } + }, error: err => { + this._toast.error('Could not ' + action + type + table + ' due to an unknown error'); + console.log(err); + } + }); } - if (!this._crud.nameIsValid(this.newTableName)) { - this._toast.warn('Please provide a valid name for the new table. The new table was not created.', 'invalid table name', ToastDuration.INFINITE); - return; + + createTable() { + if (this.newTableName === '') { + this._toast.warn('Please provide a name for the new table. The new table was not created.', 'missing table name', ToastDuration.INFINITE); + return; + } + if (!this._crud.nameIsValid(this.newTableName)) { + this._toast.warn('Please provide a valid name for the new table. The new table was not created.', 'invalid table name', ToastDuration.INFINITE); + return; + } + if (this.tables().filter((t) => { + return this.namespace().caseSensitive ? t.name === this.newTableName : t.name.toLowerCase() === this.newTableName.toLowerCase(); + }).length > 0) { + this._toast.warn('A table with this name already exists. Please choose another name.', 'invalid table name', ToastDuration.INFINITE); + return; + } + let valid = true; + //clear precision/scale for types where it is not applicable + //delete columns with no column name + let hasPk = false; + this.newColumns.forEach((v, k) => { + if (!this._types.supportsPrecision(v.dataType) && v.precision !== null) { + v.precision = null; + } + if (!this._types.supportsScale(v.dataType) && v.scale !== null) { + v.scale = null; + } + //clear cardinality and dimension if it is not an array + if (v.collectionsType !== 'ARRAY') { + v.cardinality = null; + v.dimension = null; + } + if (v.name === '') { + this.newColumns.delete(k); + } + if (!this._crud.nameIsValid(v.name)) { + valid = false; + return; + } + + if (v.primary) { + hasPk = true; + } + }); + if (!hasPk) { + this._toast.warn('Please specify a primary key. The new table was not created.', 'missing primary key', ToastDuration.INFINITE); + return; + } + if (!valid) { + this._toast.warn('Please make sure all column names are valid. The new table was not created.', 'invalid column name', ToastDuration.INFINITE); + return; + } + const request = new EditTableRequest(this.namespace().id, null, this.newTableName, 'create', Array.from(this.newColumns.values()), this.selectedStore?.id); + this.creatingTable = true; + this._crud.createTable(request).subscribe({ + next: (result: RelationalResult) => { + if (result.error) { + this._toast.exception(result, 'Could not generate table:'); + } else { + this._toast.success('Generated table ' + request.entityName, result.query); + this.newColumns.clear(); + this.counter = 0; + this.newColumns.set(this.counter++, new UiColumnDefinition(-1, '', true, false, INITIAL_TYPE, '', null, null)); + this.newTableName = ''; + this.selectedStore = null; + this._leftSidebar.setSchema(this._router, '/views/schema-editing/', true, 2, false); + } + //this._catalog.updateIfNecessary(); + }, error: err => { + this._toast.error('Could not generate table'); + console.log(err); + } + }).add(() => this.creatingTable = false); } - if (this.tables().filter((t) => { - return this.namespace().caseSensitive ? t.name === this.newTableName : t.name.toLowerCase() === this.newTableName.toLowerCase(); - }).length > 0) { - this._toast.warn('A table with this name already exists. Please choose another name.', 'invalid table name', ToastDuration.INFINITE); - return; + + rename(table: Table) { + const meta = new EntityMeta(this.namespace().id, table.id, table.newName, []); + const type = table.tableType === EntityType.VIEW ? ' View ' : ' Table '; + this._crud.renameTable(meta).subscribe({ + next: (r: RelationalResult) => { + if (r.exception) { + this._toast.exception(r); + } else { + this._toast.success('Renamed' + type + table.name + ' to ' + table.newName); + //this._catalog.updateIfNecessary(); + this._leftSidebar.setSchema(this._router, '/views/schema-editing/', true, 2, false); + } + }, error: err => { + this._toast.error('Could not rename the' + type + table.name); + console.log(err); + } + }); } - let valid = true; - //clear precision/scale for types where it is not applicable - //delete columns with no column name - let hasPk = false; - this.newColumns.forEach((v, k) => { - if (!this._types.supportsPrecision(v.dataType) && v.precision !== null) { - v.precision = null; - } - if (!this._types.supportsScale(v.dataType) && v.scale !== null) { - v.scale = null; - } - //clear cardinality and dimension if it is not an array - if (v.collectionsType !== 'ARRAY') { - v.cardinality = null; - v.dimension = null; - } - if (v.name === '') { - this.newColumns.delete(k); - } - if (!this._crud.nameIsValid(v.name)) { - valid = false; - return; - } - - if (v.primary) { - hasPk = true; - } - }); - if (!hasPk) { - this._toast.warn('Please specify a primary key. The new table was not created.', 'missing primary key', ToastDuration.INFINITE); - return; + + /** + * Check if the new table name is valid + */ + canRename(table: Table) { + //table.name !== table.newName not necessary, since the filter will catch it as well + return this.tables().filter((t) => t.name === table.newName).length === 0 && + this._crud.nameIsValid(table.newName); } - if (!valid) { - this._toast.warn('Please make sure all column names are valid. The new table was not created.', 'invalid column name', ToastDuration.INFINITE); - return; + + initSocket() { + const sub = this._crud.onSocketEvent().subscribe( + (msg: Status) => { + if (msg.context === 'tableExport') { + this.exportProgress = msg.status; + } + }, err => { + setTimeout(() => { + this.initSocket(); + }, +this._settings.getSetting('reconnection.timeout')); + }); + this.subscriptions.add(sub); } - const request = new EditTableRequest(this.namespace().id, null, this.newTableName, 'create', Array.from(this.newColumns.values()), this.selectedStore?.id); - this.creatingTable = true; - this._crud.createTable(request).subscribe({ - next: (result: RelationalResult) => { - if (result.error) { - this._toast.exception(result, 'Could not generate table:'); - } else { - this._toast.success('Generated table ' + request.entityName, result.query); - this.newColumns.clear(); - this.counter = 0; - this.newColumns.set(this.counter++, new UiColumnDefinition(-1, '', true, false, INITIAL_TYPE, '', null, null)); - this.newTableName = ''; - this.selectedStore = null; - this._leftSidebar.setSchema(this._router, '/views/schema-editing/', true, 2, false); - } - //this._catalog.updateIfNecessary(); - }, error: err => { - this._toast.error('Could not generate table'); - console.log(err); - } - }).add(() => this.creatingTable = false); - } - - rename(table: Table) { - const meta = new EntityMeta(this.namespace().id, table.id, table.newName, []); - const type = table.tableType === EntityType.VIEW ? ' View ' : ' Table '; - this._crud.renameTable(meta).subscribe({ - next: (r: RelationalResult) => { - if (r.exception) { - this._toast.exception(r); + + createTableValidation(name: string) { + const regex = this._crud.getValidationRegex(); + if (name === '') { + return ''; + } else if (regex.test(name) && name.length <= 100 && this.tables().filter((t) => { + return this.namespace().caseSensitive ? t.name === name : t.name.toLowerCase() === name.toLowerCase(); + }).length === 0) { + return 'is-valid'; } else { - this._toast.success('Renamed' + type + table.name + ' to ' + table.newName); - //this._catalog.updateIfNecessary(); - this._leftSidebar.setSchema(this._router, '/views/schema-editing/', true, 2, false); + return 'is-invalid'; } - }, error: err => { - this._toast.error('Could not rename the' + type + table.name); - console.log(err); - } - }); - } - - /** - * Check if the new table name is valid - */ - canRename(table: Table) { - //table.name !== table.newName not necessary, since the filter will catch it as well - return this.tables().filter((t) => t.name === table.newName).length === 0 && - this._crud.nameIsValid(table.newName); - } - - initSocket() { - const sub = this._crud.onSocketEvent().subscribe( - (msg: Status) => { - if (msg.context === 'tableExport') { - this.exportProgress = msg.status; - } - }, err => { - setTimeout(() => { - this.initSocket(); - }, +this._settings.getSetting('reconnection.timeout')); - }); - this.subscriptions.add(sub); - } - - createTableValidation(name: string) { - const regex = this._crud.getValidationRegex(); - if (name === '') { - return ''; - } else if (regex.test(name) && name.length <= 100 && this.tables().filter((t) => { - return this.namespace().caseSensitive ? t.name === name : t.name.toLowerCase() === name.toLowerCase(); - }).length === 0) { - return 'is-valid'; - } else { - return 'is-invalid'; } - } - - addNewColumn() { - this.newColumns.set(this.counter++, new UiColumnDefinition(-1, '', false, true, INITIAL_TYPE, '', null, null)); - } - - removeNewColumn(i: number) { - if (this.newColumns.size === 1) { - this.counter = 0; - this.newColumns.clear(); - this.newColumns.set(this.counter++, new UiColumnDefinition(-1, '', true, false, INITIAL_TYPE, '', null, null)); - } else { - //don't change the counter here! - this.newColumns.delete(i); + + addNewColumn() { + this.newColumns.set(this.counter++, new UiColumnDefinition(-1, '', false, true, INITIAL_TYPE, '', null, null)); } - } - - triggerDefaultNull(col: UiColumnDefinition) { - if (col.defaultValue === null) { - if (this._types.isNumeric(col.dataType)) { - col.defaultValue = 0; - } else if (this._types.isBoolean(col.dataType)) { - col.defaultValue = false; - } else { - col.defaultValue = ''; - } - } else { - col.defaultValue = null; + + removeNewColumn(i: number) { + if (this.newColumns.size === 1) { + this.counter = 0; + this.newColumns.clear(); + this.newColumns.set(this.counter++, new UiColumnDefinition(-1, '', true, false, INITIAL_TYPE, '', null, null)); + } else { + //don't change the counter here! + this.newColumns.delete(i); + } } - } - getTypeInfo() { - this._types.getTypes().subscribe( - t => { - this.types = t; - this.newColumns.get(0).dataType = INITIAL_TYPE; + triggerDefaultNull(col: UiColumnDefinition) { + if (col.defaultValue === null) { + if (this._types.isNumeric(col.dataType)) { + col.defaultValue = 0; + } else if (this._types.isBoolean(col.dataType)) { + col.defaultValue = false; + } else { + col.defaultValue = ''; + } + } else { + col.defaultValue = null; } - ); - } + } + + getTypeInfo() { + this._types.getTypes().subscribe( + t => { + this.types = t; + this.newColumns.get(0).dataType = INITIAL_TYPE; + } + ); + } } export class Table { - id: number; - name: string; - truncate = ''; - drop = ''; - export = false; - editing = false; - newName: string; - modifiable: boolean; - tableType: EntityType; - - constructor(id: number, name: string, newName: string, modifiable: boolean, entityType: EntityType) { - this.id = id; - this.name = name; - this.newName = newName; - this.modifiable = modifiable; - this.tableType = entityType; - } - - static fromDb(table: DbTable) { - return new Table(null, table.tableName, table.tableName, table.modifiable, table.tableType); - } - - static fromModel(table: TableModel) { - return new Table(table.id, table.name, table.name, table.modifiable, table.entityType); - } + id: number; + name: string; + truncate = ''; + drop = ''; + export = false; + editing = false; + newName: string; + modifiable: boolean; + tableType: EntityType; + + constructor(id: number, name: string, newName: string, modifiable: boolean, entityType: EntityType) { + this.id = id; + this.name = name; + this.newName = newName; + this.modifiable = modifiable; + this.tableType = entityType; + } + + static fromDb(table: DbTable) { + return new Table(null, table.tableName, table.tableName, table.modifiable, table.tableType); + } + + static fromModel(table: TableModel) { + return new Table(table.id, table.name, table.name, table.modifiable, table.entityType); + } } diff --git a/src/app/views/schema-editing/graph-edit-graph/graph-edit-graph.component.ts b/src/app/views/schema-editing/graph-edit-graph/graph-edit-graph.component.ts index 600963c6..fac239b6 100644 --- a/src/app/views/schema-editing/graph-edit-graph/graph-edit-graph.component.ts +++ b/src/app/views/schema-editing/graph-edit-graph/graph-edit-graph.component.ts @@ -6,115 +6,121 @@ import {ToasterService} from '../../../components/toast-exposer/toaster.service' import {DbmsTypesService} from '../../../services/dbms-types.service'; import {ModalDirective} from 'ngx-bootstrap/modal'; import {Subscription} from 'rxjs'; -import {AllocationEntityModel, AllocationPartitionModel, AllocationPlacementModel, NamespaceModel, TableModel} from '../../../models/catalog.model'; +import { + AllocationEntityModel, + AllocationPartitionModel, + AllocationPlacementModel, + NamespaceModel, + TableModel +} from '../../../models/catalog.model'; import {Method} from '../../../models/ui-request.model'; import {AdapterModel} from '../../adapters/adapter.model'; @Component({ - selector: 'app-graph-edit', - templateUrl: './graph-edit-graph.component.html', - styleUrls: ['./graph-edit-graph.component.scss'] + selector: 'app-graph-edit', + templateUrl: './graph-edit-graph.component.html', + styleUrls: ['./graph-edit-graph.component.scss'] }) export class GraphEditGraphComponent implements OnInit, OnDestroy { - public readonly _crud = inject(CrudService); - public readonly _types = inject(DbmsTypesService); - private readonly _toast = inject(ToasterService); + public readonly _crud = inject(CrudService); + public readonly _types = inject(DbmsTypesService); + private readonly _toast = inject(ToasterService); - constructor() { + constructor() { - } + } - @Input() - readonly entity: Signal; - @Input() - readonly namespace: Signal; - @Input() - readonly currentRoute: Signal; + @Input() + readonly entity: Signal; + @Input() + readonly namespace: Signal; + @Input() + readonly currentRoute: Signal; - @Input() - readonly placements: Signal; - @Input() - readonly partitions: Signal; - @Input() - readonly allocations: Signal; - @Input() - readonly stores: Signal; - @Input() - readonly addableStores: Signal; + @Input() + readonly placements: Signal; + @Input() + readonly partitions: Signal; + @Input() + readonly allocations: Signal; + @Input() + readonly stores: Signal; + @Input() + readonly addableStores: Signal; - types: PolyType[] = []; - editColumn = -1; - confirm = -1; + types: PolyType[] = []; + editColumn = -1; + confirm = -1; - //data placement handling - selectedStore: AdapterModel; - isAddingPlacement = false; + //data placement handling + selectedStore: AdapterModel; + isAddingPlacement = false; - subscriptions = new Subscription(); + subscriptions = new Subscription(); - @ViewChild('placementModal', {static: false}) public placementModal: ModalDirective; - @ViewChild('partitioningModal', {static: false}) public partitioningModal: ModalDirective; - @ViewChild('partitionFunctionModal', {static: false}) public partitionFunctionModal: ModalDirective; + @ViewChild('placementModal', {static: false}) public placementModal: ModalDirective; + @ViewChild('partitioningModal', {static: false}) public partitioningModal: ModalDirective; + @ViewChild('partitionFunctionModal', {static: false}) public partitionFunctionModal: ModalDirective; - protected readonly Method = Method; + protected readonly Method = Method; - ngOnInit() { - } + ngOnInit() { + } - ngOnDestroy() { - $(document).off('click'); - this.subscriptions.unsubscribe(); - } + ngOnDestroy() { + $(document).off('click'); + this.subscriptions.unsubscribe(); + } - //see https://medium.com/claritydesignsystem/1b66d45b3e3d - @HostListener('window:click', ['$event.target']) - onClick(targetElement: string) { - const self = this; - if ($(targetElement).parents('.editing').length === 0) { - self.editColumn = -1; + //see https://medium.com/claritydesignsystem/1b66d45b3e3d + @HostListener('window:click', ['$event.target']) + onClick(targetElement: string) { + const self = this; + if ($(targetElement).parents('.editing').length === 0) { + self.editColumn = -1; + } } - } - modifyPlacement(method: Method, store = null) { - if (store != null) { - this.selectedStore = store; + modifyPlacement(method: Method, store = null) { + if (store != null) { + this.selectedStore = store; + } + if (!this.selectedStore) { + return; + } + this.isAddingPlacement = true; + this._crud.addDropGraphPlacement(this.entity().name, this.selectedStore.id, method).subscribe(res => { + const result = res; + if (result.error) { + this._toast.exception(result); + return; + } + if (method === Method.ADD) { + this._toast.success('Added placement on store ' + this.selectedStore.name, result.query, 'Added placement'); + } else if (method === Method.DROP) { + this._toast.success('Dropped placement on store ' + this.selectedStore.name, result.query, 'Dropped placement'); + } + //this._catalog.updateIfNecessary(); + + }).add(() => { + this.isAddingPlacement = false; + }); } - if (!this.selectedStore) { - return; - } - this.isAddingPlacement = true; - this._crud.addDropGraphPlacement(this.entity().name, this.selectedStore.id, method).subscribe(res => { - const result = res; - if (result.error) { - this._toast.exception(result); - return; - } - if (method === Method.ADD) { - this._toast.success('Added placement on store ' + this.selectedStore.name, result.query, 'Added placement'); - } else if (method === Method.DROP) { - this._toast.success('Dropped placement on store ' + this.selectedStore.name, result.query, 'Dropped placement'); - } - //this._catalog.updateIfNecessary(); - - }).add(() => { - this.isAddingPlacement = false; - }); - } - - - validate(defaultValue) { - if (defaultValue === null) { - return ''; - } else if (isNaN(defaultValue) || defaultValue === '') { - return 'is-invalid'; - } else { - return 'is-valid'; + + + validate(defaultValue) { + if (defaultValue === null) { + return ''; + } else if (isNaN(defaultValue) || defaultValue === '') { + return 'is-invalid'; + } else { + return 'is-valid'; + } } - } } diff --git a/src/app/views/schema-editing/schema-editing.component.ts b/src/app/views/schema-editing/schema-editing.component.ts index 5aae6079..095b6659 100644 --- a/src/app/views/schema-editing/schema-editing.component.ts +++ b/src/app/views/schema-editing/schema-editing.component.ts @@ -1,4 +1,15 @@ -import {Component, computed, effect, inject, OnDestroy, OnInit, signal, Signal, untracked, WritableSignal} from '@angular/core'; +import { + Component, + computed, + effect, + inject, + OnDestroy, + OnInit, + signal, + Signal, + untracked, + WritableSignal +} from '@angular/core'; import {ActivatedRoute, Router} from '@angular/router'; import {LeftSidebarService} from '../../components/left-sidebar/left-sidebar.service'; import {CrudService} from '../../services/crud.service'; @@ -14,218 +25,218 @@ import {NamespaceModel} from '../../models/catalog.model'; import {AdapterModel} from '../adapters/adapter.model'; @Component({ - selector: 'app-schema-editing', - templateUrl: './schema-editing.component.html', - styleUrls: ['./schema-editing.component.scss'] + selector: 'app-schema-editing', + templateUrl: './schema-editing.component.html', + styleUrls: ['./schema-editing.component.scss'] }) export class SchemaEditingComponent implements OnInit, OnDestroy { - private readonly _route = inject(ActivatedRoute); - private readonly _router = inject(Router); - private readonly _leftSidebar = inject(LeftSidebarService); - private readonly _breadcrumb = inject(BreadcrumbService); - private readonly _crud = inject(CrudService); - private readonly _toast = inject(ToasterService); - private readonly _catalog = inject(CatalogService); - - constructor() { - - this._route.params.subscribe(route => { - this.currentRoute.set(route['id']); - }); - this.namespace = computed(() => { - - const catalog = this._catalog.listener(); - const route = this.currentRoute(); - if (!route) { - return null; - } - const namespaceName = route.split('\.')[0]; - return this._catalog.getNamespaceFromName(namespaceName); - }); - - this.namespaces = computed(() => { - const catalog = this._catalog.listener(); - return this._catalog.getNamespaces(); - }); - - this.stores = computed(() => { - const catalog = this._catalog.listener(); - return catalog.getStores(); - }); - - effect(() => { - const catalog = this._catalog.listener(); - - untracked(() => { - this._leftSidebar.setSchema(this._router, '/views/schema-editing/', true, 2, false, true); - }); - }); - } - - - readonly currentRoute: WritableSignal = signal(this._route.snapshot.paramMap.get('id'));//either the name of a table (schemaName.tableName) or of a schema (schemaName) - readonly namespace: Signal; - - createForm: UntypedFormGroup; - dropForm: UntypedFormGroup; - namespaces: Signal; - createSubmitted = false; - dropSubmitted = false; - createNamespaceFeedback = 'Name is invalid'; - private subscriptions = new Subscription(); - readonly stores: Signal; - graphStore: string; - - public readonly NamespaceType = DataModel; - - ngOnInit() { - //this.getSchema(); - this.initForms(); - this._route.params.subscribe((ev) => { - this.setBreadCrumb(); - }); - const sub = this._crud.onReconnection().subscribe( - b => { - this._leftSidebar.setSchema(this._router, '/views/schema-editing/', true, 2, false, true); - } - ); - - this.subscriptions.add(sub); - } - - - setBreadCrumb() { - const url = this._router.url.replace('/views/schema-editing/', ''); - if (url.length <= 0) { - this._breadcrumb.setBreadcrumbsSchema([new BreadcrumbItem('Schema')], null); - } else if (url.includes('statistics-column')) { - const colName = url.replace('/statistics-column', '').split('.')[url.replace('/statistics-column', '').split('.').length - 1]; - this._breadcrumb.setBreadcrumbs([new BreadcrumbItem('Schema', '/views/schema-editing/'), new BreadcrumbItem(url.split('.')[0], this._router.url.split('.')[0]), new BreadcrumbItem(colName, this._router.url.replace('/statistics-column', '')), new BreadcrumbItem('statistics')]); - } else if (!url.includes('.')) { - this._breadcrumb.setBreadcrumbsSchema([new BreadcrumbItem('Schema', '/views/schema-editing/'), new BreadcrumbItem(url)], null); - } else { - this._breadcrumb.setBreadcrumbs([new BreadcrumbItem('Schema', '/views/schema-editing/'), new BreadcrumbItem(url.split('.')[0], this._router.url.split('.')[0]), new BreadcrumbItem(url.split('.')[url.split('.').length - 1])]); + private readonly _route = inject(ActivatedRoute); + private readonly _router = inject(Router); + private readonly _leftSidebar = inject(LeftSidebarService); + private readonly _breadcrumb = inject(BreadcrumbService); + private readonly _crud = inject(CrudService); + private readonly _toast = inject(ToasterService); + private readonly _catalog = inject(CatalogService); + + constructor() { + + this._route.params.subscribe(route => { + this.currentRoute.set(route['id']); + }); + this.namespace = computed(() => { + + const catalog = this._catalog.listener(); + const route = this.currentRoute(); + if (!route) { + return null; + } + const namespaceName = route.split('\.')[0]; + return this._catalog.getNamespaceFromName(namespaceName); + }); + + this.namespaces = computed(() => { + const catalog = this._catalog.listener(); + return this._catalog.getNamespaces(); + }); + + this.stores = computed(() => { + const catalog = this._catalog.listener(); + return catalog.getStores(); + }); + + effect(() => { + const catalog = this._catalog.listener(); + + untracked(() => { + this._leftSidebar.setSchema(this._router, '/views/schema-editing/', true, 2, false, true); + }); + }); } - } - - ngOnDestroy() { - this._leftSidebar.close(); - this.subscriptions.unsubscribe(); - this._breadcrumb.hide(); - } - - public getSchema() { - this._leftSidebar.setSchema(this._router, '/views/schema-editing/', true, 2, false, true); - } - - - initForms() { - this.createForm = new UntypedFormGroup({ - name: new UntypedFormControl('', this._crud.getNameValidator(true)), - type: new UntypedFormControl('relational', Validators.required), - stores: new UntypedFormControl('hsqldb'), - }); - this.dropForm = new UntypedFormGroup({ - name: new UntypedFormControl('', Validators.required), - cascade: new UntypedFormControl() - }); - } - - resetForm(formName: string) { - switch (formName) { - case 'createForm': - this.createForm.controls['name'].setValue(''); - this.createForm.markAsPristine(); - break; - case 'dropForm': - this.dropForm.controls['name'].setValue(''); - this.dropForm.controls['cascade'].setValue(false); - this.dropForm.markAsPristine(); - break; + + + readonly currentRoute: WritableSignal = signal(this._route.snapshot.paramMap.get('id'));//either the name of a table (schemaName.tableName) or of a schema (schemaName) + readonly namespace: Signal; + + createForm: UntypedFormGroup; + dropForm: UntypedFormGroup; + namespaces: Signal; + createSubmitted = false; + dropSubmitted = false; + createNamespaceFeedback = 'Name is invalid'; + private subscriptions = new Subscription(); + readonly stores: Signal; + graphStore: string; + + public readonly NamespaceType = DataModel; + + ngOnInit() { + //this.getSchema(); + this.initForms(); + this._route.params.subscribe((ev) => { + this.setBreadCrumb(); + }); + const sub = this._crud.onReconnection().subscribe( + b => { + this._leftSidebar.setSchema(this._router, '/views/schema-editing/', true, 2, false, true); + } + ); + + this.subscriptions.add(sub); } - } - - createNamespace() { - if (this.createForm.valid && this.createNamespaceValidation(this.createForm.controls['name'].value) === 'is-valid') { - const val = this.createForm.value; - if (val.name.trim() === '') { - return; - } - - this.createSubmitted = true; - this._crud.createOrDropNamespace(new Namespace(val.name, DataModel[val.type.toUpperCase()], val.stores).setCreate(true)).subscribe({ - next: (res: RelationalResult) => { - if (res.error) { - this._toast.exception(res); - } else { - this._toast.success('Created namespace ' + val.name); - //this.getSchema(); - } - this.resetForm('createForm'); - }, error: err => { - this._toast.error('An unknown error occurred on the server'); + + + setBreadCrumb() { + const url = this._router.url.replace('/views/schema-editing/', ''); + if (url.length <= 0) { + this._breadcrumb.setBreadcrumbsSchema([new BreadcrumbItem('Schema')], null); + } else if (url.includes('statistics-column')) { + const colName = url.replace('/statistics-column', '').split('.')[url.replace('/statistics-column', '').split('.').length - 1]; + this._breadcrumb.setBreadcrumbs([new BreadcrumbItem('Schema', '/views/schema-editing/'), new BreadcrumbItem(url.split('.')[0], this._router.url.split('.')[0]), new BreadcrumbItem(colName, this._router.url.replace('/statistics-column', '')), new BreadcrumbItem('statistics')]); + } else if (!url.includes('.')) { + this._breadcrumb.setBreadcrumbsSchema([new BreadcrumbItem('Schema', '/views/schema-editing/'), new BreadcrumbItem(url)], null); + } else { + this._breadcrumb.setBreadcrumbs([new BreadcrumbItem('Schema', '/views/schema-editing/'), new BreadcrumbItem(url.split('.')[0], this._router.url.split('.')[0]), new BreadcrumbItem(url.split('.')[url.split('.').length - 1])]); } - }).add(() => this.createSubmitted = false); - } else { - this._toast.warn(this.createNamespaceFeedback, 'cannot create'); } - } - - dropNamespace() { - if (this.dropForm.valid && this.getValidationClass(this.dropForm.controls['name'].value) === 'is-valid') { - const val = this.dropForm.value; - this.dropSubmitted = true; - this._crud.createOrDropNamespace(new Namespace(val.name, val.type, this.graphStore).setDrop(true).setCascade(val.cascade)).subscribe({ - next: (res: RelationalResult) => { - if (res.error) { - this._toast.exception(res); - } else { - this._toast.success('Dropped namespace ' + val.name); - //this.getSchema(); - } - this.resetForm('dropForm'); - }, error: err => { - this._toast.error('An unknown error occurred on the server'); + + ngOnDestroy() { + this._leftSidebar.close(); + this.subscriptions.unsubscribe(); + this._breadcrumb.hide(); + } + + public getSchema() { + this._leftSidebar.setSchema(this._router, '/views/schema-editing/', true, 2, false, true); + } + + + initForms() { + this.createForm = new UntypedFormGroup({ + name: new UntypedFormControl('', this._crud.getNameValidator(true)), + type: new UntypedFormControl('relational', Validators.required), + stores: new UntypedFormControl('hsqldb'), + }); + this.dropForm = new UntypedFormGroup({ + name: new UntypedFormControl('', Validators.required), + cascade: new UntypedFormControl() + }); + } + + resetForm(formName: string) { + switch (formName) { + case 'createForm': + this.createForm.controls['name'].setValue(''); + this.createForm.markAsPristine(); + break; + case 'dropForm': + this.dropForm.controls['name'].setValue(''); + this.dropForm.controls['cascade'].setValue(false); + this.dropForm.markAsPristine(); + break; } - }).add(() => this.dropSubmitted = false); - } else { - this._toast.warn('This namespace does not exist', 'cannot drop'); } - } - - getValidationClass(val) { - if (val === '') { - return ''; - } else if (this.namespaces().filter((o) => o.name === val).length > 0) { - return 'is-valid'; - } else { - return 'is-invalid'; + + createNamespace() { + if (this.createForm.valid && this.createNamespaceValidation(this.createForm.controls['name'].value) === 'is-valid') { + const val = this.createForm.value; + if (val.name.trim() === '') { + return; + } + + this.createSubmitted = true; + this._crud.createOrDropNamespace(new Namespace(val.name, DataModel[val.type.toUpperCase()], val.stores).setCreate(true)).subscribe({ + next: (res: RelationalResult) => { + if (res.error) { + this._toast.exception(res); + } else { + this._toast.success('Created namespace ' + val.name); + //this.getSchema(); + } + this.resetForm('createForm'); + }, error: err => { + this._toast.error('An unknown error occurred on the server'); + } + }).add(() => this.createSubmitted = false); + } else { + this._toast.warn(this.createNamespaceFeedback, 'cannot create'); + } } - } - createNamespaceValidation(name) { - if (name === '') { - return ''; + dropNamespace() { + if (this.dropForm.valid && this.getValidationClass(this.dropForm.controls['name'].value) === 'is-valid') { + const val = this.dropForm.value; + this.dropSubmitted = true; + this._crud.createOrDropNamespace(new Namespace(val.name, val.type, this.graphStore).setDrop(true).setCascade(val.cascade)).subscribe({ + next: (res: RelationalResult) => { + if (res.error) { + this._toast.exception(res); + } else { + this._toast.success('Dropped namespace ' + val.name); + //this.getSchema(); + } + this.resetForm('dropForm'); + }, error: err => { + this._toast.error('An unknown error occurred on the server'); + } + }).add(() => this.dropSubmitted = false); + } else { + this._toast.warn('This namespace does not exist', 'cannot drop'); + } } - if (this.namespaces) { - if (this.namespaces().filter((o) => o.name === name).length > 0) { - this.createNamespaceFeedback = 'Namespace name is already taken'; - return 'is-invalid'; - } else { - this.createNamespaceFeedback = 'Namespace name is invalid'; - } + + getValidationClass(val) { + if (val === '') { + return ''; + } else if (this.namespaces().filter((o) => o.name === val).length > 0) { + return 'is-valid'; + } else { + return 'is-invalid'; + } } - const regex = this._crud.getNamespaceValidationRegex(); - if (regex.test(name) && name.length <= 100) { - return 'is-valid'; - } else { - return 'is-invalid'; + + createNamespaceValidation(name) { + if (name === '') { + return ''; + } + if (this.namespaces) { + if (this.namespaces().filter((o) => o.name === name).length > 0) { + this.createNamespaceFeedback = 'Namespace name is already taken'; + return 'is-invalid'; + } else { + this.createNamespaceFeedback = 'Namespace name is invalid'; + } + } + const regex = this._crud.getNamespaceValidationRegex(); + if (regex.test(name) && name.length <= 100) { + return 'is-valid'; + } else { + return 'is-invalid'; + } } - } - isStatistic() { - return this._router.url.includes('statistics'); - } + isStatistic() { + return this._router.url.includes('statistics'); + } } diff --git a/src/app/views/schema-editing/statistics-column/statistics-column.component.ts b/src/app/views/schema-editing/statistics-column/statistics-column.component.ts index 88e507cd..ed80e0d4 100644 --- a/src/app/views/schema-editing/statistics-column/statistics-column.component.ts +++ b/src/app/views/schema-editing/statistics-column/statistics-column.component.ts @@ -6,46 +6,46 @@ import {Subscription} from 'rxjs'; import {ToasterService} from '../../../components/toast-exposer/toaster.service'; @Component({ - selector: 'app-statistics-column', - templateUrl: './statistics-column.component.html', - styleUrls: ['./statistics-column.component.scss'] + selector: 'app-statistics-column', + templateUrl: './statistics-column.component.html', + styleUrls: ['./statistics-column.component.scss'] }) export class StatisticsColumnComponent implements OnInit, OnDestroy { - private readonly _crud = inject(CrudService); - private readonly _toast = inject(ToasterService); - - subscriptions = new Subscription(); - entityId: number; - statisticSet: StatisticTableSet; - alphabeticStatisticSet: StatisticColumnSet; - numericalStatisticSet: StatisticColumnSet; - temporalStatisticSet: StatisticColumnSet; - - constructor() { - } - - ngOnInit(): void { - this.getTableStatistics(this.entityId); - } - - ngOnDestroy() { - this.subscriptions.unsubscribe(); - } - - getTableStatistics(entityId: number) { - this._crud.getTableStatistics(new StatisticRequest(entityId)).subscribe({ - next: (res: StatisticTableSet) => { - this.statisticSet = res; - this.alphabeticStatisticSet = this.statisticSet.alphabeticColumn; - this.numericalStatisticSet = this.statisticSet.numericalColumn; - this.temporalStatisticSet = this.statisticSet.temporalColumn; - }, error: err => { - this._toast.warn('There are no statistics for this entity.'); - - } - }); - } + private readonly _crud = inject(CrudService); + private readonly _toast = inject(ToasterService); + + subscriptions = new Subscription(); + entityId: number; + statisticSet: StatisticTableSet; + alphabeticStatisticSet: StatisticColumnSet; + numericalStatisticSet: StatisticColumnSet; + temporalStatisticSet: StatisticColumnSet; + + constructor() { + } + + ngOnInit(): void { + this.getTableStatistics(this.entityId); + } + + ngOnDestroy() { + this.subscriptions.unsubscribe(); + } + + getTableStatistics(entityId: number) { + this._crud.getTableStatistics(new StatisticRequest(entityId)).subscribe({ + next: (res: StatisticTableSet) => { + this.statisticSet = res; + this.alphabeticStatisticSet = this.statisticSet.alphabeticColumn; + this.numericalStatisticSet = this.statisticSet.numericalColumn; + this.temporalStatisticSet = this.statisticSet.temporalColumn; + }, error: err => { + this._toast.warn('There are no statistics for this entity.'); + + } + }); + } } diff --git a/src/app/views/table-view/table-view.component.ts b/src/app/views/table-view/table-view.component.ts index f3ce5c6e..60c4e6ab 100644 --- a/src/app/views/table-view/table-view.component.ts +++ b/src/app/views/table-view/table-view.component.ts @@ -2,63 +2,63 @@ import {Component, computed, effect, OnDestroy, OnInit, Signal, untracked} from import {DataTemplateComponent} from '../../components/data-view/data-template/data-template.component'; @Component({ - selector: 'app-table-view', - templateUrl: './table-view.component.html', - styleUrls: ['./table-view.component.scss'] + selector: 'app-table-view', + templateUrl: './table-view.component.html', + styleUrls: ['./table-view.component.scss'] }) export class TableViewComponent extends DataTemplateComponent implements OnInit, OnDestroy { - readonly fullName: Signal; - reload = () => { // we can preserve the "this" context - if (!this.entity()) { - return; - } - this.getEntityData(); - } + readonly fullName: Signal; + reload = () => { // we can preserve the "this" context + if (!this.entity()) { + return; + } + this.getEntityData(); + }; - constructor() { - super(); - this.fullName = computed(() => this.routeParams()['id']); + constructor() { + super(); + this.fullName = computed(() => this.routeParams()['id']); - effect(() => { - if (!this.entity()) { - return; - } + effect(() => { + if (!this.entity()) { + return; + } - untracked(() => { - this.getEntityData(); - }); - }); + untracked(() => { + this.getEntityData(); + }); + }); - effect(() => { - const catalog = this._catalog.listener(); - untracked(() => { - this._sidebar.setSchema(this._router, '/views/data-table/', true, 2, false); - }); - }); - } + effect(() => { + const catalog = this._catalog.listener(); + untracked(() => { + this._sidebar.setSchema(this._router, '/views/data-table/', true, 2, false); + }); + }); + } - ngOnInit() { - super.ngOnInit(); + ngOnInit() { + super.ngOnInit(); - //this._sidebar.setSchema(this._router, '/views/data-table/', true, 2, false); - const sub = this.webSocket.reconnecting.subscribe( - b => { - if (b) { - //this._sidebar.setSchema(this._router, '/views/data-table/', true, 2, false); - this.getEntityData(); - } - } - ); - this.subscriptions.add(sub); + //this._sidebar.setSchema(this._router, '/views/data-table/', true, 2, false); + const sub = this.webSocket.reconnecting.subscribe( + b => { + if (b) { + //this._sidebar.setSchema(this._router, '/views/data-table/', true, 2, false); + this.getEntityData(); + } + } + ); + this.subscriptions.add(sub); - } + } - ngOnDestroy() { - this._sidebar.close(); - this.subscriptions.unsubscribe(); - this.webSocket.close(); - } + ngOnDestroy() { + this._sidebar.close(); + this.subscriptions.unsubscribe(); + this.webSocket.close(); + } } diff --git a/src/app/views/uml/uml.component.spec.ts b/src/app/views/uml/uml.component.spec.ts index bf184848..5f76ce13 100644 --- a/src/app/views/uml/uml.component.spec.ts +++ b/src/app/views/uml/uml.component.spec.ts @@ -3,23 +3,23 @@ import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {UmlComponent} from './uml.component'; describe('UmlComponent', () => { - let component: UmlComponent; - let fixture: ComponentFixture; + let component: UmlComponent; + let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [UmlComponent] - }) - .compileComponents(); - })); + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [UmlComponent] + }) + .compileComponents(); + })); - beforeEach(() => { - fixture = TestBed.createComponent(UmlComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(UmlComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); }); diff --git a/src/app/views/uml/uml.component.ts b/src/app/views/uml/uml.component.ts index e3179cad..34fefc49 100644 --- a/src/app/views/uml/uml.component.ts +++ b/src/app/views/uml/uml.component.ts @@ -13,308 +13,308 @@ import {Subscription} from 'rxjs'; import {ModalDirective} from 'ngx-bootstrap/modal'; @Component({ - selector: 'app-uml', - templateUrl: './uml.component.html', - styleUrls: ['./uml.component.scss'] + selector: 'app-uml', + templateUrl: './uml.component.html', + styleUrls: ['./uml.component.scss'] }) export class UmlComponent implements OnInit, AfterViewInit, OnDestroy { - private readonly _route = inject(ActivatedRoute); - private readonly _router = inject(Router); - public readonly _crud = inject(CrudService); - private readonly _leftSidebar = inject(LeftSidebarService); - private readonly _formBuilder = inject(UntypedFormBuilder); - private readonly _toast = inject(ToasterService); - private readonly _dbmsTypes = inject(DbmsTypesService); + private readonly _route = inject(ActivatedRoute); + private readonly _router = inject(Router); + public readonly _crud = inject(CrudService); + private readonly _leftSidebar = inject(LeftSidebarService); + private readonly _formBuilder = inject(UntypedFormBuilder); + private readonly _toast = inject(ToasterService); + private readonly _dbmsTypes = inject(DbmsTypesService); - uml: Uml; - temporalLine: SvgLine; - schema; - connections = []; - zIndex = 2; - errorMsg: string; - types: {}; + uml: Uml; + temporalLine: SvgLine; + schema; + connections = []; + zIndex = 2; + errorMsg: string; + types: {}; - @ViewChild('myModal', {static: false}) myModal: ModalDirective; - sourceTable;//schema.table - sourceCol; - targetTable;//schema.table - targetCol; - fkActions; - fkForm = this._formBuilder.group({update: 'RESTRICT', delete: 'RESTRICT'}); - constraintName = ''; - proposedConstraintName = 'fk1'; - private subscriptions = new Subscription(); + @ViewChild('myModal', {static: false}) myModal: ModalDirective; + sourceTable;//schema.table + sourceCol; + targetTable;//schema.table + targetCol; + fkActions; + fkForm = this._formBuilder.group({update: 'RESTRICT', delete: 'RESTRICT'}); + constraintName = ''; + proposedConstraintName = 'fk1'; + private subscriptions = new Subscription(); - //offsets - offsetLineX1 = 15; - offsetLineX2 = 220; - offsetLineY = 150; - offsetConnLeft1 = 21; - offsetConnLeft2 = -5; - offsetConnTop = 20; - schemaType = ''; + //offsets + offsetLineX1 = 15; + offsetLineX2 = 220; + offsetLineY = 150; + offsetConnLeft1 = 21; + offsetConnLeft2 = -5; + offsetConnTop = 20; + schemaType = ''; - constructor() { - this._dbmsTypes.getFkActions().subscribe( - types => { - this.fkActions = types; - } - ); - } - - ngOnInit() { - this.schema = this._route.snapshot.paramMap.get('id'); - this._crud.getTypeSchemas().subscribe(res => { - this.types = res; - }); - this._route.params.subscribe(params => { - this.schema = params['id']; - console.log(this.types); - if (this.types && this.types.hasOwnProperty(this.schema)) { - this.schemaType = this.types[this.schema]; - } - this.getUml(); - }); - const sub = this._crud.onReconnection().subscribe( - b => { - if (b) { - this._leftSidebar.setSchema(this._router, '/views/uml/', false, 1, true, false, [DataModel.RELATIONAL]); - } - } - ); - this.subscriptions.add(sub); - this._leftSidebar.setSchema(this._router, '/views/uml/', true, 1, true, false, [DataModel.RELATIONAL]); - } + constructor() { + this._dbmsTypes.getFkActions().subscribe( + types => { + this.fkActions = types; + } + ); + } - ngAfterViewInit() { - this.getUml(); - this.connectTables(); - this.getGeneratedNames(); - } + ngOnInit() { + this.schema = this._route.snapshot.paramMap.get('id'); + this._crud.getTypeSchemas().subscribe(res => { + this.types = res; + }); + this._route.params.subscribe(params => { + this.schema = params['id']; + console.log(this.types); + if (this.types && this.types.hasOwnProperty(this.schema)) { + this.schemaType = this.types[this.schema]; + } + this.getUml(); + }); + const sub = this._crud.onReconnection().subscribe( + b => { + if (b) { + this._leftSidebar.setSchema(this._router, '/views/uml/', false, 1, true, false, [DataModel.RELATIONAL]); + } + } + ); + this.subscriptions.add(sub); + this._leftSidebar.setSchema(this._router, '/views/uml/', true, 1, true, false, [DataModel.RELATIONAL]); + } - ngOnDestroy() { - $(document).off();//remove event listener from connectTables() when leaving this view - this._leftSidebar.close(); - this.subscriptions.unsubscribe(); - } + ngAfterViewInit() { + this.getUml(); + this.connectTables(); + this.getGeneratedNames(); + } - getUml() { - if (!this.schema) { - this.uml = null; - this._leftSidebar.reset(); - return; + ngOnDestroy() { + $(document).off();//remove event listener from connectTables() when leaving this view + this._leftSidebar.close(); + this.subscriptions.unsubscribe(); } - this._crud.getUml(new EditTableRequest(this.schema)).subscribe({ - next: (uml: Uml) => { - this.errorMsg = null; - this.uml = new Uml(uml.tables, uml.foreignKeys); - this.mapConnections(); - }, error: err => { - this.errorMsg = 'Could not connect with the server.'; - } - }); - } - getGeneratedNames() { - this._crud.getGeneratedNames().subscribe({ - next: res => { - const names = res; - if (!names.error) { - this.proposedConstraintName = names.data[0][1]; - } else { - console.log(names.error); + getUml() { + if (!this.schema) { + this.uml = null; + this._leftSidebar.reset(); + return; } - }, error: err => { - console.log(err); - } - }); - } - - getColumnClass(table: DbTable, col: UiColumnDefinition) { - if (table.primaryKeyFields.indexOf(col.name) > -1) { - return 'bg-primary pk'; - } else if (table.uniqueColumns.indexOf(col.name) > -1) { - return 'bg-warning unique'; - } else { - return ''; + this._crud.getUml(new EditTableRequest(this.schema)).subscribe({ + next: (uml: Uml) => { + this.errorMsg = null; + this.uml = new Uml(uml.tables, uml.foreignKeys); + this.mapConnections(); + }, error: err => { + this.errorMsg = 'Could not connect with the server.'; + } + }); } - } - getUpdateDeleteActions() { - if (this.uml && this.targetTable) { - const tableName = this.targetTable.split('.')[1]; - const table = this.uml.tables[tableName]; - if (table?.tableType === 'SOURCE') { - return ['NONE']; - } + getGeneratedNames() { + this._crud.getGeneratedNames().subscribe({ + next: res => { + const names = res; + if (!names.error) { + this.proposedConstraintName = names.data[0][1]; + } else { + console.log(names.error); + } + }, error: err => { + console.log(err); + } + }); } - return this.fkActions; - } - mapConnections() { - this.connections = []; - this.uml.foreignKeys.forEach((v, k) => { - this.connections.push({ - source: v.sourceSchema + '_' + v.sourceTable + '_' + v.sourceColumn, - target: v.targetSchema + '_' + v.targetTable + '_' + v.targetColumn, - }); - }); - } - - updateZIndex(e) { - this.zIndex++; - const z = this.zIndex; - $(e.source.element.nativeElement).css('z-index', z); - } - - onDragging(e) { - $(e.source.element.nativeElement).css('z-index', 9000); - } - - connectTables() { - const self = this; - let isDragging = false; - let offsetX = this.offsetLineX1; - $(document).on('mousedown', '.uml .cols', function (e) { - if ($('body').hasClass('sidebar-lg-show') && document.documentElement.clientWidth > 992) { - offsetX = self.offsetLineX2; - } else { - offsetX = self.offsetLineX1; - } - isDragging = true; - self.sourceTable = $(e.target).parents('.uml').attr('tableName'); - self.sourceCol = $(e.target).attr('colName'); - self.temporalLine = { - x1: e.pageX - offsetX, - y1: e.pageY - self.offsetLineY, - x2: e.pageX - offsetX, - y2: e.pageY - self.offsetLineY - }; - e.preventDefault(); - }).on('mousemove', function (e) { - if (isDragging) { - self.temporalLine.x2 = e.pageX - offsetX; - self.temporalLine.y2 = e.pageY - self.offsetLineY; - e.preventDefault(); - } - }).on('mouseup', function (e) { - if (!isDragging) { - return; - } - if (($(e.target).hasClass('pk') || $(e.target).hasClass('unique')) && $(e.target).parents('.uml').attr('tableName') !== self.sourceTable) { - self.targetTable = $(e.target).parents('.uml').attr('tableName'); - self.targetCol = $(e.target).attr('colName'); - if (self.uml) { - const tableName = self.targetTable.split('.')[1]; - const table = self.uml.tables[tableName]; - if (table?.tableType === 'SOURCE') { - self.fkForm.controls['update'].setValue('NONE'); - self.fkForm.controls['delete'].setValue('NONE'); - } else { - self.fkForm.controls['update'].setValue('RESTRICT'); - self.fkForm.controls['delete'].setValue('RESTRICT'); - } + getColumnClass(table: DbTable, col: UiColumnDefinition) { + if (table.primaryKeyFields.indexOf(col.name) > -1) { + return 'bg-primary pk'; + } else if (table.uniqueColumns.indexOf(col.name) > -1) { + return 'bg-warning unique'; + } else { + return ''; } - self.myModal.show(); - } - self.temporalLine = null; - isDragging = false; - }); - } + } - /** get x position of div of source column - * param: source:string, target:string -> ids of the column divs */ - getX1(source: string, target: string) { - if (source === undefined || target === undefined) { - return; + getUpdateDeleteActions() { + if (this.uml && this.targetTable) { + const tableName = this.targetTable.split('.')[1]; + const table = this.uml.tables[tableName]; + if (table?.tableType === 'SOURCE') { + return ['NONE']; + } + } + return this.fkActions; } - const sourceEle = $('#' + source); - const targetEle = $('#' + target); - if (sourceEle === undefined || targetEle === undefined) { - return; + + mapConnections() { + this.connections = []; + this.uml.foreignKeys.forEach((v, k) => { + this.connections.push({ + source: v.sourceSchema + '_' + v.sourceTable + '_' + v.sourceColumn, + target: v.targetSchema + '_' + v.targetTable + '_' + v.targetColumn, + }); + }); } - // if($(sourceEle).offset() === undefined || $(targetEle).offset() === undefined ) return; - if ($(sourceEle).offset().left < $(targetEle).offset().left) { - return $(sourceEle).position().left + $(sourceEle).parents('.uml').position().left + $(sourceEle).width() + this.offsetConnLeft1 - 5;//-5 because no arrowhead - } else { - return $(sourceEle).position().left + $(sourceEle).parents('.uml').position().left + this.offsetConnLeft2 + 5;//+5 because no arrowhead + + updateZIndex(e) { + this.zIndex++; + const z = this.zIndex; + $(e.source.element.nativeElement).css('z-index', z); } - } - /** get x position of div of target column - * param: source:string, target:string -> ids of the column divs */ - getX2(source: string, target: string) { - if (source === undefined || target === undefined) { - return; + onDragging(e) { + $(e.source.element.nativeElement).css('z-index', 9000); } - const sourceEle = $('#' + source); - const targetEle = $('#' + target); - // if($(sourceEle).offset() === undefined || $(targetEle).offset() === undefined ) return; - if ($(sourceEle).offset().left < $(targetEle).offset().left + $(targetEle).width() / 2) { - return $(targetEle).position().left + $(targetEle).parents('.uml').position().left + this.offsetConnLeft2; - } else { - return $(targetEle).position().left + $(targetEle).parents('.uml').position().left + $(targetEle).width() + this.offsetConnLeft1; + + connectTables() { + const self = this; + let isDragging = false; + let offsetX = this.offsetLineX1; + $(document).on('mousedown', '.uml .cols', function (e) { + if ($('body').hasClass('sidebar-lg-show') && document.documentElement.clientWidth > 992) { + offsetX = self.offsetLineX2; + } else { + offsetX = self.offsetLineX1; + } + isDragging = true; + self.sourceTable = $(e.target).parents('.uml').attr('tableName'); + self.sourceCol = $(e.target).attr('colName'); + self.temporalLine = { + x1: e.pageX - offsetX, + y1: e.pageY - self.offsetLineY, + x2: e.pageX - offsetX, + y2: e.pageY - self.offsetLineY + }; + e.preventDefault(); + }).on('mousemove', function (e) { + if (isDragging) { + self.temporalLine.x2 = e.pageX - offsetX; + self.temporalLine.y2 = e.pageY - self.offsetLineY; + e.preventDefault(); + } + }).on('mouseup', function (e) { + if (!isDragging) { + return; + } + if (($(e.target).hasClass('pk') || $(e.target).hasClass('unique')) && $(e.target).parents('.uml').attr('tableName') !== self.sourceTable) { + self.targetTable = $(e.target).parents('.uml').attr('tableName'); + self.targetCol = $(e.target).attr('colName'); + if (self.uml) { + const tableName = self.targetTable.split('.')[1]; + const table = self.uml.tables[tableName]; + if (table?.tableType === 'SOURCE') { + self.fkForm.controls['update'].setValue('NONE'); + self.fkForm.controls['delete'].setValue('NONE'); + } else { + self.fkForm.controls['update'].setValue('RESTRICT'); + self.fkForm.controls['delete'].setValue('RESTRICT'); + } + } + self.myModal.show(); + } + self.temporalLine = null; + isDragging = false; + }); } - } - /** get y position of div of source/target column - * param: source:string, target:string -> ids of the column divs */ - getY(ele: string) { - if (ele === undefined) { - return; + /** get x position of div of source column + * param: source:string, target:string -> ids of the column divs */ + getX1(source: string, target: string) { + if (source === undefined || target === undefined) { + return; + } + const sourceEle = $('#' + source); + const targetEle = $('#' + target); + if (sourceEle === undefined || targetEle === undefined) { + return; + } + // if($(sourceEle).offset() === undefined || $(targetEle).offset() === undefined ) return; + if ($(sourceEle).offset().left < $(targetEle).offset().left) { + return $(sourceEle).position().left + $(sourceEle).parents('.uml').position().left + $(sourceEle).width() + this.offsetConnLeft1 - 5;//-5 because no arrowhead + } else { + return $(sourceEle).position().left + $(sourceEle).parents('.uml').position().left + this.offsetConnLeft2 + 5;//+5 because no arrowhead + } } - const element = $('#' + ele); - // if( $(element).position() === undefined ) return; - return $(element).position().top + $(element).parents('.uml').position().top + this.offsetConnTop; - } - closeModal() { - this.myModal.hide(); - this.sourceTable = null; - this.sourceCol = null; - this.targetTable = null; - this.targetCol = null; - } + /** get x position of div of target column + * param: source:string, target:string -> ids of the column divs */ + getX2(source: string, target: string) { + if (source === undefined || target === undefined) { + return; + } + const sourceEle = $('#' + source); + const targetEle = $('#' + target); + // if($(sourceEle).offset() === undefined || $(targetEle).offset() === undefined ) return; + if ($(sourceEle).offset().left < $(targetEle).offset().left + $(targetEle).width() / 2) { + return $(targetEle).position().left + $(targetEle).parents('.uml').position().left + this.offsetConnLeft2; + } else { + return $(targetEle).position().left + $(targetEle).parents('.uml').position().left + $(targetEle).width() + this.offsetConnLeft1; + } + } - createForeignKey() { - if (!this.constraintName || this.constraintName === '') { - this.constraintName = this.proposedConstraintName; + /** get y position of div of source/target column + * param: source:string, target:string -> ids of the column divs */ + getY(ele: string) { + if (ele === undefined) { + return; + } + const element = $('#' + ele); + // if( $(element).position() === undefined ) return; + return $(element).position().top + $(element).parents('.uml').position().top + this.offsetConnTop; } - if (!this._crud.nameIsValid(this.constraintName)) { - this._toast.warn(this._crud.invalidNameMessage('constraint'), 'invalid constraint name', ToastDuration.INFINITE); - return; + + closeModal() { + this.myModal.hide(); + this.sourceTable = null; + this.sourceCol = null; + this.targetTable = null; + this.targetCol = null; } - const fk: ForeignKey = new ForeignKey(-1, this.constraintName, this.schema, this.sourceTable, this.sourceCol, this.targetTable, this.targetCol) - .updateAction(this.fkForm.value.update).deleteAction(this.fkForm.value.delete); - this._crud.createForeignKey(fk).subscribe({ - next: res => { - this.closeModal(); - const result = res; - if (result.error) { - this._toast.exception(result, null, null, ToastDuration.INFINITE); - } else if (result.affectedTuples === 1) { - this._toast.success('new foreign key was created', result.query); - // this.getUml(); - // this.connectTables(); - const fkTable = fk.sourceTable.substr(fk.sourceTable.indexOf('.') + 1, fk.sourceTable.length); - const pkTable = fk.targetTable.substr(fk.targetTable.indexOf('.') + 1, fk.targetTable.length); - this.connections.push({ - source: fk.sourceSchema + '_' + fkTable + '_' + fk.sourceColumn, - target: fk.targetSchema + '_' + pkTable + '_' + fk.targetColumn - }); - this.constraintName = ''; - this.getGeneratedNames(); + createForeignKey() { + if (!this.constraintName || this.constraintName === '') { + this.constraintName = this.proposedConstraintName; } - }, error: err => { - this.closeModal(); - this._toast.error('An unknown error occurred on the server'); - } - }); - } + if (!this._crud.nameIsValid(this.constraintName)) { + this._toast.warn(this._crud.invalidNameMessage('constraint'), 'invalid constraint name', ToastDuration.INFINITE); + return; + } + const fk: ForeignKey = new ForeignKey(-1, this.constraintName, this.schema, this.sourceTable, this.sourceCol, this.targetTable, this.targetCol) + .updateAction(this.fkForm.value.update).deleteAction(this.fkForm.value.delete); + + this._crud.createForeignKey(fk).subscribe({ + next: res => { + this.closeModal(); + const result = res; + if (result.error) { + this._toast.exception(result, null, null, ToastDuration.INFINITE); + } else if (result.affectedTuples === 1) { + this._toast.success('new foreign key was created', result.query); + // this.getUml(); + // this.connectTables(); + const fkTable = fk.sourceTable.substr(fk.sourceTable.indexOf('.') + 1, fk.sourceTable.length); + const pkTable = fk.targetTable.substr(fk.targetTable.indexOf('.') + 1, fk.targetTable.length); + this.connections.push({ + source: fk.sourceSchema + '_' + fkTable + '_' + fk.sourceColumn, + target: fk.targetSchema + '_' + pkTable + '_' + fk.targetColumn + }); + this.constraintName = ''; + this.getGeneratedNames(); + } + }, error: err => { + this.closeModal(); + this._toast.error('An unknown error occurred on the server'); + } + }); + } } diff --git a/src/app/views/uml/uml.model.ts b/src/app/views/uml/uml.model.ts index 366b63c4..306cc8b0 100644 --- a/src/app/views/uml/uml.model.ts +++ b/src/app/views/uml/uml.model.ts @@ -2,64 +2,64 @@ import {UiColumnDefinition} from '../../components/data-view/models/result-set.m import {EntityType} from '../../models/catalog.model'; export class Uml { - constructor( - public tables: Map, - public foreignKeys: ForeignKey[] - ) { - } + constructor( + public tables: Map, + public foreignKeys: ForeignKey[] + ) { + } } export class DbTable { - tableName: string; - schema: string; - columns: UiColumnDefinition[]; - primaryKeyFields: string[]; - uniqueColumns: string[]; - modifiable: boolean; - tableType: EntityType; + tableName: string; + schema: string; + columns: UiColumnDefinition[]; + primaryKeyFields: string[]; + uniqueColumns: string[]; + modifiable: boolean; + tableType: EntityType; } export class ForeignKey { - fkName: string; - id: number; + fkName: string; + id: number; - targetSchema: string; - targetTable: string; - targetColumn: string; + targetSchema: string; + targetTable: string; + targetColumn: string; - sourceSchema: string; - sourceTable: string; - sourceColumn: string; + sourceSchema: string; + sourceTable: string; + sourceColumn: string; - onUpdate: string; - onDelete: string; + onUpdate: string; + onDelete: string; - constructor(id: number, fkName: string, schema: string, fkTable: string, fkCol: string, pkTable: string, pkCol: string) { - this.id = id; - this.fkName = fkName; - this.targetSchema = schema; - this.sourceSchema = schema; - this.sourceTable = fkTable; - this.sourceColumn = fkCol; - this.targetTable = pkTable; - this.targetColumn = pkCol; - } + constructor(id: number, fkName: string, schema: string, fkTable: string, fkCol: string, pkTable: string, pkCol: string) { + this.id = id; + this.fkName = fkName; + this.targetSchema = schema; + this.sourceSchema = schema; + this.sourceTable = fkTable; + this.sourceColumn = fkCol; + this.targetTable = pkTable; + this.targetColumn = pkCol; + } - updateAction(action: string) { - this.onUpdate = action; - return this; - } + updateAction(action: string) { + this.onUpdate = action; + return this; + } - deleteAction(action: string) { - this.onDelete = action; - return this; - } + deleteAction(action: string) { + this.onDelete = action; + return this; + } } export interface SvgLine { - x1: number; - y1: number; - x2: number; - y2: number; + x1: number; + y1: number; + x2: number; + y2: number; } diff --git a/src/app/views/util/reload-button/reload-button.component.ts b/src/app/views/util/reload-button/reload-button.component.ts index 9e60faf6..e46ad9cf 100644 --- a/src/app/views/util/reload-button/reload-button.component.ts +++ b/src/app/views/util/reload-button/reload-button.component.ts @@ -1,25 +1,25 @@ import {Component, Input, signal} from '@angular/core'; @Component({ - selector: 'app-reload-button', - templateUrl: './reload-button.component.html', - styleUrls: ['./reload-button.component.scss'] + selector: 'app-reload-button', + templateUrl: './reload-button.component.html', + styleUrls: ['./reload-button.component.scss'] }) export class ReloadButtonComponent { - $condition = signal(false); + $condition = signal(false); - @Input() set condition(condition: NonNullable) { - this.$condition.set(condition); - } + @Input() set condition(condition: NonNullable) { + this.$condition.set(condition); + } - $loading = signal(false); + $loading = signal(false); - @Input() set loading(loading: boolean) { - this.$loading.set(loading); - } + @Input() set loading(loading: boolean) { + this.$loading.set(loading); + } - @Input() action: (() => void); + @Input() action: (() => void); } diff --git a/src/app/views/views-routing.module.ts b/src/app/views/views-routing.module.ts index c495e935..e40afa30 100644 --- a/src/app/views/views-routing.module.ts +++ b/src/app/views/views-routing.module.ts @@ -15,177 +15,177 @@ import {UnsavedChangesGuard} from '../plugins/notebooks/services/unsaved-changes import {DockerconfigComponent} from './dockerconfig/dockerconfig.component'; const routes: Routes = [ - { - path: 'about', - component: AboutComponent, - data: { - title: 'about' - } - }, - { - path: 'monitoring', - component: MonitoringComponent, - data: { - title: 'Monitoring' - } - }, - { - path: 'monitoring/:id', - component: MonitoringComponent, - data: { - title: 'Monitoring' - } - }, - { - path: 'dashboard', - component: DashboardComponent, - data: { - title: 'Dashboard' - } - }, - { - path: 'dashboard/:id', - component: DashboardComponent, - data: { - title: 'Dashboard' - } - }, - { - path: 'uml', - redirectTo: 'uml/', - pathMatch: 'full' - }, - { - path: 'uml/:id', - component: UmlComponent, - data: { - title: 'UML' - } - }, - { - path: 'querying', - redirectTo: 'querying/console', - pathMatch: 'full' - }, - { - path: 'querying/:route', - component: QueryingComponent, - data: { - title: 'Querying' - } - }, - { - path: 'data-table', - redirectTo: 'data-table/', - pathMatch: 'full' - }, - { - path: 'data-table/:id', - component: TableViewComponent, - data: { - title: 'Data Table' - } - }, - { - path: 'data-table/:id/:page', - component: TableViewComponent, - data: { - title: 'Data Table' - } - }, - { - path: 'schema-editing', - redirectTo: 'schema-editing/', - pathMatch: 'full' - }, - { - path: 'schema-editing/:id', - component: SchemaEditingComponent, - data: { - title: 'Namespaces' - } - }, - { - path: 'schema-editing/:id/statistics-column', - component: SchemaEditingComponent, - data: { - title: 'Statistics' - } - }, - { - path: 'config', - component: FormGeneratorComponent, - data: { - title: 'Form Generator' - } - }, - { - path: 'config/dockerConfig', - component: DockerconfigComponent, - data: { - title: 'Docker Setup' - } - }, - { - path: 'config/dockerPage', - component: DockerconfigComponent, - data: { - title: 'Docker Setup' - } - }, - { - path: 'config/:page', - component: FormGeneratorComponent, - data: { - title: 'Form Generator' - } - }, - { - path: 'adapters', - component: AdaptersComponent, - data: { - title: 'Adapters' - } - }, - { - path: 'adapters/:action', - component: AdaptersComponent, - data: { - title: 'Adapters' - } - }, - { - path: 'queryInterfaces', - component: QueryInterfacesComponent, - data: { - title: 'QueryInterfaces' - } - }, - { - path: 'queryInterfaces/:action', - component: QueryInterfacesComponent, - data: { - title: 'QueryInterfaces' - } - }, - { - path: 'notebooks', - children: [ - { - path: '**', - component: NotebooksComponent, - data: { - title: 'Notebooks' - }, - canDeactivate: [UnsavedChangesGuard] - } - ] - }, + { + path: 'about', + component: AboutComponent, + data: { + title: 'about' + } + }, + { + path: 'monitoring', + component: MonitoringComponent, + data: { + title: 'Monitoring' + } + }, + { + path: 'monitoring/:id', + component: MonitoringComponent, + data: { + title: 'Monitoring' + } + }, + { + path: 'dashboard', + component: DashboardComponent, + data: { + title: 'Dashboard' + } + }, + { + path: 'dashboard/:id', + component: DashboardComponent, + data: { + title: 'Dashboard' + } + }, + { + path: 'uml', + redirectTo: 'uml/', + pathMatch: 'full' + }, + { + path: 'uml/:id', + component: UmlComponent, + data: { + title: 'UML' + } + }, + { + path: 'querying', + redirectTo: 'querying/console', + pathMatch: 'full' + }, + { + path: 'querying/:route', + component: QueryingComponent, + data: { + title: 'Querying' + } + }, + { + path: 'data-table', + redirectTo: 'data-table/', + pathMatch: 'full' + }, + { + path: 'data-table/:id', + component: TableViewComponent, + data: { + title: 'Data Table' + } + }, + { + path: 'data-table/:id/:page', + component: TableViewComponent, + data: { + title: 'Data Table' + } + }, + { + path: 'schema-editing', + redirectTo: 'schema-editing/', + pathMatch: 'full' + }, + { + path: 'schema-editing/:id', + component: SchemaEditingComponent, + data: { + title: 'Namespaces' + } + }, + { + path: 'schema-editing/:id/statistics-column', + component: SchemaEditingComponent, + data: { + title: 'Statistics' + } + }, + { + path: 'config', + component: FormGeneratorComponent, + data: { + title: 'Form Generator' + } + }, + { + path: 'config/dockerConfig', + component: DockerconfigComponent, + data: { + title: 'Docker Setup' + } + }, + { + path: 'config/dockerPage', + component: DockerconfigComponent, + data: { + title: 'Docker Setup' + } + }, + { + path: 'config/:page', + component: FormGeneratorComponent, + data: { + title: 'Form Generator' + } + }, + { + path: 'adapters', + component: AdaptersComponent, + data: { + title: 'Adapters' + } + }, + { + path: 'adapters/:action', + component: AdaptersComponent, + data: { + title: 'Adapters' + } + }, + { + path: 'queryInterfaces', + component: QueryInterfacesComponent, + data: { + title: 'QueryInterfaces' + } + }, + { + path: 'queryInterfaces/:action', + component: QueryInterfacesComponent, + data: { + title: 'QueryInterfaces' + } + }, + { + path: 'notebooks', + children: [ + { + path: '**', + component: NotebooksComponent, + data: { + title: 'Notebooks' + }, + canDeactivate: [UnsavedChangesGuard] + } + ] + }, ]; @NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] }) export class ViewsRoutingModule { } diff --git a/src/app/views/views.module.ts b/src/app/views/views.module.ts index 278928d6..e995f546 100644 --- a/src/app/views/views.module.ts +++ b/src/app/views/views.module.ts @@ -20,7 +20,9 @@ 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'; @@ -33,160 +35,164 @@ 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'; import {DockerconfigComponent} from './dockerconfig/dockerconfig.component'; import { - BadgeComponent, - BorderDirective, - ButtonCloseDirective, - ButtonDirective, - ButtonGroupComponent, - CardBodyComponent, - CardComponent, - CardFooterComponent, - CardHeaderComponent, - ColComponent, - ColDirective, - ContainerComponent, - DropdownComponent, - DropdownDividerDirective, - DropdownItemDirective, - DropdownMenuDirective, - DropdownToggleDirective, - FormCheckComponent, - FormCheckInputDirective, - FormCheckLabelDirective, - FormControlDirective, - FormDirective, - FormFeedbackComponent, - FormSelectDirective, - FormTextDirective, - GutterDirective, - HeaderComponent, - InputGroupComponent, - InputGroupTextDirective, - ModalBodyComponent, - ModalComponent, - ModalContentComponent, - ModalDialogComponent, - ModalFooterComponent, - ModalHeaderComponent, - ModalTitleDirective, - ModalToggleDirective, - PlaceholderDirective, - ProgressBarComponent, - ProgressComponent, - RowComponent, - RowDirective, - SpinnerComponent, - TableDirective, - TooltipDirective -} from '@coreui/angular'; -import {EditEntityComponent} from './schema-editing/edit-entity/edit-entity.component'; -import {TreeModule} from "@ali-hm/angular-tree-component"; - - -@NgModule({ - imports: [ - //AppModule, - CommonModule, - ViewsRoutingModule, - FormsModule, - ReactiveFormsModule, - ButtonsModule.forRoot(), - CollapseModule, - ComponentsModule, - TypeaheadModule, - // coreui / bootstrap - TooltipModule.forRoot(), - BsDropdownModule, - DragDropModule, - ModalModule.forRoot(), - AutocompleteLibModule, - ProgressbarModule, - PopoverModule, - RowComponent, - ColComponent, - ContainerComponent, - CardComponent, - CardHeaderComponent, + BadgeComponent, + BorderDirective, + ButtonCloseDirective, + ButtonDirective, + ButtonGroupComponent, CardBodyComponent, - GutterDirective, - HeaderComponent, + CardComponent, CardFooterComponent, - BorderDirective, - InputGroupComponent, - FormDirective, - FormFeedbackComponent, - InputGroupTextDirective, - FormControlDirective, - RowDirective, + CardHeaderComponent, + ColComponent, ColDirective, - FormSelectDirective, - ButtonDirective, - TableDirective, - FormCheckLabelDirective, - FormCheckInputDirective, + ContainerComponent, DropdownComponent, - DropdownToggleDirective, + DropdownDividerDirective, + DropdownItemDirective, DropdownMenuDirective, + DropdownToggleDirective, + FormCheckComponent, + FormCheckInputDirective, + FormCheckLabelDirective, + FormControlDirective, + FormDirective, + FormFeedbackComponent, + FormSelectDirective, FormTextDirective, + GutterDirective, + HeaderComponent, + InputGroupComponent, + InputGroupTextDirective, + ModalBodyComponent, ModalComponent, ModalContentComponent, ModalDialogComponent, - ModalHeaderComponent, - ModalBodyComponent, ModalFooterComponent, + ModalHeaderComponent, ModalTitleDirective, - ButtonCloseDirective, ModalToggleDirective, - ButtonGroupComponent, - DropdownItemDirective, - DropdownDividerDirective, - TooltipDirective, - SpinnerComponent, - NgOptimizedImage, - BadgeComponent, - FormCheckComponent, - TreeModule, PlaceholderDirective, + ProgressBarComponent, ProgressComponent, - ProgressBarComponent - ], - declarations: [ - EditColumnsComponent, - FormGeneratorComponent, - GraphicalQueryingComponent, - ConsoleComponent, - TableViewComponent, - UmlComponent, - SchemaEditingComponent, - EditTablesComponent, - DocumentEditCollectionsComponent, - DocumentEditCollectionComponent, - GraphEditGraphComponent, - MonitoringComponent, - DashboardComponent, - AlgebraComponent, - QueryingComponent, - NodeComponent, - AdaptersComponent, - RefinementOptionsComponent, - AboutComponent, - QueryInterfacesComponent, - EditSourceColumnsComponent, - StatisticsColumnComponent, - ValuePipe, - SearchFilterPipe, - FileUploaderComponent, - DockerconfigComponent, - EditEntityComponent, - ], - exports: [] + RowComponent, + RowDirective, + SpinnerComponent, + TableDirective, + TooltipDirective +} from '@coreui/angular'; +import {EditEntityComponent} from './schema-editing/edit-entity/edit-entity.component'; +import {TreeModule} from '@ali-hm/angular-tree-component'; + + +@NgModule({ + imports: [ + //AppModule, + CommonModule, + ViewsRoutingModule, + FormsModule, + ReactiveFormsModule, + ButtonsModule.forRoot(), + CollapseModule, + ComponentsModule, + TypeaheadModule, + // coreui / bootstrap + TooltipModule.forRoot(), + BsDropdownModule, + DragDropModule, + ModalModule.forRoot(), + AutocompleteLibModule, + ProgressbarModule, + PopoverModule, + RowComponent, + ColComponent, + ContainerComponent, + CardComponent, + CardHeaderComponent, + CardBodyComponent, + GutterDirective, + HeaderComponent, + CardFooterComponent, + BorderDirective, + InputGroupComponent, + FormDirective, + FormFeedbackComponent, + InputGroupTextDirective, + FormControlDirective, + RowDirective, + ColDirective, + FormSelectDirective, + ButtonDirective, + TableDirective, + FormCheckLabelDirective, + FormCheckInputDirective, + DropdownComponent, + DropdownToggleDirective, + DropdownMenuDirective, + FormTextDirective, + ModalComponent, + ModalContentComponent, + ModalDialogComponent, + ModalHeaderComponent, + ModalBodyComponent, + ModalFooterComponent, + ModalTitleDirective, + ButtonCloseDirective, + ModalToggleDirective, + ButtonGroupComponent, + DropdownItemDirective, + DropdownDividerDirective, + TooltipDirective, + SpinnerComponent, + NgOptimizedImage, + BadgeComponent, + FormCheckComponent, + TreeModule, + PlaceholderDirective, + ProgressComponent, + ProgressBarComponent + ], + declarations: [ + EditColumnsComponent, + FormGeneratorComponent, + GraphicalQueryingComponent, + ConsoleComponent, + TableViewComponent, + UmlComponent, + SchemaEditingComponent, + EditTablesComponent, + DocumentEditCollectionsComponent, + DocumentEditCollectionComponent, + GraphEditGraphComponent, + MonitoringComponent, + DashboardComponent, + AlgebraComponent, + QueryingComponent, + NodeComponent, + AdaptersComponent, + RefinementOptionsComponent, + AboutComponent, + QueryInterfacesComponent, + EditSourceColumnsComponent, + StatisticsColumnComponent, + ValuePipe, + SearchFilterPipe, + FileUploaderComponent, + DockerconfigComponent, + EditEntityComponent, + ], + exports: [] }) export class ViewsModule { } diff --git a/src/test.ts b/src/test.ts index 1abdde2f..7563caa6 100644 --- a/src/test.ts +++ b/src/test.ts @@ -8,6 +8,6 @@ import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angul getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { - teardown: { destroyAfterEach: false } -} + teardown: {destroyAfterEach: false} + } );