From 0137e63d4626039751170000a2bf0aa730b01592 Mon Sep 17 00:00:00 2001 From: Mark Drilling Date: Mon, 27 Aug 2018 15:34:10 -0500 Subject: [PATCH] TEIIDTOOLS-477 consolidates dataservice views --- .../add-connection-wizard.component.ts | 1 - .../about-dialog/about-dialog.component.ts | 2 +- ...create-virtualization-dialog.component.css | 0 ...reate-virtualization-dialog.component.html | 50 ++ ...te-virtualization-dialog.component.spec.ts | 51 ++ .../create-virtualization-dialog.component.ts | 204 ++++++++ .../create-virtualization-result.model.ts | 88 ++++ .../dataservices-routing.module.ts | 2 - .../dataservices/dataservices.component.html | 8 +- .../dataservices/dataservices.component.ts | 256 ++++++++-- .../app/dataservices/dataservices.module.ts | 15 +- .../dataservices/shared/composition.model.ts | 18 + .../dataservices/shared/dataservice.model.ts | 18 +- .../shared/dataservice.service.ts | 3 +- .../shared/dataservices-constants.ts | 10 +- .../shared/mock-dataservice.service.ts | 32 ++ .../dataservices/shared/mock-vdb.service.ts | 35 ++ .../app/dataservices/shared/vdb.service.ts | 6 +- .../shared/view-definition.model.ts | 60 ++- .../view-card/view-card.component.css | 4 - .../view-card/view-card.component.html | 60 --- .../view-card/view-card.component.spec.ts | 57 --- .../view-card/view-card.component.ts | 144 ------ .../view-cards/view-cards.component.css | 4 - .../view-cards/view-cards.component.html | 8 - .../view-cards/view-cards.component.spec.ts | 57 --- .../view-cards/view-cards.component.ts | 71 --- .../add-composition-wizard.component.ts | 7 +- .../create-view-dialog.component.css | 0 .../create-view-dialog.component.html | 30 ++ .../create-view-dialog.component.spec.ts | 51 ++ .../create-view-dialog.component.ts | 164 +++++++ .../view-editor/view-canvas/canvas.service.ts | 4 + .../view-canvas/models/canvas-graph.ts | 6 + .../view-canvas/view-canvas.component.ts | 37 +- .../view-editor-header.component.css | 67 ++- .../view-editor-header.component.html | 93 ++-- .../view-editor-header.component.spec.ts | 8 +- .../view-editor-header.component.ts | 363 ++++++++++++-- .../view-editor/view-editor-i18n.ts | 17 +- .../view-editor/view-editor.component.html | 3 +- .../view-editor/view-editor.component.ts | 344 +++++++------ .../view-editor/view-editor.service.ts | 77 +-- .../view-editor/view-validator.ts | 5 +- .../virtualization.component.css | 3 - .../virtualization.component.html | 125 ----- .../virtualization.component.spec.ts | 77 --- .../virtualization.component.ts | 456 ------------------ .../confirm-dialog.component.ts | 2 + ngapp/src/app/shared/test-data.service.ts | 25 +- 50 files changed, 1718 insertions(+), 1510 deletions(-) create mode 100644 ngapp/src/app/dataservices/create-virtualization-dialog/create-virtualization-dialog.component.css create mode 100644 ngapp/src/app/dataservices/create-virtualization-dialog/create-virtualization-dialog.component.html create mode 100644 ngapp/src/app/dataservices/create-virtualization-dialog/create-virtualization-dialog.component.spec.ts create mode 100644 ngapp/src/app/dataservices/create-virtualization-dialog/create-virtualization-dialog.component.ts create mode 100644 ngapp/src/app/dataservices/create-virtualization-dialog/create-virtualization-result.model.ts delete mode 100644 ngapp/src/app/dataservices/virtualization/view-cards/view-card/view-card.component.css delete mode 100644 ngapp/src/app/dataservices/virtualization/view-cards/view-card/view-card.component.html delete mode 100644 ngapp/src/app/dataservices/virtualization/view-cards/view-card/view-card.component.spec.ts delete mode 100644 ngapp/src/app/dataservices/virtualization/view-cards/view-card/view-card.component.ts delete mode 100644 ngapp/src/app/dataservices/virtualization/view-cards/view-cards.component.css delete mode 100644 ngapp/src/app/dataservices/virtualization/view-cards/view-cards.component.html delete mode 100644 ngapp/src/app/dataservices/virtualization/view-cards/view-cards.component.spec.ts delete mode 100644 ngapp/src/app/dataservices/virtualization/view-cards/view-cards.component.ts create mode 100644 ngapp/src/app/dataservices/virtualization/view-editor/create-view-dialog/create-view-dialog.component.css create mode 100644 ngapp/src/app/dataservices/virtualization/view-editor/create-view-dialog/create-view-dialog.component.html create mode 100644 ngapp/src/app/dataservices/virtualization/view-editor/create-view-dialog/create-view-dialog.component.spec.ts create mode 100644 ngapp/src/app/dataservices/virtualization/view-editor/create-view-dialog/create-view-dialog.component.ts delete mode 100644 ngapp/src/app/dataservices/virtualization/virtualization.component.css delete mode 100644 ngapp/src/app/dataservices/virtualization/virtualization.component.html delete mode 100644 ngapp/src/app/dataservices/virtualization/virtualization.component.spec.ts delete mode 100644 ngapp/src/app/dataservices/virtualization/virtualization.component.ts diff --git a/ngapp/src/app/connections/add-connection-wizard/add-connection-wizard.component.ts b/ngapp/src/app/connections/add-connection-wizard/add-connection-wizard.component.ts index f72d82c5..7ab38a94 100644 --- a/ngapp/src/app/connections/add-connection-wizard/add-connection-wizard.component.ts +++ b/ngapp/src/app/connections/add-connection-wizard/add-connection-wizard.component.ts @@ -367,7 +367,6 @@ export class AddConnectionWizardComponent implements OnInit { connection.setDescription(this.connectionDescription); connection.setServiceCatalogSource(this.selectedServiceCatalogSource.getId()); - const self = this; if (this.selectionService.hasSelectedConnection) { this.updateDeployConnection(connection); } else { diff --git a/ngapp/src/app/core/about-dialog/about-dialog.component.ts b/ngapp/src/app/core/about-dialog/about-dialog.component.ts index 425ef9aa..1e891ed0 100644 --- a/ngapp/src/app/core/about-dialog/about-dialog.component.ts +++ b/ngapp/src/app/core/about-dialog/about-dialog.component.ts @@ -73,7 +73,7 @@ export class AboutDialogComponent implements OnInit { } as AboutModalConfig; }, ( error ) => { - this.logger.error( error, "Error getting about information."); + self.logger.error( error, "Error getting about information."); } ); } diff --git a/ngapp/src/app/dataservices/create-virtualization-dialog/create-virtualization-dialog.component.css b/ngapp/src/app/dataservices/create-virtualization-dialog/create-virtualization-dialog.component.css new file mode 100644 index 00000000..e69de29b diff --git a/ngapp/src/app/dataservices/create-virtualization-dialog/create-virtualization-dialog.component.html b/ngapp/src/app/dataservices/create-virtualization-dialog/create-virtualization-dialog.component.html new file mode 100644 index 00000000..4de0c55a --- /dev/null +++ b/ngapp/src/app/dataservices/create-virtualization-dialog/create-virtualization-dialog.component.html @@ -0,0 +1,50 @@ + + + diff --git a/ngapp/src/app/dataservices/create-virtualization-dialog/create-virtualization-dialog.component.spec.ts b/ngapp/src/app/dataservices/create-virtualization-dialog/create-virtualization-dialog.component.spec.ts new file mode 100644 index 00000000..f26bfcc5 --- /dev/null +++ b/ngapp/src/app/dataservices/create-virtualization-dialog/create-virtualization-dialog.component.spec.ts @@ -0,0 +1,51 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HttpModule } from "@angular/http"; +import { BsModalRef, ModalModule } from "ngx-bootstrap"; +import { + ActionModule, + NotificationModule +} from "patternfly-ng"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { DataserviceService } from "@dataservices/shared/dataservice.service"; +import { MockDataserviceService } from "@dataservices/shared/mock-dataservice.service"; +import { VdbService } from "@dataservices/shared/vdb.service"; +import { MockVdbService } from "@dataservices/shared/mock-vdb.service"; +import { CreateVirtualizationDialogComponent } from "./create-virtualization-dialog.component"; +import { AppSettingsService } from "@core/app-settings.service"; +import { LoggerService } from "@core/logger.service"; +import { NotifierService } from "@dataservices/shared/notifier.service"; + +describe('CreateVirtualizationDialogComponent', () => { + let component: CreateVirtualizationDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpModule, + FormsModule, + ReactiveFormsModule, + ModalModule.forRoot(), + ActionModule, + NotificationModule + ], + declarations: [ CreateVirtualizationDialogComponent ], + providers: [ AppSettingsService, BsModalRef, LoggerService, NotifierService, + { provide: DataserviceService, useClass: MockDataserviceService }, + { provide: VdbService, useClass: MockVdbService } + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CreateVirtualizationDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ngapp/src/app/dataservices/create-virtualization-dialog/create-virtualization-dialog.component.ts b/ngapp/src/app/dataservices/create-virtualization-dialog/create-virtualization-dialog.component.ts new file mode 100644 index 00000000..ace1d70b --- /dev/null +++ b/ngapp/src/app/dataservices/create-virtualization-dialog/create-virtualization-dialog.component.ts @@ -0,0 +1,204 @@ +/** + * @license + * Copyright 2017 JBoss Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, OnInit } from "@angular/core"; +import { Output } from "@angular/core"; +import { EventEmitter } from "@angular/core"; +import { BsModalRef } from "ngx-bootstrap"; +import { LoggerService } from "@core/logger.service"; +import { ViewEditorI18n } from "@dataservices/virtualization/view-editor/view-editor-i18n"; +import { AbstractControl, FormControl, FormGroup } from "@angular/forms"; +import { VdbService } from "@dataservices/shared/vdb.service"; +import { DataserviceService } from "@dataservices/shared/dataservice.service"; +import { CreateVirtualizationResult } from "@dataservices/create-virtualization-dialog/create-virtualization-result.model"; + +@Component({ + selector: "app-create-virtualization-dialog", + templateUrl: "./create-virtualization-dialog.component.html", + styleUrls: ["./create-virtualization-dialog.component.css"] +}) +/** + * CreateVirtualization Dialog. Invoke this from another component as follows: + * + * this.modalRef = this.modalService.show(CreateVirtualizationDialogComponent, {initialState}); + * this.modalRef.content.okAction.take(1).subscribe((createResult) => { + * // do something with dialog result - CreateVirtualizationResult + * }); + * + * The expected initial state is as follows: + * const initialState = { + * title: "The dialog title", + * cancelButtonText: "Text for cancel button", + * confirmButtonText: "Text for confirm button" + * }; + */ +export class CreateVirtualizationDialogComponent implements OnInit { + + @Output() public okAction: EventEmitter = new EventEmitter(); + + public readonly title = ViewEditorI18n.createVirtualizationDialogTitle; + public readonly message = ViewEditorI18n.createVirtualizationDialogMessage; + public readonly cancelButtonText = ViewEditorI18n.cancelButtonText; + public readonly okButtonText = ViewEditorI18n.okButtonText; + public okButtonEnabled = false; + public bsModalRef: BsModalRef; + public virtNameValidationError = ""; + public viewNameValidationError = ""; + public virtualizationPropertyForm: FormGroup; + + private loggerService: LoggerService; + private dataserviceService: DataserviceService; + private vdbService: VdbService; + private serviceVdbName = ""; + + constructor(bsModalRef: BsModalRef, logger: LoggerService, + dataserviceService: DataserviceService, vdbService: VdbService) { + this.bsModalRef = bsModalRef; + this.loggerService = logger; + this.dataserviceService = dataserviceService; + const dService = this.dataserviceService.getSelectedDataservice(); + if ( dService && dService !== null ) { + this.serviceVdbName = dService.getServiceVdbName(); + } + this.vdbService = vdbService; + this.createPropertyForm(); + } + + public ngOnInit(): void { + this.virtualizationPropertyForm.controls["virtName"].setValue(""); + this.virtualizationPropertyForm.controls["virtDescription"].setValue(""); + this.virtualizationPropertyForm.controls["viewName"].setValue(""); + this.virtualizationPropertyForm.controls["viewDescription"].setValue(""); + } + + /* + * Creates the view property form + */ + private createPropertyForm(): void { + this.virtualizationPropertyForm = new FormGroup({ + virtName: new FormControl( "", this.handleVirtNameChanged.bind( this ) ), + virtDescription: new FormControl(""), + viewName: new FormControl( "", this.handleViewNameChanged.bind( this ) ), + viewDescription: new FormControl("") + }); + } + + /** + * Handler for virtualization name changes. + * @param {AbstractControl} input + */ + public handleVirtNameChanged( input: AbstractControl ): void { + const self = this; + + this.dataserviceService.isValidName( input.value ).subscribe( + ( errorMsg ) => { + if ( errorMsg ) { + // only update if error has changed + if ( errorMsg !== self.virtNameValidationError ) { + self.virtNameValidationError = errorMsg; + } + } else { // name is valid + self.virtNameValidationError = ""; + } + self.setOkButtonEnablement(); + }, + ( error ) => { + self.loggerService.error( "[handleNameChanged] Error: %o", error ); + self.virtNameValidationError = "Error validating view name"; + self.setOkButtonEnablement(); + } ); + } + + /** + * Handler for view name changes. Since this will be the first view in the virtualization, + * no need to make a service call - just do some general name checking + * @param {AbstractControl} input + */ + public handleViewNameChanged( input: AbstractControl ): void { + this.viewNameValidationError = this.validateViewName(input.value); + this.setOkButtonEnablement(); + } + + /** + * OK selected. Emit ViewDefinition with the view, then modal is closed + */ + public onOkSelected(): void { + const virtName = this.virtualizationPropertyForm.controls["virtName"].value; + const virtDescr = this.virtualizationPropertyForm.controls["virtDescription"].value; + const viewName = this.virtualizationPropertyForm.controls["viewName"].value; + const viewDescr = this.virtualizationPropertyForm.controls["viewDescription"].value; + + const result = new CreateVirtualizationResult(); + result.setVirtualizationName(virtName); + result.setVirtualizationDescription(virtDescr); + result.setViewName(viewName); + result.setViewDescription(viewDescr); + this.okAction.emit(result); + + this.bsModalRef.hide(); + } + + /** + * Cancel selected. The modal is closed. + */ + public onCancelSelected(): void { + this.bsModalRef.hide(); + } + + /* + * Return the virtualization name valid state + */ + public get virtNameValid(): boolean { + return this.virtNameValidationError == null || this.virtNameValidationError.length === 0; + } + + /* + * Return the view name valid state + */ + public get viewNameValid(): boolean { + return this.viewNameValidationError == null || this.viewNameValidationError.length === 0; + } + + /** + * Validate the provided view name. If the name is valid, an empty string will be returned. If the view is invalid, + * the error message is returned. + * @param {string} viewName the view name being validated + * @return {string} the validation message + */ + private validateViewName( viewName: string ): string { + if ( !viewName || viewName === null || viewName.length === 0 ) { + return "View name cannot be empty"; + } + const isValid = /^\w+$/.test(viewName); + if ( !isValid ) { + return "View name can only contain letters, digits and underscores"; + } + return ""; + } + + /** + * Sets the OK button enablement. Both the virtualization name and view name must be valid. + */ + private setOkButtonEnablement(): void { + if (this.virtNameValid && this.viewNameValid) { + this.okButtonEnabled = true; + } else { + this.okButtonEnabled = false; + } + } + +} diff --git a/ngapp/src/app/dataservices/create-virtualization-dialog/create-virtualization-result.model.ts b/ngapp/src/app/dataservices/create-virtualization-dialog/create-virtualization-result.model.ts new file mode 100644 index 00000000..21b98e04 --- /dev/null +++ b/ngapp/src/app/dataservices/create-virtualization-dialog/create-virtualization-result.model.ts @@ -0,0 +1,88 @@ +/** + * @license + * Copyright 2017 JBoss Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * CreateVirtualizationResult model - to hold the results of the dialog entry + */ +export class CreateVirtualizationResult { + + private virtName: string; + private virtDescription = ""; + private viewName: string; + private viewDescription = ""; + + constructor() { + // nothing to do + } + + /** + * @returns {string} the virtualization name + */ + public getVirtualizationName(): string { + return this.virtName; + } + + /** + * @param {string} name the virtualization name + */ + public setVirtualizationName( name?: string ): void { + this.virtName = name ? name : null; + } + + /** + * @returns {string} the virtualization description + */ + public getVirtualizationDescription(): string { + return this.virtDescription; + } + + /** + * @param {string} description the virtualization description + */ + public setVirtualizationDescription( description?: string ): void { + this.virtDescription = description ? description : ""; + } + + /** + * @returns {string} the view name + */ + public getViewName(): string { + return this.viewName; + } + + /** + * @param {string} name the view name + */ + public setViewName( name?: string ): void { + this.viewName = name ? name : null; + } + + /** + * @returns {string} the view description + */ + public getViewDescription(): string { + return this.viewDescription; + } + + /** + * @param {string} description the view description + */ + public setViewDescription( description?: string ): void { + this.viewDescription = description ? description : ""; + } + +} diff --git a/ngapp/src/app/dataservices/dataservices-routing.module.ts b/ngapp/src/app/dataservices/dataservices-routing.module.ts index ba777faa..62894f5b 100644 --- a/ngapp/src/app/dataservices/dataservices-routing.module.ts +++ b/ngapp/src/app/dataservices/dataservices-routing.module.ts @@ -22,11 +22,9 @@ import { DataservicesComponent } from "@dataservices/dataservices.component"; import { DataservicesConstants } from "@dataservices/shared/dataservices-constants"; import { TestDataserviceComponent } from "@dataservices/test-dataservice/test-dataservice.component"; import { ViewEditorComponent } from "@dataservices/virtualization/view-editor/view-editor.component"; -import { VirtualizationComponent } from "@dataservices/virtualization/virtualization.component"; const dataservicesRoutes: Routes = [ { path: DataservicesConstants.dataservicesRootRoute, component: DataservicesComponent }, - { path: DataservicesConstants.virtualizationRoute, component: VirtualizationComponent }, { path: DataservicesConstants.viewRoute, component: ViewEditorComponent }, { path: DataservicesConstants.testDataserviceRoute, component: TestDataserviceComponent } ]; diff --git a/ngapp/src/app/dataservices/dataservices.component.html b/ngapp/src/app/dataservices/dataservices.component.html index 76a9718b..5fc0426b 100644 --- a/ngapp/src/app/dataservices/dataservices.component.html +++ b/ngapp/src/app/dataservices/dataservices.component.html @@ -72,10 +72,10 @@

- - + +
diff --git a/ngapp/src/app/dataservices/dataservices.component.ts b/ngapp/src/app/dataservices/dataservices.component.ts index cfba1887..0c1d6457 100644 --- a/ngapp/src/app/dataservices/dataservices.component.ts +++ b/ngapp/src/app/dataservices/dataservices.component.ts @@ -51,6 +51,10 @@ import { } from "patternfly-ng"; import { Subscription } from "rxjs/Subscription"; import { SqlView } from "@dataservices/shared/sql-view.model"; +import { ViewEditorI18n } from "@dataservices/virtualization/view-editor/view-editor-i18n"; +import { CreateVirtualizationDialogComponent } from "@dataservices/create-virtualization-dialog/create-virtualization-dialog.component"; +import { ViewDefinition } from "@dataservices/shared/view-definition.model"; +import { ViewEditorState } from "@dataservices/shared/view-editor-state.model"; @Component({ moduleId: module.id, @@ -60,14 +64,8 @@ import { SqlView } from "@dataservices/shared/sql-view.model"; }) export class DataservicesComponent extends AbstractPageComponent implements OnInit { - public readonly exportInProgressHeader: string = "Publishing: "; - public readonly exportSuccessHeader: string = "Publishing Begun: "; - public readonly exportFailedHeader: string = "Publishing Failed: "; public readonly connectionsLoadedTag = "connections"; - public readonly downloadSuccessHeader: string = "Download Succeeded: "; - public readonly downloadFailedHeader: string = "Download Failed: "; - public filterConfig: FilterConfig; public filtersText = ""; public items: Dataservice[]; @@ -97,10 +95,10 @@ export class DataservicesComponent extends AbstractPageComponent implements OnIn private dataserviceService: DataserviceService; private vdbService: VdbService; private sortDirection: SortDirection = SortDirection.ASC; - private exportNotificationHeader: string; - private exportNotificationMessage: string; - private exportNotificationType = NotificationType.SUCCESS; - private exportNotificationVisible = false; + private toastNotificationHeader: string; + private toastNotificationMessage: string; + private toastNotificationType = NotificationType.SUCCESS; + private toastNotificationVisible = false; private dataserviceDeployStateSubscription: Subscription; private dataservicePublishStateSubscription: Subscription; private notifierService: NotifierService; @@ -323,10 +321,10 @@ export class DataservicesComponent extends AbstractPageComponent implements OnIn } /** - * @returns {boolean} true if dataservice export notification is to be shown + * @returns {boolean} true if dataservice toast notification is to be shown */ - public get showExportNotification(): boolean { - return this.exportNotificationVisible; + public get showToastNotification(): boolean { + return this.toastNotificationVisible; } /** @@ -475,63 +473,100 @@ export class DataservicesComponent extends AbstractPageComponent implements OnIn public onDownload(svcName: string): void { this.closeLookPanels(); - this.exportNotificationHeader = this.exportInProgressHeader; - this.exportNotificationMessage = "Downloading " + svcName + "..."; - this.exportNotificationType = NotificationType.INFO; - this.exportNotificationVisible = true; + this.setToastNotification(Toast.Type.Download, Toast.State.InProgress, svcName); + this.toastNotificationVisible = true; this.logger.debug("[DataservicesPageComponent] Downloading Dataservice: " + svcName); const self = this; this.dataserviceService .downloadDataservice(svcName) .subscribe( (wasSuccess) => { - self.exportNotificationHeader = this.downloadSuccessHeader; - self.exportNotificationMessage = " " + svcName + " was downloaded successfully!"; - self.exportNotificationType = NotificationType.SUCCESS; + self.setToastNotification(Toast.Type.Download, Toast.State.Successful, svcName); // Dismiss toast notification after 8 sec - setTimeout(() => self.exportNotificationVisible = false, 8000); + setTimeout(() => self.toastNotificationVisible = false, 8000); this.logger.debug("[DataservicesPageComponent] Download Dataservice was successful"); }, (error) => { - self.exportNotificationHeader = this.downloadFailedHeader; - self.exportNotificationMessage = " Failed to download dataservice " + svcName; - self.exportNotificationType = NotificationType.DANGER; + self.setToastNotification(Toast.Type.Download, Toast.State.Failed, svcName); // Dismiss toast notification after 8 sec - setTimeout(() => self.exportNotificationVisible = false, 8000); + setTimeout(() => self.toastNotificationVisible = false, 8000); this.logger.error("[DataservicesPageComponent] Download dataservice " + svcName + " failed."); } ); } + /** + * Set the toast notification details based on state and type. + * @param {Toast.Type} type the type of toast message (Toast.Type.xyz) + * @param {Toast.State} state the state of the progress (Toast.State.xyz) + * @param {string} virtualizationName the name of the virtualization + */ + private setToastNotification( type: Toast.Type, state: Toast.State, virtualizationName: string ): void { + // Set InProgress Toast details + if ( state === Toast.State.InProgress ) { + this.toastNotificationType = NotificationType.INFO; + if ( type === Toast.Type.Download ) { + this.toastNotificationHeader = "Downloading: "; + this.toastNotificationMessage = "Downloading " + virtualizationName + "..."; + } else if ( type === Toast.Type.Publish ) { + this.toastNotificationHeader = "Publishing: "; + this.toastNotificationMessage = "Publishing " + virtualizationName + "..."; + } else if ( type === Toast.Type.NewVirtualization ) { + this.toastNotificationHeader = "Creating: "; + this.toastNotificationMessage = "Creating " + virtualizationName + "..."; + } + // Set Successful Toast details + } else if ( state === Toast.State.Successful ) { + this.toastNotificationType = NotificationType.SUCCESS; + if ( type === Toast.Type.Download ) { + this.toastNotificationHeader = "Download Succeeded: "; + this.toastNotificationMessage = " " + virtualizationName + " was downloaded successfully!"; + } else if ( type === Toast.Type.Publish ) { + this.toastNotificationHeader = "Publishing Started: "; + this.toastNotificationMessage = " " + virtualizationName + " publishing successfully initiated."; + } else if ( type === Toast.Type.NewVirtualization ) { + this.toastNotificationHeader = "Create Succeeded: "; + this.toastNotificationMessage = " " + virtualizationName + " was created successfully!"; + } + // Set Failed Toast details + } else if ( state === Toast.State.Failed ) { + this.toastNotificationType = NotificationType.DANGER; + if ( type === Toast.Type.Download ) { + this.toastNotificationHeader = "Download Failed: "; + this.toastNotificationMessage = " Failed to download dataservice " + virtualizationName; + } else if ( type === Toast.Type.Publish ) { + this.toastNotificationHeader = "Publishing Failed: "; + this.toastNotificationMessage = " Failed to publish dataservice " + virtualizationName + "."; + } else if ( type === Toast.Type.NewVirtualization ) { + this.toastNotificationHeader = "Create Failed: "; + this.toastNotificationMessage = " Failed to create dataservice " + virtualizationName; + } + } + } + public onPublish(svcName: string): void { const selectedService = this.filteredDataservices.find((x) => x.getId() === svcName); const virtual: Virtualization = new Virtualization(selectedService.getServiceVdbName(), PublishState.SUBMITTED); selectedService.setServiceVirtualization(virtual); this.closeLookPanels(); - this.exportNotificationHeader = this.exportInProgressHeader; - this.exportNotificationMessage = "Publishing " + svcName + "..."; - this.exportNotificationType = NotificationType.INFO; - this.exportNotificationVisible = true; + this.setToastNotification(Toast.Type.Publish, Toast.State.InProgress, svcName); + this.toastNotificationVisible = true; this.logger.debug("[DataservicesPageComponent] Publishing Dataservice: " + svcName); const self = this; this.dataserviceService .publishDataservice(svcName) .subscribe( (status) => { - self.exportNotificationHeader = this.exportSuccessHeader; - self.exportNotificationMessage = " " + svcName + " publishing successfully initiated."; - self.exportNotificationType = NotificationType.INFO; + self.setToastNotification(Toast.Type.Publish, Toast.State.Successful, svcName); // Dismiss toast notification after 8 sec - setTimeout(() => self.exportNotificationVisible = false, 8000); + setTimeout(() => self.toastNotificationVisible = false, 8000); this.logger.debug("[DataservicesPageComponent] Initiated publishing of dataservice"); }, (error) => { - self.exportNotificationHeader = this.exportFailedHeader; - self.exportNotificationMessage = " Failed to publish dataservice " + svcName + "."; - self.exportNotificationType = NotificationType.DANGER; + self.setToastNotification(Toast.Type.Publish, Toast.State.Failed, svcName); // Dismiss toast notification after 8 sec - setTimeout(() => self.exportNotificationVisible = false, 8000); + setTimeout(() => self.toastNotificationVisible = false, 8000); this.logger.error("[DataservicesPageComponent] Publish dataservice " + svcName + " failed."); } ); @@ -563,16 +598,131 @@ export class DataservicesComponent extends AbstractPageComponent implements OnIn } /** - * Handle request for new Dataservice + * Handle request for new Virtualization */ public onNew(): void { - this.selectionService.setSelectedVirtualization(null); + // Open New Virtualization dialog + const initialState = { + title: ViewEditorI18n.createVirtualizationDialogTitle, + cancelButtonText: ViewEditorI18n.cancelButtonText, + okButtonText: ViewEditorI18n.okButtonText + }; + + // Show Dialog, act upon confirmation click + const modalRef = this.modalService.show(CreateVirtualizationDialogComponent, {initialState}); + modalRef.content.okAction.take(1).subscribe((dialogResult) => { + + // Create the new virtualization and view. + const virtName = dialogResult.getVirtualizationName(); + const virtDescr = dialogResult.getVirtualizationDescription(); + const viewName = dialogResult.getViewName(); + const viewDescr = dialogResult.getViewDescription(); + const viewDefn = new ViewDefinition(); + viewDefn.setName(viewName); + viewDefn.setDescription(viewDescr); + + // Display Toast notification + this.setToastNotification(Toast.Type.NewVirtualization, Toast.State.InProgress, virtName); + this.toastNotificationVisible = true; + + // Create the new virtualization + const newVirtualization = this.dataserviceService.newDataserviceInstance(virtName, virtDescr); + const self = this; + this.dataserviceService + .createDataservice(newVirtualization) + .subscribe( + (wasSuccess) => { + // Set the current virtualization to the newly created virtualization + self.selectVirtualizationCreateView(virtName, viewDefn); + }, + (error) => { + self.logger.error("[VirtualizationComponent] Error creating virtualization: %o", error); + // Display Toast notification + self.setToastNotification(Toast.Type.NewVirtualization, Toast.State.Failed, virtName); + // Dismiss toast notification after 8 sec + setTimeout(() => self.toastNotificationVisible = false, 8000); + } + ); - const link: string[] = [ DataservicesConstants.virtualizationPath ]; - this.logger.debug("[DataservicesPageComponent] Navigating to: %o", link); - this.router.navigate(link).then(() => { - // nothing to do }); + + } + + /* + * Select the specified Dataservice. create a starter view under it + * @param {string} dsName the name of the dataservice + * @param {string} viewDefn the view definition to create + */ + private selectVirtualizationCreateView(virtName: string, viewDefn: ViewDefinition): void { + const self = this; + this.dataserviceService + .getAllDataservices() + .subscribe( + (dataservices) => { + for (const ds of dataservices) { + if (ds.getId() === virtName) { + self.dataserviceService.setSelectedDataservice(ds); + self.selectionService.setSelectedVirtualization(ds); + self.createView(ds, viewDefn); + } + } + }, + (error) => { + self.logger.error("[VirtualizationComponent] Error selecting the virtualization: %o", error); + // Display Toast notification + self.setToastNotification(Toast.Type.NewVirtualization, Toast.State.Failed, virtName); + // Dismiss toast notification after 8 sec + setTimeout(() => self.toastNotificationVisible = false, 8000); + } + ); + } + + private createView(dataservice: Dataservice, viewDefn: ViewDefinition): void { + const selectedDs = this.dataserviceService.getSelectedDataservice(); + let editorId = ""; + if ( selectedDs || selectedDs !== null ) { + editorId = this.getEditorStateId(selectedDs, viewDefn); + } + + // Create new editor state to save + const editorState = new ViewEditorState(); + editorState.setId(editorId); + editorState.setViewDefinition(viewDefn); + + const virtName = selectedDs.getId(); + const self = this; + this.dataserviceService + .saveViewEditorStateRefreshViews(editorState, selectedDs.getId()) + .subscribe( + (wasSuccess) => { + // Dismiss toast since navigating away + self.toastNotificationVisible = false; + + // transfer control to the viewEditor + const link: string[] = [ DataservicesConstants.viewPath ]; + this.logger.debug("[DataservicesPageComponent] Navigating to: %o", link); + this.router.navigate(link).then(() => { + // nothing to do + }); + }, + (error) => { + self.logger.error("[VirtualizationComponent] Error saving the editor state: %o", error); + // Display Toast notification + self.setToastNotification(Toast.Type.NewVirtualization, Toast.State.Failed, virtName); + // Dismiss toast notification after 8 sec + setTimeout(() => self.toastNotificationVisible = false, 8000); + } + ); + } + + /** + * Construct id for the editor state + * @param {Dataservice} dataservice the dataservice + * @param {ViewDefinition} viewDefn the view definition + * @returns {string} the ID used to persist the editor state + */ + private getEditorStateId(dataservice: Dataservice, viewDefn: ViewDefinition): string { + return dataservice.getServiceVdbName().toLowerCase() + "." + viewDefn.getName(); } /** @@ -588,7 +738,7 @@ export class DataservicesComponent extends AbstractPageComponent implements OnIn // Sets the selected dataservice and edit mode before transferring this.selectionService.setSelectedVirtualization(selectedService); - const link: string[] = [ DataservicesConstants.virtualizationPath ]; + const link: string[] = [ DataservicesConstants.viewPath ]; this.logger.debug("[DataservicesPageComponent] Navigating to: %o", link); this.router.navigate(link).then(() => { // nothing to do @@ -715,8 +865,6 @@ export class DataservicesComponent extends AbstractPageComponent implements OnIn match = item.getId().match(filter.value) !== null; } else if (filter.field.id === "description") { match = item.getDescription().match(filter.value) !== null; - } else if (filter.field.id === "view") { - match = item.getViews() === filter.value; } return match; } @@ -821,3 +969,19 @@ export class DataservicesComponent extends AbstractPageComponent implements OnIn this.odataSvcName = svcName; } } + +/** + * Internal enums for Toast Notifications + */ +export namespace Toast { + export enum State { + InProgress, + Successful, + Failed + } + export enum Type { + Download, + Publish, + NewVirtualization + } +} diff --git a/ngapp/src/app/dataservices/dataservices.module.ts b/ngapp/src/app/dataservices/dataservices.module.ts index 52231ea8..29612eae 100644 --- a/ngapp/src/app/dataservices/dataservices.module.ts +++ b/ngapp/src/app/dataservices/dataservices.module.ts @@ -67,14 +67,13 @@ import { WizardModule } from "patternfly-ng"; import { OdataControlComponent } from "./odata-control/odata-control.component"; import { AccordionModule, BsDropdownModule, TabsModule, TooltipModule } from 'ngx-bootstrap'; -import { ViewCardComponent } from "./virtualization/view-cards/view-card/view-card.component"; -import { ViewCardsComponent } from "./virtualization/view-cards/view-cards.component"; -import { VirtualizationComponent } from "./virtualization/virtualization.component"; import { ConnectionTreeSelectorComponent } from './virtualization/view-editor/connection-table-dialog/connection-tree-selector/connection-tree-selector.component'; import { ConnectionTableDialogComponent } from './virtualization/view-editor/connection-table-dialog/connection-table-dialog.component'; import { ProgressDialogComponent } from "@shared/progress-dialog/progress-dialog.component"; import { ViewPropertyEditorsComponent } from './virtualization/view-editor/view-property-editors/view-property-editors.component'; import { AddCompositionWizardComponent } from './virtualization/view-editor/add-composition-wizard/add-composition-wizard.component'; +import { CreateViewDialogComponent } from './virtualization/view-editor/create-view-dialog/create-view-dialog.component'; +import { CreateVirtualizationDialogComponent } from './create-virtualization-dialog/create-virtualization-dialog.component'; @NgModule({ imports: [ @@ -118,9 +117,6 @@ import { AddCompositionWizardComponent } from './virtualization/view-editor/add- ViewPreviewComponent, ViewEditorHeaderComponent, ViewCanvasComponent, - VirtualizationComponent, - ViewCardsComponent, - ViewCardComponent, MessageLogComponent, EditorViewsComponent, ConnectionTreeSelectorComponent, @@ -129,7 +125,9 @@ import { AddCompositionWizardComponent } from './virtualization/view-editor/add- GraphVisualComponent, NodeVisualComponent, LinkVisualComponent, - AddCompositionWizardComponent + AddCompositionWizardComponent, + CreateViewDialogComponent, + CreateVirtualizationDialogComponent ], providers: [ { @@ -151,7 +149,8 @@ import { AddCompositionWizardComponent } from './virtualization/view-editor/add- ], exports: [ ], - entryComponents: [AddCompositionWizardComponent, ConfirmDialogComponent, ConnectionTableDialogComponent, ProgressDialogComponent] + entryComponents: [AddCompositionWizardComponent, ConfirmDialogComponent, ConnectionTableDialogComponent, + CreateViewDialogComponent, CreateVirtualizationDialogComponent, ProgressDialogComponent] }) export class DataservicesModule { } diff --git a/ngapp/src/app/dataservices/shared/composition.model.ts b/ngapp/src/app/dataservices/shared/composition.model.ts index 442dabc6..373b8f01 100644 --- a/ngapp/src/app/dataservices/shared/composition.model.ts +++ b/ngapp/src/app/dataservices/shared/composition.model.ts @@ -205,6 +205,24 @@ export class Composition { } } + /** + * Determine if the supplied Composition is equal to this + * @param {Object} values + */ + public isEqual( otherComp: Composition ): boolean { + let equal = false; + if (this.getName() === otherComp.getName() && + this.getLeftSourcePath() === otherComp.getLeftSourcePath() && + this.getRightSourcePath() === otherComp.getRightSourcePath() && + this.getLeftCriteriaColumn() === otherComp.getLeftCriteriaColumn() && + this.getRightCriteriaColumn() === otherComp.getRightCriteriaColumn() && + this.getType() === otherComp.getType() && + this.getOperator() === otherComp.getOperator() ) { + equal = true; + } + return equal; + } + /** * Set all object values using the supplied View json * @param {Object} values diff --git a/ngapp/src/app/dataservices/shared/dataservice.model.ts b/ngapp/src/app/dataservices/shared/dataservice.model.ts index 796b41c1..73b53057 100644 --- a/ngapp/src/app/dataservices/shared/dataservice.model.ts +++ b/ngapp/src/app/dataservices/shared/dataservice.model.ts @@ -105,7 +105,10 @@ export class Dataservice implements Identifiable< string > { * @returns {string} the dataservice Vdb name (can be null) */ public getServiceVdbName(): string { - return this.serviceVdbName; + if (this.serviceVdbName && this.serviceVdbName !== null) { + return this.serviceVdbName; + } + return ""; } /** @@ -126,6 +129,19 @@ export class Dataservice implements Identifiable< string > { return []; } + /** + * Remove the specified view name, if it exists + * @param {string} viewNameToRemove the view name to remove + */ + public removeServiceViewName( viewNameToRemove: string ): void { + const index = this.serviceViewDefinitions.findIndex( ( viewName ) => + viewName === viewNameToRemove ); + + if ( index !== -1 ) { + this.serviceViewDefinitions.splice( index, 1 ); + } + } + /** * @returns {string} the dataservice view model name (can be null) */ diff --git a/ngapp/src/app/dataservices/shared/dataservice.service.ts b/ngapp/src/app/dataservices/shared/dataservice.service.ts index d98d3497..1d14e149 100644 --- a/ngapp/src/app/dataservices/shared/dataservice.service.ts +++ b/ngapp/src/app/dataservices/shared/dataservice.service.ts @@ -670,8 +670,7 @@ export class DataserviceService extends ApiService { return this.http.get(environment.viewEditorState + "/" + editorId, this.getAuthRequestOptions() ) .map( ( response ) => { const editorState = response.json(); - const viewEditorState = ViewEditorState.create(editorState); - return Observable.of( viewEditorState ); + return ViewEditorState.create(editorState); } ) .catch( ( error ) => { // no editor state found diff --git a/ngapp/src/app/dataservices/shared/dataservices-constants.ts b/ngapp/src/app/dataservices/shared/dataservices-constants.ts index 24b480c8..d9216686 100644 --- a/ngapp/src/app/dataservices/shared/dataservices-constants.ts +++ b/ngapp/src/app/dataservices/shared/dataservices-constants.ts @@ -24,14 +24,8 @@ export class DataservicesConstants { public static readonly dataserviceRestPath = "/dataservice"; public static readonly dataservicesRestPath = "/dataservices"; - public static readonly addDataserviceRoute = DataservicesConstants.dataservicesRootRoute + "/add-virtualization"; - public static readonly addDataservicePath = DataservicesConstants.dataservicesRootPath + "/add-virtualization"; - - public static readonly virtualizationRoute = DataservicesConstants.dataservicesRootRoute + "/virtualization"; - public static readonly virtualizationPath = DataservicesConstants.dataservicesRootPath + "/virtualization"; - - public static readonly viewRoute = DataservicesConstants.virtualizationRoute + "/view"; - public static readonly viewPath = DataservicesConstants.virtualizationPath + "/view"; + public static readonly viewRoute = DataservicesConstants.dataservicesRootRoute + "/view"; + public static readonly viewPath = DataservicesConstants.dataservicesRootPath + "/view"; public static readonly testDataserviceRoute = DataservicesConstants.dataservicesRootRoute + "/test-virtualization"; public static readonly testDataservicePath = DataservicesConstants.dataservicesRootPath + "/test-virtualization"; diff --git a/ngapp/src/app/dataservices/shared/mock-dataservice.service.ts b/ngapp/src/app/dataservices/shared/mock-dataservice.service.ts index 4fea7724..4ae48741 100644 --- a/ngapp/src/app/dataservices/shared/mock-dataservice.service.ts +++ b/ngapp/src/app/dataservices/shared/mock-dataservice.service.ts @@ -42,6 +42,7 @@ export class MockDataserviceService extends DataserviceService { private readonly services: Dataservice[]; private readonly queryResults: QueryResults; private editorViewStateMap = new Map(); + private selectedDs: Dataservice; constructor(http: Http, vdbService: VdbService, appSettings: AppSettingsService, notifierService: NotifierService, logger: LoggerService ) { @@ -56,6 +57,12 @@ export class MockDataserviceService extends DataserviceService { this.queryResults = testDataService.getQueryResults(); this.editorViewStateMap = testDataService.getViewEditorStateMap(); + + // set selected dataservice, so it's not empty + const ds = new Dataservice(); + ds.setId("testDs"); + ds.setServiceVdbName("testDsVdb"); + this.setSelectedDataservice(ds); } /** @@ -94,6 +101,22 @@ export class MockDataserviceService extends DataserviceService { return Observable.of( true ); } + /** + * Set the current Dataservice selection + * @param {Dataservice} service the Dataservice + */ + public setSelectedDataservice(service: Dataservice): void { + this.selectedDs = service; + } + + /** + * Get the current Dataservice selection + * @returns {Dataservice} the selected Dataservice + */ + public getSelectedDataservice( ): Dataservice { + return this.selectedDs; + } + /** * Get the view definitions for the selected Dataservice * @returns {ViewDefinition[]} the view definitions @@ -228,4 +251,13 @@ export class MockDataserviceService extends DataserviceService { return Observable.of(true); } + /** + * @param {string} editorId the ID of the editor state being deleted + * @param {string} dataserviceName the name of the dataservice + * @returns {Observable} `true` if the editor state was successfully saved + */ + public deleteViewEditorStateRefreshViews( editorId: string, dataserviceName: string ): Observable< boolean > { + return Observable.of(true); + } + } diff --git a/ngapp/src/app/dataservices/shared/mock-vdb.service.ts b/ngapp/src/app/dataservices/shared/mock-vdb.service.ts index 79192380..679e5fb2 100644 --- a/ngapp/src/app/dataservices/shared/mock-vdb.service.ts +++ b/ngapp/src/app/dataservices/shared/mock-vdb.service.ts @@ -178,4 +178,39 @@ export class MockVdbService extends VdbService { return Observable.of(true); } + /** + * Validates the specified view name within the specified vdb model. If the name contains valid characters + * and the name is unique, the service returns 'null'. Otherwise, a 'string' containing an error message is returned. + * + * @param {string} vdbName the vdb name + * @param {string} modelName the model name + * @param {string} viewName the view name + * @returns {Observable} + */ + public isValidViewName( vdbName: string, modelName: string, viewName: string ): Observable< string > { + // Check that valid names were supplied + // if ( !vdbName || vdbName.length === 0 ) { + // return Observable.of( "VDB name cannot be empty" ); + // } + if ( !modelName || modelName.length === 0 ) { + return Observable.of( "Model name cannot be empty" ); + } + if ( !viewName || viewName.length === 0 ) { + return Observable.of( "View name cannot be empty" ); + } + + // just implement a case where no special characters allowed + for ( let i = 0; i < viewName.length; i++ ) { + const c = viewName.charAt( i ); + + // special characters have the same upper and lower case values + if ( c.toUpperCase() === c.toLowerCase() ) { + return Observable.of( "No special characters allowed" ); + } + } + + // valid + return Observable.of( "" ); + } + } diff --git a/ngapp/src/app/dataservices/shared/vdb.service.ts b/ngapp/src/app/dataservices/shared/vdb.service.ts index 7c1f2a43..c8fc0aec 100644 --- a/ngapp/src/app/dataservices/shared/vdb.service.ts +++ b/ngapp/src/app/dataservices/shared/vdb.service.ts @@ -16,7 +16,7 @@ */ import { Injectable } from "@angular/core"; -import { Http, RequestOptions } from "@angular/http"; +import { Http } from "@angular/http"; import { ApiService } from "@core/api.service"; import { AppSettingsService } from "@core/app-settings.service"; import { LoggerService } from "@core/logger.service"; @@ -31,7 +31,6 @@ import { environment } from "@environments/environment"; import { Observable } from "rxjs/Rx"; import { Subscription } from "rxjs/Subscription"; import { QueryResults } from "@dataservices/shared/query-results.model"; -import { ViewEditorState } from "@dataservices/shared/view-editor-state.model"; @Injectable() /** @@ -158,7 +157,8 @@ export class VdbService extends ApiService { return Observable.of( "View name cannot be empty" ); } - const url = environment.komodoWorkspaceUrl + "/vdbs/" + vdbName + "/Models/" + modelName + "Views/nameValidation/" + encodeURIComponent( name ); + const url = environment.komodoWorkspaceUrl + "/vdbs/" + vdbName + "/Models/" + modelName + + "/Views/nameValidation/" + encodeURIComponent( viewName ); return this.http.get( url, this.getAuthRequestOptions() ) .map( ( response ) => { diff --git a/ngapp/src/app/dataservices/shared/view-definition.model.ts b/ngapp/src/app/dataservices/shared/view-definition.model.ts index 0a179915..270a1a61 100644 --- a/ngapp/src/app/dataservices/shared/view-definition.model.ts +++ b/ngapp/src/app/dataservices/shared/view-definition.model.ts @@ -26,10 +26,11 @@ import { CompositionType } from "@dataservices/shared/composition-type.enum"; */ export class ViewDefinition { private viewName: string; - private keng__description: string; + private keng__description = ""; private isEditable = false; private sourcePaths: string[] = []; private compositions: Composition[] = []; + private isSelected = false; /** * @param {Object} json the JSON representation of a ViewDefinition @@ -92,7 +93,7 @@ export class ViewDefinition { * @param {string} description the view description */ public setDescription( description?: string ): void { - this.keng__description = description ? description : null; + this.keng__description = description ? description : ""; } /** @@ -330,6 +331,61 @@ export class ViewDefinition { return connectionName.toLowerCase() + VdbsConstants.SCHEMA_MODEL_SUFFIX + "." + sourceNodeName; } + /** + * @returns {boolean} 'true' if ViewDefinition isSelected + */ + public get selected(): boolean { + return this.isSelected; + } + + /** + * @param {boolean} selected the ViewDefinition isSelected state + */ + public setSelected( selected: boolean ): void { + this.isSelected = selected; + } + + /** + * Determine if the supplied ViewDefinition is equal to this + * @param {Object} values + */ + public isEqual( otherView: ViewDefinition ): boolean { + let equal = false; + if (this.getName() === otherView.getName() && + this.getDescription() === otherView.getDescription() && + this.pathsEqual(this.getSourcePaths(), otherView.getSourcePaths()) && + this.compositionsEqual(this.getCompositions(), otherView.getCompositions()) ) { + equal = true; + } + return equal; + } + + private pathsEqual(left: string[], right: string[]): boolean { + if (left === right) return true; + if (left == null || right == null) return false; + if (left.length !== right.length) return false; + + left.sort(); + right.sort(); + for (let i = 0; i < right.length; ++i) { + if (left[i] !== right[i]) return false; + } + return true; + } + + private compositionsEqual(left: Composition[], right: Composition[]): boolean { + if (left === right) return true; + if (left == null || right == null) return false; + if (left.length !== right.length) return false; + + left.sort(); + right.sort(); + for (let i = 0; i < right.length; ++i) { + if (!left[i].isEqual(right[i])) return false; + } + return true; + } + /** * Set all object values using the supplied ViewDefinition json * @param {Object} values diff --git a/ngapp/src/app/dataservices/virtualization/view-cards/view-card/view-card.component.css b/ngapp/src/app/dataservices/virtualization/view-cards/view-card/view-card.component.css deleted file mode 100644 index fe51ba96..00000000 --- a/ngapp/src/app/dataservices/virtualization/view-cards/view-card/view-card.component.css +++ /dev/null @@ -1,4 +0,0 @@ -.sources-indent { - margin-top: 7px; - margin-left: 10px; -} diff --git a/ngapp/src/app/dataservices/virtualization/view-cards/view-card/view-card.component.html b/ngapp/src/app/dataservices/virtualization/view-cards/view-card/view-card.component.html deleted file mode 100644 index f6285a38..00000000 --- a/ngapp/src/app/dataservices/virtualization/view-cards/view-card/view-card.component.html +++ /dev/null @@ -1,60 +0,0 @@ - - -
-
- - -
- - - - - - - - -
- -
-
- - {{ view.getDescription() }} - -
-
  {{sourceStr}}
-
-
-
[No sources defined]
-
-
-
-
-
diff --git a/ngapp/src/app/dataservices/virtualization/view-cards/view-card/view-card.component.spec.ts b/ngapp/src/app/dataservices/virtualization/view-cards/view-card/view-card.component.spec.ts deleted file mode 100644 index 5ff8692d..00000000 --- a/ngapp/src/app/dataservices/virtualization/view-cards/view-card/view-card.component.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ViewCardComponent } from './view-card.component'; -import { - ActionModule, - CardModule, - EmptyStateModule, - FilterModule, - ListModule, - NotificationModule, - SortModule, - TableModule, - WizardModule } from "patternfly-ng"; -import { RouterTestingModule } from "@angular/router/testing"; -import { LoggerService } from "@core/logger.service"; -import { ViewDefinition } from "@dataservices/shared/view-definition.model"; - -describe('ViewCardComponent', () => { - let component: ViewCardComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ ViewCardComponent ], - imports: [ - ActionModule, - CardModule, - EmptyStateModule, - FilterModule, - ListModule, - NotificationModule, - SortModule, - TableModule, - WizardModule, - RouterTestingModule - ], - providers: [ - LoggerService - ] - }) - .compileComponents().then(() => { - // nothing to do - }); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(ViewCardComponent); - component = fixture.componentInstance; - - component.view = new ViewDefinition(); - fixture.detectChanges(); - }); - - it('should be created', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/ngapp/src/app/dataservices/virtualization/view-cards/view-card/view-card.component.ts b/ngapp/src/app/dataservices/virtualization/view-cards/view-card/view-card.component.ts deleted file mode 100644 index b706690a..00000000 --- a/ngapp/src/app/dataservices/virtualization/view-cards/view-card/view-card.component.ts +++ /dev/null @@ -1,144 +0,0 @@ -/** - * @license - * Copyright 2017 JBoss Inc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Component, DoCheck, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from "@angular/core"; -import { Action, ActionConfig, CardAction, CardConfig } from "patternfly-ng"; -import { LoggerService } from "@core/logger.service"; -import { PathUtils } from "@dataservices/shared/path-utils"; -import { ViewDefinition } from "@dataservices/shared/view-definition.model"; - -@Component({ - encapsulation: ViewEncapsulation.None, - selector: "app-view-card", - templateUrl: "./view-card.component.html", - styleUrls: ["./view-card.component.css"] -}) -export class ViewCardComponent implements DoCheck, OnInit { - - public static readonly deleteViewEvent = "delete"; - public static readonly editViewEvent = "edit"; - - public readonly editEvent = ViewCardComponent.editViewEvent; - - @Input() public view: ViewDefinition; - @Input() public selectedViews: ViewDefinition[] = []; - @Output() public cardEvent: EventEmitter< {} > = new EventEmitter< {} >(); - @Output() public selectEvent: EventEmitter< ViewDefinition > = new EventEmitter< ViewDefinition >(); - - public actionConfig: ActionConfig; - public cardConfig: CardConfig; - public showDetails = false; - - private readonly deleteActionId = "delete"; - private readonly deleteActionIndex = 0; // index in moreActions - - private isLoading = false; - private logger: LoggerService; - - constructor( logger: LoggerService ) { - this.logger = logger; - } - - private get detailsIconStyle(): string { - return this.showDetails ? "fa fa-close card-footer-action-icon" : "fa fa-angle-right card-footer-action-icon"; - } - - /** - * Event handler for when a toolbar kebab action is clicked. - * @param {Action} action the action that was selected. - */ - public handleAction( action: Action ): void { - if ( action.id === this.deleteActionId ) { - this.onClick( ViewCardComponent.deleteViewEvent ); - } else { - this.logger.error( "Action '" + action.id + "' not handled." ); - } - } - - /** - * @returns {boolean} `true` if the connection represented by this card is selected - */ - public isSelected(): boolean { - return this.selectedViews.indexOf( this.view ) !== -1; - } - - public ngDoCheck(): void { - this.actionConfig.moreActions[ this.deleteActionIndex ].disabled = this.isLoading; - // this.cardConfig.action.iconStyleClass = this.detailsIconStyle; - } - - /** - * Initializes the ActionConfig, CardConfig, and ListConfig. - */ - public ngOnInit(): void { - this.actionConfig = { - primaryActions: [ - ], - moreActions: [ - { - id: this.deleteActionId, - disabled: !this.view.editable, - title: "Delete", - tooltip: "Delete" - } - ] - } as ActionConfig; - - this.cardConfig = { - // action: { - // id: "showDetails", - // hypertext: this.showDetailsTitle, - // iconStyleClass: this.detailsIconStyle - // }, - titleBorder: true, - noPadding: true, - topBorder: false - } as CardConfig; - } - - /** - * An event handler for when a toolbar action is invoked. - * @param {string} type the type of event being processed - */ - public onClick( type: string ): void { - this.cardEvent.emit( { eventType: type, viewName: this.view.getName() } ); - } - - /** - * An event handler for when the card is clicked. - */ - public onSelect(): void { - this.selectEvent.emit( this.view ); - } - - /** - * Returns display text array for the selected view sources - * @returns {string} - */ - public get sourceTablesText(): string[] { - const sourceTextArray: string[] = []; - const sourcePaths = this.view.getSourcePaths(); - if (sourcePaths && sourcePaths.length > 0) { - for (const path of sourcePaths) { - const sourceText = "[" + PathUtils.getConnectionName(path) + "]: " + PathUtils.getSourceName(path); - sourceTextArray.push(sourceText); - } - } - return sourceTextArray; - } - -} diff --git a/ngapp/src/app/dataservices/virtualization/view-cards/view-cards.component.css b/ngapp/src/app/dataservices/virtualization/view-cards/view-cards.component.css deleted file mode 100644 index 89f54e0d..00000000 --- a/ngapp/src/app/dataservices/virtualization/view-cards/view-cards.component.css +++ /dev/null @@ -1,4 +0,0 @@ -.views-container { - height: 90vh; - overflow: auto; -} diff --git a/ngapp/src/app/dataservices/virtualization/view-cards/view-cards.component.html b/ngapp/src/app/dataservices/virtualization/view-cards/view-cards.component.html deleted file mode 100644 index c49d8f19..00000000 --- a/ngapp/src/app/dataservices/virtualization/view-cards/view-cards.component.html +++ /dev/null @@ -1,8 +0,0 @@ -
-
- -
-
diff --git a/ngapp/src/app/dataservices/virtualization/view-cards/view-cards.component.spec.ts b/ngapp/src/app/dataservices/virtualization/view-cards/view-cards.component.spec.ts deleted file mode 100644 index d5480bc7..00000000 --- a/ngapp/src/app/dataservices/virtualization/view-cards/view-cards.component.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from "@angular/router/testing"; -import { LoggerService } from "@core/logger.service"; -import { ViewCardsComponent } from "@dataservices/virtualization/view-cards/view-cards.component"; -import { ViewCardComponent } from "@dataservices/virtualization/view-cards/view-card/view-card.component"; -import { - ActionModule, - CardModule, - EmptyStateModule, - FilterModule, - ListModule, - NotificationModule, - SortModule, - TableModule, - WizardModule } from "patternfly-ng"; - -describe('ViewCardsComponent', () => { - let component: ViewCardsComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ - ViewCardsComponent, - ViewCardComponent - ], - imports: [ - ActionModule, - CardModule, - EmptyStateModule, - FilterModule, - ListModule, - NotificationModule, - SortModule, - TableModule, - WizardModule, - RouterTestingModule - ], - providers: [ - LoggerService - ] - }) - .compileComponents().then(() => { - // nothing to do - }); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(ViewCardsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should be created', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/ngapp/src/app/dataservices/virtualization/view-cards/view-cards.component.ts b/ngapp/src/app/dataservices/virtualization/view-cards/view-cards.component.ts deleted file mode 100644 index 97aa2ca3..00000000 --- a/ngapp/src/app/dataservices/virtualization/view-cards/view-cards.component.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * @license - * Copyright 2017 JBoss Inc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { ViewCardComponent } from "@dataservices/virtualization/view-cards/view-card/view-card.component"; -import { LoggerService } from "@core/logger.service"; -import { ViewDefinition } from "@dataservices/shared/view-definition.model"; - -@Component({ - moduleId: module.id, - selector: "app-view-cards", - templateUrl: "view-cards.component.html", - styleUrls: ["view-cards.component.css"] -}) -export class ViewCardsComponent { - - @Input() public views: ViewDefinition[]; - @Input() public selectedViews: ViewDefinition[]; - - @Output() public viewSelected: EventEmitter = new EventEmitter(); - @Output() public viewDeselected: EventEmitter = new EventEmitter(); - @Output() public deleteView: EventEmitter = new EventEmitter(); - @Output() public editView: EventEmitter = new EventEmitter(); - - public logger: LoggerService; - - /** - * @param {LoggerService} logger the logging service - */ - constructor( logger: LoggerService ) { - this.logger = logger; - } - - public isSelected( view: ViewDefinition ): boolean { - return this.selectedViews.indexOf( view ) !== -1; - } - - public onCardEvent( event: { eventType: string, - viewName: string } ): void { - switch ( event.eventType ) { - case ViewCardComponent.deleteViewEvent: - this.deleteView.emit( event.viewName ); - break; - case ViewCardComponent.editViewEvent: - this.editView.emit( event.viewName ); - break; - default: - this.logger.error( "Unhandled event type of '" + event.eventType + "'" ); - break; - } - } - - public onSelectEvent( view: ViewDefinition ): void { - this.viewSelected.emit( view ); - } - -} diff --git a/ngapp/src/app/dataservices/virtualization/view-editor/add-composition-wizard/add-composition-wizard.component.ts b/ngapp/src/app/dataservices/virtualization/view-editor/add-composition-wizard/add-composition-wizard.component.ts index 8d27c27e..b4327c2d 100644 --- a/ngapp/src/app/dataservices/virtualization/view-editor/add-composition-wizard/add-composition-wizard.component.ts +++ b/ngapp/src/app/dataservices/virtualization/view-editor/add-composition-wizard/add-composition-wizard.component.ts @@ -66,7 +66,9 @@ export class AddCompositionWizardComponent implements OnInit { public readonly currentSelectionMsg = ViewEditorI18n.currentSelection; public selectedCompositionType: CompositionType = CompositionType.INNER_JOIN; public selectedCompositionCondition: CompositionOperator = CompositionOperator.EQ; - public swapButtonText = ViewEditorI18n.addCompositionWizardSwapButtonText; + public readonly columnLoadFailedHeader = "Loading Failed: "; + public readonly columnLoadFailedMsg = "Columns failed to load!"; + public readonly columnLoadFailedType = NotificationType.DANGER; private selectedTreeNodePath: string; @@ -76,9 +78,6 @@ export class AddCompositionWizardComponent implements OnInit { private readonly connectionService: ConnectionService; private composition: Composition; private columnsMap = new Map(); // Maintain loaded columns map to reduce rest calls - private readonly columnLoadFailedHeader = "Loading Failed: "; - private readonly columnLoadFailedMsg = "Columns failed to load!"; - private readonly columnLoadFailedType = NotificationType.DANGER; constructor( connectionService: ConnectionService, logger: LoggerService ) { this.connectionService = connectionService; diff --git a/ngapp/src/app/dataservices/virtualization/view-editor/create-view-dialog/create-view-dialog.component.css b/ngapp/src/app/dataservices/virtualization/view-editor/create-view-dialog/create-view-dialog.component.css new file mode 100644 index 00000000..e69de29b diff --git a/ngapp/src/app/dataservices/virtualization/view-editor/create-view-dialog/create-view-dialog.component.html b/ngapp/src/app/dataservices/virtualization/view-editor/create-view-dialog/create-view-dialog.component.html new file mode 100644 index 00000000..263ccd76 --- /dev/null +++ b/ngapp/src/app/dataservices/virtualization/view-editor/create-view-dialog/create-view-dialog.component.html @@ -0,0 +1,30 @@ + + + diff --git a/ngapp/src/app/dataservices/virtualization/view-editor/create-view-dialog/create-view-dialog.component.spec.ts b/ngapp/src/app/dataservices/virtualization/view-editor/create-view-dialog/create-view-dialog.component.spec.ts new file mode 100644 index 00000000..c5097ce6 --- /dev/null +++ b/ngapp/src/app/dataservices/virtualization/view-editor/create-view-dialog/create-view-dialog.component.spec.ts @@ -0,0 +1,51 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CreateViewDialogComponent } from './create-view-dialog.component'; +import { HttpModule } from "@angular/http"; +import { BsModalRef, ModalModule } from "ngx-bootstrap"; +import { + ActionModule, + NotificationModule +} from "patternfly-ng"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { DataserviceService } from "@dataservices/shared/dataservice.service"; +import { MockDataserviceService } from "@dataservices/shared/mock-dataservice.service"; +import { VdbService } from "@dataservices/shared/vdb.service"; +import { MockVdbService } from "@dataservices/shared/mock-vdb.service"; +import { AppSettingsService } from "@core/app-settings.service"; +import { LoggerService } from "@core/logger.service"; +import { NotifierService } from "@dataservices/shared/notifier.service"; + +describe('CreateViewDialogComponent', () => { + let component: CreateViewDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpModule, + FormsModule, + ReactiveFormsModule, + ModalModule.forRoot(), + ActionModule, + NotificationModule + ], + declarations: [ CreateViewDialogComponent ], + providers: [ AppSettingsService, BsModalRef, LoggerService, NotifierService, + { provide: DataserviceService, useClass: MockDataserviceService }, + { provide: VdbService, useClass: MockVdbService } + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CreateViewDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ngapp/src/app/dataservices/virtualization/view-editor/create-view-dialog/create-view-dialog.component.ts b/ngapp/src/app/dataservices/virtualization/view-editor/create-view-dialog/create-view-dialog.component.ts new file mode 100644 index 00000000..a51eec6f --- /dev/null +++ b/ngapp/src/app/dataservices/virtualization/view-editor/create-view-dialog/create-view-dialog.component.ts @@ -0,0 +1,164 @@ +/** + * @license + * Copyright 2017 JBoss Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, OnInit } from "@angular/core"; +import { Output } from "@angular/core"; +import { EventEmitter } from "@angular/core"; +import { BsModalRef } from "ngx-bootstrap"; +import { LoggerService } from "@core/logger.service"; +import { ViewEditorI18n } from "@dataservices/virtualization/view-editor/view-editor-i18n"; +import { AbstractControl, FormControl, FormGroup } from "@angular/forms"; +import { VdbService } from "@dataservices/shared/vdb.service"; +import { DataserviceService } from "@dataservices/shared/dataservice.service"; +import { ViewDefinition } from "@dataservices/shared/view-definition.model"; + +@Component({ + selector: "app-create-view-dialog", + templateUrl: "./create-view-dialog.component.html", + styleUrls: ["./create-view-dialog.component.css"] +}) +/** + * CreateView Dialog. Invoke this from another component as follows: + * + * this.modalRef = this.modalService.show(CreateViewDialogComponent, {initialState}); + * this.modalRef.content.okAction.take(1).subscribe((selectedNodes) => { + * // do something with array of selected nodes - selectedNodes - SchemaNode[] + * }); + * + * The expected initial state is as follows: + * const initialState = { + * title: "The dialog title", + * cancelButtonText: "Text for cancel button", + * confirmButtonText: "Text for confirm button" + * }; + */ +export class CreateViewDialogComponent implements OnInit { + + @Output() public okAction: EventEmitter = new EventEmitter(); + + public readonly title = ViewEditorI18n.createViewDialogTitle; + public readonly message = ViewEditorI18n.createViewDialogMessage; + public readonly cancelButtonText = ViewEditorI18n.cancelButtonText; + public readonly okButtonText = ViewEditorI18n.okButtonText; + public okButtonEnabled = false; + public bsModalRef: BsModalRef; + public nameValidationError = ""; + public viewPropertyForm: FormGroup; + + private loggerService: LoggerService; + private dataserviceService: DataserviceService; + private vdbService: VdbService; + private serviceVdbName = ""; + + constructor(bsModalRef: BsModalRef, logger: LoggerService, + dataserviceService: DataserviceService, vdbService: VdbService) { + this.bsModalRef = bsModalRef; + this.loggerService = logger; + this.dataserviceService = dataserviceService; + const dService = this.dataserviceService.getSelectedDataservice(); + if ( dService && dService !== null ) { + this.serviceVdbName = dService.getServiceVdbName(); + } + this.vdbService = vdbService; + this.createViewPropertyForm(); + } + + public ngOnInit(): void { + this.viewPropertyForm.controls["name"].setValue(""); + this.viewPropertyForm.controls["description"].setValue(""); + } + + /* + * Creates the view property form + */ + private createViewPropertyForm(): void { + this.viewPropertyForm = new FormGroup({ + name: new FormControl( "", this.handleNameChanged.bind( this ) ), + description: new FormControl("") + }); + // Responds to basic property changes - updates the page status + this.viewPropertyForm.valueChanges.subscribe((val) => { + // this.updatePage2aValidStatus( ); + }); + } + + /** + * Handler for view name changes. + * @param {AbstractControl} input + */ + public handleNameChanged( input: AbstractControl ): void { + const self = this; + + this.vdbService.isValidViewName( this.serviceVdbName, "views", input.value ).subscribe( + ( errorMsg ) => { + if ( errorMsg ) { + // only update if error has changed + if ( errorMsg !== self.nameValidationError ) { + self.nameValidationError = errorMsg; + } + } else { // name is valid + self.nameValidationError = ""; + } + self.setOkButtonEnablement(); + }, + ( error ) => { + self.loggerService.error( "[handleNameChanged] Error: %o", error ); + self.nameValidationError = "Error validating view name"; + self.setOkButtonEnablement(); + } ); + } + + /** + * OK selected. Emit ViewDefinition with the view, then modal is closed + */ + public onOkSelected(): void { + const theName = this.viewPropertyForm.controls["name"].value; + const theDescr = this.viewPropertyForm.controls["description"].value; + + const viewDefn = new ViewDefinition(); + viewDefn.setName(theName); + viewDefn.setDescription(theDescr); + this.okAction.emit(viewDefn); + this.bsModalRef.hide(); + } + + /** + * Cancel selected. The modal is closed. + */ + public onCancelSelected(): void { + this.bsModalRef.hide(); + } + + /* + * Return the name valid state + */ + public get nameValid(): boolean { + return this.nameValidationError == null || this.nameValidationError.length === 0; + } + + /** + * Sets the OK button enablement, based upon the selections + */ + private setOkButtonEnablement(): void { + if (this.nameValid) { + this.okButtonEnabled = true; + } else { + this.okButtonEnabled = false; + } + } + +} diff --git a/ngapp/src/app/dataservices/virtualization/view-editor/view-canvas/canvas.service.ts b/ngapp/src/app/dataservices/virtualization/view-editor/view-canvas/canvas.service.ts index 46b288eb..423a742f 100644 --- a/ngapp/src/app/dataservices/virtualization/view-editor/view-canvas/canvas.service.ts +++ b/ngapp/src/app/dataservices/virtualization/view-editor/view-canvas/canvas.service.ts @@ -163,6 +163,10 @@ export class CanvasService { return this.canvasGraph.links; } + public clear(): void { + this.canvasGraph.clear(); + } + /** * Makes sure both the canvas graph and the * view are up to date and all events have diff --git a/ngapp/src/app/dataservices/virtualization/view-editor/view-canvas/models/canvas-graph.ts b/ngapp/src/app/dataservices/virtualization/view-editor/view-canvas/models/canvas-graph.ts index 6710a87c..db9058d8 100644 --- a/ngapp/src/app/dataservices/virtualization/view-editor/view-canvas/models/canvas-graph.ts +++ b/ngapp/src/app/dataservices/virtualization/view-editor/view-canvas/models/canvas-graph.ts @@ -306,6 +306,12 @@ export class CanvasGraph { this.canvasService.update(true); } + public clear(): void { + this._nodes = []; + this._links = []; + this.layout = null; + } + /** * Refreshes / restarts / updates the layout */ diff --git a/ngapp/src/app/dataservices/virtualization/view-editor/view-canvas/view-canvas.component.ts b/ngapp/src/app/dataservices/virtualization/view-editor/view-canvas/view-canvas.component.ts index 5ee83839..25135173 100644 --- a/ngapp/src/app/dataservices/virtualization/view-editor/view-canvas/view-canvas.component.ts +++ b/ngapp/src/app/dataservices/virtualization/view-editor/view-canvas/view-canvas.component.ts @@ -65,6 +65,7 @@ export class ViewCanvasComponent implements OnInit, AfterViewInit, OnDestroy { private editorSubscription: Subscription; private canvasSubscription: Subscription; + private viewNameSaveInProgress: string; constructor( editorService: ViewEditorService, logger: LoggerService, @@ -105,23 +106,26 @@ export class ViewCanvasComponent implements OnInit, AfterViewInit, OnDestroy { this.initCanvas(viewDefn); } else if ( event.typeIsEditorViewSaveProgressChanged() ) { if ( event.args.length !== 0 ) { - const viewName = this.editorService.getEditorView().getName(); - // Detect changes in view editor save progress if ( event.args[ 0 ] === ViewEditorProgressChangeId.IN_PROGRESS ) { + this.viewNameSaveInProgress = this.editorService.getEditorView().getName(); this.saveViewNotificationHeader = this.viewSaveInProgressHeader; - this.saveViewNotificationMessage = "Saving View '" + viewName + "'..."; + this.saveViewNotificationMessage = this.getViewNotificationMessage(ViewEditorProgressChangeId.IN_PROGRESS, + this.viewNameSaveInProgress); this.saveViewNotificationType = NotificationType.INFO; this.saveViewNotificationVisible = true; } else if ( event.args[ 0 ] === ViewEditorProgressChangeId.COMPLETED_SUCCESS ) { this.saveViewNotificationHeader = this.viewSaveSuccessHeader; - this.saveViewNotificationMessage = "View '" + viewName + "' save successful"; + this.saveViewNotificationMessage = this.getViewNotificationMessage(ViewEditorProgressChangeId.COMPLETED_SUCCESS, + this.viewNameSaveInProgress); + this.saveViewNotificationType = NotificationType.SUCCESS; // After 8 seconds, the notification is dismissed setTimeout(() => this.saveViewNotificationVisible = false, 8000); } else if ( event.args[ 0 ] === ViewEditorProgressChangeId.COMPLETED_FAILED ) { this.saveViewNotificationHeader = this.viewSaveFailedHeader; - this.saveViewNotificationMessage = "View '" + viewName + "' save failed"; + this.saveViewNotificationMessage = this.getViewNotificationMessage(ViewEditorProgressChangeId.COMPLETED_FAILED, + this.viewNameSaveInProgress); this.saveViewNotificationType = NotificationType.DANGER; // After 8 seconds, the notification is dismissed setTimeout(() => this.saveViewNotificationVisible = false, 8000); @@ -136,6 +140,24 @@ export class ViewCanvasComponent implements OnInit, AfterViewInit, OnDestroy { } } + /** + * Generates a view notification message based on the progress type and viewName + * @param {ViewEditorProgressChangeId} viewProgressId the progress type + * @param {string} viewName the view name + */ + private getViewNotificationMessage(viewProgressId: ViewEditorProgressChangeId, viewName: string): string { + let msg = ""; + const viewStr = ( viewName && viewName !== null ) ? "'" + viewName + "'" : ""; + if ( viewProgressId === ViewEditorProgressChangeId.IN_PROGRESS ) { + msg = "View save in progress for " + viewStr; + } else if ( viewProgressId === ViewEditorProgressChangeId.COMPLETED_SUCCESS ) { + msg = "View save SUCCESS for " + viewStr; + } else if ( viewProgressId === ViewEditorProgressChangeId.COMPLETED_FAILED ) { + msg = "View save FAILED for " + viewStr; + } + return msg; + } + private handleCanvasEvent(event: ViewCanvasEvent): void { switch (event.type) { case ViewCanvasEventType.CREATE_SOURCE: @@ -238,6 +260,9 @@ export class ViewCanvasComponent implements OnInit, AfterViewInit, OnDestroy { * @param {ViewDefinition} viewDefn the ViewDefinition */ private initCanvas( viewDefn: ViewDefinition ): void { + // Make sure canvas is cleared + this.canvasService.clear(); + if (viewDefn && viewDefn !== null) { // ------------------------ // Create the source nodes @@ -265,6 +290,8 @@ export class ViewCanvasComponent implements OnInit, AfterViewInit, OnDestroy { } } } + + this.canvasService.update(true); } /** diff --git a/ngapp/src/app/dataservices/virtualization/view-editor/view-editor-header/view-editor-header.component.css b/ngapp/src/app/dataservices/virtualization/view-editor/view-editor-header/view-editor-header.component.css index cb288f82..828d08cd 100644 --- a/ngapp/src/app/dataservices/virtualization/view-editor/view-editor-header/view-editor-header.component.css +++ b/ngapp/src/app/dataservices/virtualization/view-editor/view-editor-header/view-editor-header.component.css @@ -3,11 +3,50 @@ */ #view-editor-header-description-input { max-width: max-content; - min-height: 25px; - min-width: 600px; + min-height: 20px; + min-width: 400px; vertical-align: top; } +.list-pf-container { + -ms-flex-align: start; + align-items: flex-start; + display: -ms-flexbox; + display: flex; + padding: 0; +} + +.view-table-div { + padding-left: 0; + padding-right: 0; + margin-bottom: 5px; + min-height: 70px; + max-height: 70px; + border: 1px inset grey; + overflow-y: auto; +} + +.view-editor-header-name-input { + padding-left: 0; + padding-right: 5px; +} + +.view-editor-header-create-button { + margin-left: 0; + padding-left: 0; + padding-top: 8px; +} + +.view-editor-header-delete-button { + padding-left: 0; + padding-top: 8px; +} + +.view-editor-header-description-area { + padding-left: 0; + margin-left: 0; +} + /* * A div containing an input field in the header. */ @@ -16,6 +55,13 @@ padding: 1px; } +/* + * A type for the header title + */ +.view-editor-header-title { + margin-left: 10px; +} + /* * A type for vertically aligned labels in the header. */ @@ -45,3 +91,20 @@ #view-editor-header-virtualization-name { text-align: left; } + +/* + * Style the empty state component so that it is centered and extends the entire width. + */ +#view-editor-header-table .blank-slate-pf { + background-color: inherit; + min-width: 200px; + border: none; + padding: 0; +} + +/* + * Style text in blank slate for table + */ +#view-editor-header-table h1, .h1 { + font-size: 14px; +} diff --git a/ngapp/src/app/dataservices/virtualization/view-editor/view-editor-header/view-editor-header.component.html b/ngapp/src/app/dataservices/virtualization/view-editor/view-editor-header/view-editor-header.component.html index 4c89a6a6..10f945a5 100644 --- a/ngapp/src/app/dataservices/virtualization/view-editor/view-editor-header/view-editor-header.component.html +++ b/ngapp/src/app/dataservices/virtualization/view-editor/view-editor-header/view-editor-header.component.html @@ -3,69 +3,54 @@ -
- - {{virtualizationName}} - +
+

Virtualization Name: '{{virtualizationName}}'

- -
- +
+ Views: +
+
+ +
- - - - -
+
+
+ +
+
+ +
+
- - - -
- -
- + + + +
+ {{viewDescriptionLabel}} +
+ diff --git a/ngapp/src/app/dataservices/virtualization/view-editor/view-editor-header/view-editor-header.component.spec.ts b/ngapp/src/app/dataservices/virtualization/view-editor/view-editor-header/view-editor-header.component.spec.ts index 272b8cc9..21783d23 100644 --- a/ngapp/src/app/dataservices/virtualization/view-editor/view-editor-header/view-editor-header.component.spec.ts +++ b/ngapp/src/app/dataservices/virtualization/view-editor/view-editor-header/view-editor-header.component.spec.ts @@ -12,6 +12,9 @@ import { MockVdbService } from "@dataservices/shared/mock-vdb.service"; import { NotifierService } from "@dataservices/shared/notifier.service"; import { DataserviceService } from "@dataservices/shared/dataservice.service"; import { MockDataserviceService } from "@dataservices/shared/mock-dataservice.service"; +import { TableModule } from "patternfly-ng"; +import { SelectionService } from "@core/selection.service"; +import { BsModalService } from "ngx-bootstrap"; describe('ViewEditorHeaderComponent', () => { let component: ViewEditorHeaderComponent; @@ -22,14 +25,17 @@ describe('ViewEditorHeaderComponent', () => { imports: [ FormsModule, HttpModule, - RouterTestingModule + RouterTestingModule, + TableModule ], declarations: [ ViewEditorHeaderComponent ], providers: [ + BsModalService, { provide: AppSettingsService, useClass: MockAppSettingsService }, { provide: DataserviceService, useClass: MockDataserviceService }, LoggerService, NotifierService, + SelectionService, { provide: VdbService, useClass: MockVdbService }, ViewEditorService ] diff --git a/ngapp/src/app/dataservices/virtualization/view-editor/view-editor-header/view-editor-header.component.ts b/ngapp/src/app/dataservices/virtualization/view-editor/view-editor-header/view-editor-header.component.ts index 6132374b..07f72cd1 100644 --- a/ngapp/src/app/dataservices/virtualization/view-editor/view-editor-header/view-editor-header.component.ts +++ b/ngapp/src/app/dataservices/virtualization/view-editor/view-editor-header/view-editor-header.component.ts @@ -24,6 +24,17 @@ import { ViewEditorI18n } from "@dataservices/virtualization/view-editor/view-ed import { CommandFactory } from "@dataservices/virtualization/view-editor/command/command-factory"; import { Subscription } from "rxjs/Subscription"; import { Command } from "@dataservices/virtualization/view-editor/command/command"; +import { EmptyStateConfig, NgxDataTableConfig, TableConfig } from "patternfly-ng"; +import { ViewDefinition } from "@dataservices/shared/view-definition.model"; +import { BsModalService } from "ngx-bootstrap"; +import { CreateViewDialogComponent } from "@dataservices/virtualization/view-editor/create-view-dialog/create-view-dialog.component"; +import { DataserviceService } from "@dataservices/shared/dataservice.service"; +import { LoadingState } from "@shared/loading-state.enum"; +import { ConfirmDialogComponent } from "@shared/confirm-dialog/confirm-dialog.component"; +import { SelectionService } from "@core/selection.service"; +import { ViewEditorState } from "@dataservices/shared/view-editor-state.model"; +import { Dataservice } from "@dataservices/shared/dataservice.model"; +import { ViewEditorProgressChangeId } from "@dataservices/virtualization/view-editor/event/view-editor-save-progress-change-id.enum"; @Component({ encapsulation: ViewEncapsulation.None, @@ -34,29 +45,54 @@ import { Command } from "@dataservices/virtualization/view-editor/command/comman export class ViewEditorHeaderComponent implements OnInit, OnDestroy { // used by html - public readonly descriptionLabel = ViewEditorI18n.descriptionLabel; - public readonly descriptionPlaceholder = ViewEditorI18n.descriptionPlaceholder; - public readonly showDescriptionCheckbox = ViewEditorI18n.showDescriptionCheckbox; - public readonly viewNameLabel = ViewEditorI18n.viewNameLabel; - public readonly viewNamePlaceholder = ViewEditorI18n.viewNamePlaceholder; + public readonly viewDescriptionLabel = ViewEditorI18n.viewDescriptionLabel; + public readonly viewDescriptionPlaceholder = ViewEditorI18n.viewDescriptionPlaceholder; + + public ngxTableConfig: NgxDataTableConfig; + public tableConfig: TableConfig; + public tableColumns: any[] = []; + public tableRows: ViewDefinition[] = []; + private emptyStateConfig: EmptyStateConfig; private readonly logger: LoggerService; private readonly editorService: ViewEditorService; - public showDescription = false; private subscription: Subscription; + private modalService: BsModalService; + private dataserviceService: DataserviceService; + private selectionService: SelectionService; + private viewsLoadingState: LoadingState = LoadingState.LOADING; + private selectedVirtualization: Dataservice; + private viewSavedUponCompletion: ViewDefinition; constructor( editorService: ViewEditorService, - logger: LoggerService ) { + dataserviceService: DataserviceService, + selectionService: SelectionService, + logger: LoggerService, + modalService: BsModalService) { this.editorService = editorService; + this.dataserviceService = dataserviceService; + this.selectionService = selectionService; this.logger = logger; + this.modalService = modalService; } /** * @param {ViewEditorEvent} event the event being processed */ public handleEditorEvent( event: ViewEditorEvent ): void { - // TODO implement this.logger.debug( "ViewEditorHeaderComponent received event: " + event.toString() ); + + if ( event.typeIsEditorViewSaveProgressChanged() ) { + if ( event.args.length !== 0 ) { + // Detect changes in view editor save progress + if ( event.args[ 0 ] === ViewEditorProgressChangeId.COMPLETED_SUCCESS || + event.args[ 0 ] === ViewEditorProgressChangeId.COMPLETED_FAILED ) { + if (this.viewSavedUponCompletion && this.viewSavedUponCompletion !== null) { + this.createNewView(this.viewSavedUponCompletion); + } + } + } + } } /** @@ -71,6 +107,80 @@ export class ViewEditorHeaderComponent implements OnInit, OnDestroy { */ public ngOnInit(): void { this.subscription = this.editorService.editorEvent.subscribe( ( event ) => this.handleEditorEvent( event ) ); + + // ---------------------------------- + // View Table configurations + // ---------------------------------- + this.tableColumns = [ + { + draggable: false, + name: "Views", + prop: "viewName", + resizeable: true, + sortable: false, + width: "100" + } + ]; + + this.ngxTableConfig = { + headerHeight: 0, + rowHeight: 20, + reorderable: false, + selectionType: "'single'" + } as NgxDataTableConfig; + + this.emptyStateConfig = { + title: ViewEditorI18n.noViewsFound + } as EmptyStateConfig; + + this.tableConfig = { + emptyStateConfig: this.emptyStateConfig + } as TableConfig; + + // init the available views + this.initViews(); + } + + /* + * Initialize the views for the current dataservice. Makes a rest call to get the ViewEditorStates for the serviceVdb + */ + private initViews( ): void { + this.viewsLoadingState = LoadingState.LOADING; + this.selectedVirtualization = this.dataserviceService.getSelectedDataservice(); + if ( !this.selectedVirtualization || this.selectedVirtualization === null ) { + this.tableRows = []; + } + + const vdbName = this.selectedVirtualization.getServiceVdbName(); + const editorStatesPattern = vdbName.toLowerCase() + "*"; + + const self = this; + this.dataserviceService + .getViewEditorStates(editorStatesPattern) + .subscribe( + (viewEditorStates) => { + const viewDefns: ViewDefinition[] = []; + for ( const viewState of viewEditorStates ) { + const viewDefn = viewState.getViewDefinition(); + if ( viewDefn ) { + viewDefns.push( viewDefn ); + } + } + self.tableRows = viewDefns.sort( (left, right): number => { + if (left.getName() < right.getName()) return -1; + if (left.getName() > right.getName()) return 1; + return 0; + }); + const initialView = (self.tableRows && self.tableRows.length > 0) ? self.tableRows[0] : null; + self.selectView(initialView); + self.viewsLoadingState = LoadingState.LOADED_VALID; + }, + (error) => { + self.logger.error("[VirtualizationComponent] Error updating the views for the virtualization: %o", error); + self.viewsLoadingState = LoadingState.LOADED_INVALID; + self.tableRows = []; + } + ); } /** @@ -123,66 +233,231 @@ export class ViewEditorHeaderComponent implements OnInit, OnDestroy { } /** - * @returns {string} the view name + * @returns {string} the name of the dataservice of the view being edited */ - public get viewName(): string { - if ( this.editorService.getEditorView() ) { - return this.editorService.getEditorView().getName(); + public get virtualizationName(): string { + const virtualization = this.editorService.getEditorVirtualization(); + + if ( virtualization ) { + return virtualization.getId(); } - return ""; + // should always have a virtualization name so shouldn't get here + return "< error >"; } /** - * @param {string} newName the new name of the view + * @returns {string} the description of the dataservice of the view being edited */ - public set viewName( newName: string ) { - if ( this.editorService.getEditorView() ) { - if ( newName !== this.editorService.getEditorView().getName() ) { - const oldName = this.editorService.getEditorView().getName(); - const temp = CommandFactory.createUpdateViewNameCommand( newName, oldName ); + public get virtualizationDescription(): string { + const virtualization = this.editorService.getEditorVirtualization(); - if ( temp instanceof Command ) { - this.editorService.fireViewStateHasChanged( ViewEditorPart.HEADER, temp as Command ); - } else { - const error = temp as Error; - this.logger.error( error.message ); - } - } - } else { - // shouldn't get here as description text input should be disabled if no view being edited - this.logger.error( "Trying to set name but there is no view being edited" ); + if ( virtualization ) { + return virtualization.getDescription(); } + + // should always have a virtualization description so shouldn't get here + return "< error >"; + } + + public get deleteViewButtonEnabled(): boolean { + return ( this.getSelectedView() !== null ); } /** - * Called when text in the view name texteditor changes. - * - * @param {string} newName the new name of the view + * Handles view selection from table + * @param $event */ - public viewNameChanged( newName: string ): void { - this.viewName = newName; + public viewSelectionChanged( $event ): void { + const selectedViews: ViewDefinition[] = $event.selected; + // If the current view has pending changes, auto save it + if ( this.editorService.hasChanges() ) { + this.editorService.saveEditorState(); + } + this.selectView(selectedViews[0]); } /** - * @returns {string} the router link of the virtualization + * Handle creation of a new View. Displays the createView dialog, + * then saves the viewDefinition and adds it to the list */ - public get virtualizationLink(): string { - return this.editorService.getVirtualizationLink(); + public onCreateView(): void { + // Open New View dialog + const initialState = { + title: ViewEditorI18n.createViewDialogTitle, + cancelButtonText: ViewEditorI18n.cancelButtonText, + okButtonText: ViewEditorI18n.okButtonText + }; + + // Show Dialog, act upon confirmation click + const modalRef = this.modalService.show(CreateViewDialogComponent, {initialState}); + modalRef.content.okAction.take(1).subscribe((viewDefn) => { + // If the current view has pending changes, save them first + if ( this.editorService.hasChanges() ) { + this.viewSavedUponCompletion = viewDefn; + this.editorService.saveEditorState(); + } else { + this.createNewView(viewDefn); + } + }); + } + + private createNewView(viewDefn: ViewDefinition): void { + const selectedDs = this.dataserviceService.getSelectedDataservice(); + const editorId = this.getEditorStateId(selectedDs, viewDefn); + + // Create new editor state to save + const editorState = new ViewEditorState(); + editorState.setId(editorId); + editorState.setViewDefinition(viewDefn); + + const dsName = selectedDs.getId(); + const self = this; + this.dataserviceService + .saveViewEditorStateRefreshViews(editorState, selectedDs.getId()) + .subscribe( + (wasSuccess) => { + // Add the new ViewDefinition to the table + self.addViewDefinitionToList(viewDefn); + self.viewSavedUponCompletion = null; + }, + (error) => { + self.logger.error("[VirtualizationComponent] Error saving the editor state: %o", error); + self.viewSavedUponCompletion = null; + } + ); } /** - * @returns {string} the name of the dataservice of the view being edited + * Construct id for the editor state + * @param {Dataservice} dataservice the dataservice + * @param {ViewDefinition} viewDefn the view definition + * @returns {string} the ID used to persist the editor state */ - public get virtualizationName(): string { - const virtualization = this.editorService.getEditorVirtualization(); + private getEditorStateId(dataservice: Dataservice, viewDefn: ViewDefinition): string { + return dataservice.getServiceVdbName().toLowerCase() + "." + viewDefn.getName(); + } - if ( virtualization ) { - return virtualization.getId(); + /** + * Handle Delete of the selected View + * @param {string} viewName + */ + public onDeleteView( ): void { + const viewName = this.getSelectedView().getName(); + + // Dialog Content + const message = "Do you really want to delete View '" + viewName + "'?"; + const initialState = { + title: "Confirm Delete", + bodyContent: message, + cancelButtonText: "Cancel", + confirmButtonText: "Delete" + }; + + // Show Dialog, act upon confirmation click + const modalRef = this.modalService.show(ConfirmDialogComponent, {initialState}); + modalRef.content.confirmAction.take(1).subscribe((value) => { + this.doDeleteView(viewName); + }); + } + + /** + * Deletes the specified ViewEditorState from the userProfile, and removes ViewDefinition from the current list. + * @param {string} viewDefnName the name of the view + */ + private doDeleteView(viewDefnName: string): void { + const selectedViewDefn = this.tableRows.find((x) => x.getName() === viewDefnName); + const selectedDs = this.dataserviceService.getSelectedDataservice(); + const vdbName = selectedDs.getServiceVdbName(); + const editorStateId = vdbName.toLowerCase() + "." + viewDefnName; + const dataserviceName = selectedDs.getId(); + // Note: we can only doDelete selected items that we can see in the UI. + this.logger.debug("[VirtualizationComponent] Deleting selected Virtualization View."); + const self = this; + this.dataserviceService + .deleteViewEditorStateRefreshViews(editorStateId, dataserviceName) + .subscribe( + (wasSuccess) => { + self.removeViewDefinitionFromList(selectedViewDefn); + }, + (error) => { + self.logger.error("[VirtualizationComponent] Error deleting the editor state: %o", error); + } + ); + } + + /* + * Add the specified ViewDefinition to the view definitions table + * @param {ViewDefinition} viewDefn the view definition to add + */ + private addViewDefinitionToList(viewDefn: ViewDefinition): void { + const newRows: ViewDefinition[] = []; + newRows.push(viewDefn); + for ( const row of this.tableRows ) { + if ( row.getName() !== viewDefn.getName() ) { + newRows.push( row ); + } } + this.tableRows = newRows.sort( (left, right): number => { + if (left.getName() < right.getName()) return -1; + if (left.getName() > right.getName()) return 1; + return 0; + }); + this.selectView(viewDefn); + } - // should always have a virtualization name so shouldn't get here - return "< error >"; + /* + * Remove the specified ViewDefinition from the view definitions table + * @param {ViewDefinition} viewDefn the view definition to remove + */ + private removeViewDefinitionFromList(viewDefn: ViewDefinition): void { + const origIndex = this.tableRows.findIndex( ( defn ) => defn.getName() === viewDefn.getName() ); + + const newRows: ViewDefinition[] = []; + for ( const row of this.tableRows ) { + if ( row.getName() !== viewDefn.getName() ) { + newRows.push( row ); + } + } + this.tableRows = newRows; + + // auto select another row + if ( this.tableRows.length > origIndex ) { + this.selectView( this.tableRows[origIndex] ); + } else if ( this.tableRows.length > 0 ) { + this.selectView( this.tableRows[origIndex - 1] ); + } else if ( this.tableRows.length === 0 ) { + this.selectView( null ); + } + } + + private selectView( selView: ViewDefinition ): void { + // Updates table selection display + let viewSelection = null; + if ( selView !== null ) { + for (const view of this.tableRows) { + if (view.getName() === selView.getName()) { + view.setSelected(true); + viewSelection = view; + } else { + view.setSelected(false); + } + } + } + // Update selection service, then fire event + this.selectionService.setSelectedViewDefinition(this.selectedVirtualization, selView); + this.editorService.setEditorView(viewSelection, ViewEditorPart.HEADER); + } + + private getSelectedView( ): ViewDefinition { + let selectedView: ViewDefinition = null; + for (const view of this.tableRows) { + if (view.selected) { + selectedView = view; + break; + } + } + return selectedView; } } diff --git a/ngapp/src/app/dataservices/virtualization/view-editor/view-editor-i18n.ts b/ngapp/src/app/dataservices/virtualization/view-editor/view-editor-i18n.ts index e252e043..962d4b16 100644 --- a/ngapp/src/app/dataservices/virtualization/view-editor/view-editor-i18n.ts +++ b/ngapp/src/app/dataservices/virtualization/view-editor/view-editor-i18n.ts @@ -38,6 +38,14 @@ export class ViewEditorI18n { public static readonly currentSelection = "Current Selection:"; public static readonly noSelection = "Nothing selected"; + // create virtualization dialog + public static readonly createVirtualizationDialogMessage = "Enter name and optional description for the virtualization and view"; + public static readonly createVirtualizationDialogTitle = "Create Virtualization"; + + // create view dialog + public static readonly createViewDialogMessage = "Enter name and description(optional) for the new view"; + public static readonly createViewDialogTitle = "Create View"; + // Add Composition Wizard public static readonly addCompositionWizardTitle = "Add Composition"; public static readonly addCompositionWizardSelectSourceMessage = "Expand connection and select a source for the composition"; @@ -46,7 +54,6 @@ export class ViewEditorI18n { public static readonly addCompositionWizardStep2Text = "Define Composition"; public static readonly addCompositionWizardLoadingPrimaryText = "Add Composition Wizard loading"; public static readonly addCompositionWizardLoadingSecondaryText = "Please wait for the wizard to finish loading..."; - public static readonly addCompositionWizardSwapButtonText = "Swap"; // editor views public static readonly messagesTabName = "Messages"; @@ -101,11 +108,9 @@ export class ViewEditorI18n { public static readonly warningsActionTooltip = "Show warning messages"; // view editor header - public static readonly descriptionLabel = "Description:"; - public static readonly descriptionPlaceholder = "Enter a view description"; - public static readonly showDescriptionCheckbox = "Show Description"; - public static readonly viewNameLabel = "View Name:"; - public static readonly viewNamePlaceholder = "Enter a view name"; + public static readonly viewDescriptionLabel = "Selected View Description"; + public static readonly viewDescriptionPlaceholder = "Enter a view description"; + public static readonly noViewsFound = "No views found"; // view preview public static readonly previewDataUnavailable = "Preview data unavailable"; diff --git a/ngapp/src/app/dataservices/virtualization/view-editor/view-editor.component.html b/ngapp/src/app/dataservices/virtualization/view-editor/view-editor.component.html index efbfdec4..38912bf7 100644 --- a/ngapp/src/app/dataservices/virtualization/view-editor/view-editor.component.html +++ b/ngapp/src/app/dataservices/virtualization/view-editor/view-editor.component.html @@ -7,8 +7,7 @@
  • -
  • -
  • +
  • diff --git a/ngapp/src/app/dataservices/virtualization/view-editor/view-editor.component.ts b/ngapp/src/app/dataservices/virtualization/view-editor/view-editor.component.ts index ecbe9c77..40ab593b 100644 --- a/ngapp/src/app/dataservices/virtualization/view-editor/view-editor.component.ts +++ b/ngapp/src/app/dataservices/virtualization/view-editor/view-editor.component.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { AfterViewInit, Component, DoCheck, OnDestroy, OnInit, TemplateRef, ViewEncapsulation } from "@angular/core"; +import { Component, DoCheck, OnDestroy, OnInit, TemplateRef, ViewEncapsulation } from "@angular/core"; import { LoggerService } from "@core/logger.service"; import { SelectionService } from "@core/selection.service"; import { Connection } from "@connections/shared/connection.model"; @@ -24,8 +24,6 @@ import { DataservicesConstants } from "@dataservices/shared/dataservices-constan import { ViewEditorService } from "@dataservices/virtualization/view-editor/view-editor.service"; import { ViewEditorPart } from "@dataservices/virtualization/view-editor/view-editor-part.enum"; import { ViewEditorEvent } from "@dataservices/virtualization/view-editor/event/view-editor-event"; -import { Message } from "@dataservices/virtualization/view-editor/editor-views/message-log/message"; -import { Problem } from "@dataservices/virtualization/view-editor/editor-views/message-log/problem"; import { ViewEditorEventType } from "@dataservices/virtualization/view-editor/event/view-editor-event-type.enum"; import { ConnectionTableDialogComponent } from "@dataservices/virtualization/view-editor/connection-table-dialog/connection-table-dialog.component"; import { ViewEditorProgressChangeId } from "@dataservices/virtualization/view-editor/event/view-editor-save-progress-change-id.enum"; @@ -41,7 +39,8 @@ import { AddSourcesCommand } from "@dataservices/virtualization/view-editor/comm import { AddCompositionCommand } from "@dataservices/virtualization/view-editor/command/add-composition-command"; import { SchemaNode } from "@connections/shared/schema-node.model"; import { Composition } from "@dataservices/shared/composition.model"; -import { ViewDefinition } from "@dataservices/shared/view-definition.model"; +import { Router } from "@angular/router"; +import { NavigationStart } from "@angular/router"; @Component({ encapsulation: ViewEncapsulation.None, @@ -50,19 +49,19 @@ import { ViewDefinition } from "@dataservices/shared/view-definition.model"; styleUrls: ["./view-editor.component.css"], providers: [ ViewEditorService ] }) -export class ViewEditorComponent implements DoCheck, OnDestroy, OnInit, AfterViewInit { +export class ViewEditorComponent implements DoCheck, OnDestroy, OnInit { private actionConfig: ActionConfig; private connections: Connection[] = []; private connectionService: ConnectionService; private readonly editorService: ViewEditorService; - private fatalErrorOccurred = false; - private isNewView = false; private readonly logger: LoggerService; private modalService: BsModalService; + private readonly router: Router; private selectionService: SelectionService; private subscription: Subscription; private saveInProgress = false; + private routeSub: Subscription; public toolbarConfig: ToolbarConfig; public readonly virtualizationsLink = DataservicesConstants.dataservicesRootPath; @@ -104,10 +103,12 @@ export class ViewEditorComponent implements DoCheck, OnDestroy, OnInit, AfterVie selectionService: SelectionService, logger: LoggerService, editorService: ViewEditorService, - modalService: BsModalService ) { + modalService: BsModalService, + router: Router ) { this.connectionService = connectionService; this.logger = logger; this.modalService = modalService; + this.router = router; this.selectionService = selectionService; // this is the service that is injected into all the editor parts @@ -115,27 +116,175 @@ export class ViewEditorComponent implements DoCheck, OnDestroy, OnInit, AfterVie this.editorService.setEditorVirtualization( selectionService.getSelectedVirtualization() ); } + /** + * Executed by javascript framework when something changes. Used to set then enable state of the toolbar buttons. + */ + public ngDoCheck(): void { + if (this.actionConfig ) { + this.actionConfig.primaryActions[ this.addCompositionActionIndex ].disabled = !this.canAddComposition(); + this.actionConfig.primaryActions[ this.addSourceActionIndex ].disabled = !this.canAddSource(); + this.actionConfig.primaryActions[ this.deleteActionIndex ].disabled = !this.canDelete(); + this.actionConfig.primaryActions[ this.errorsActionIndex ].disabled = !this.hasErrors(); + this.actionConfig.primaryActions[ this.infosActionIndex ].disabled = !this.hasInfos(); + this.actionConfig.primaryActions[ this.redoActionIndex ].disabled = !this.canRedo(); + this.actionConfig.primaryActions[ this.redoActionIndex ].tooltip = this.editorService.getRedoActionTooltip(); + this.actionConfig.primaryActions[ this.saveActionIndex ].disabled = !this.canSave(); + this.actionConfig.primaryActions[ this.undoActionIndex ].disabled = !this.canUndo(); + this.actionConfig.primaryActions[ this.undoActionIndex ].tooltip = this.editorService.getUndoActionTooltip(); + this.actionConfig.primaryActions[ this.warningsActionIndex ].disabled = !this.hasWarnings(); + } + } + + /** + * Cleanup code when destroying the editor. + */ + public ngOnDestroy(): void { + this.subscription.unsubscribe(); + this.routeSub.unsubscribe(); + } + + /** + * Initialization code run after construction. + */ + public ngOnInit(): void { + this.editorService.setEditorConfig( this.fullEditorCssType ); // this could be set via preference or last used config + this.subscription = this.editorService.editorEvent.subscribe( ( event ) => this.handleEditorEvent( event ) ); + + this.toolbarConfig = { + views: [ + { + id: this.fullEditorCssType, + iconStyleClass: "fa fa-file-text-o", + tooltip: ViewEditorI18n.showEditorCanvasAndViewsActionTooltip + }, + { + id: this.canvasOnlyCssType, + iconStyleClass: "fa fa-file-image-o", + tooltip: ViewEditorI18n.showEditorCanvasOnlyActionTooltip + }, + { + id: this.viewsOnlyCssType, + iconStyleClass: "fa fa-table", + tooltip: ViewEditorI18n.showEditorViewsOnlyActionTooltip + } + ] + } as ToolbarConfig; + + // Load the connections + const self = this; + this.connectionService + .getConnections(true, true) + .subscribe( + (connectionSummaries) => { + const conns = []; + for ( const connectionSummary of connectionSummaries ) { + const connStatus = connectionSummary.getStatus(); + const conn = connectionSummary.getConnection(); + conn.setStatus(connStatus); + conns.push(conn); + self.connections = conns; + } + }, + (error) => { + // self.logger.error("[ConnectionSchemaTreeComponent] Error getting connections: %o", error); + // self.connectionLoadingState = LoadingState.LOADED_INVALID; + } + ); + + // Listen for event when user moves away from this page + this.routeSub = this.router.events.pairwise().subscribe((event) => { + if (event[1] instanceof NavigationStart) { + if (this.editorService.hasChanges()) { + this.editorService.saveEditorState(); + } + } + }); + } + + /** + * Determine if a view is currently selected + */ + private get hasSelectedView(): boolean { + const selView = this.editorService.getEditorView(); + return (selView && selView !== null); + } + + /** + * @param {ViewEditorEvent} event the event being processed + */ + public handleEditorEvent( event: ViewEditorEvent ): void { + this.logger.debug( "ViewEditorComponent received event: " + event.toString() ); + + if ( event.typeIsShowEditorPart() ) { + if ( event.args.length !== 0 ) { + // make sure the bottom area is showing if part is an additional editor view + if ( ( event.args[ 0 ] === ViewEditorPart.PREVIEW || event.args[ 0 ] === ViewEditorPart.MESSAGE_LOG ) + && !this.isShowingAdditionalViews ) { + this.editorService.setEditorConfig( this.fullEditorCssType ); + } + } + } + else if (event.typeIsCreateSource()) { + if (event.sourceIsCanvas()) { + alert("Multiple compositions not yet supported"); + } else { + this.doAddSource(); + } + } + else if (event.typeIsCreateComposition()) { + this.doAddComposition(event.args); + } + else if (event.typeIsDeleteNode()) { + const selection = []; + selection.push(event.args[0]); + this.doDelete(selection); + } + else if (event.typeIsCanvasSelectionChanged()) { + this.doSelection(event.args); + } + else if ( event.typeIsEditorViewSaveProgressChanged() ) { + if ( event.args.length !== 0 ) { + // Detect changes in view editor save progress + if ( event.args[ 0 ] === ViewEditorProgressChangeId.IN_PROGRESS ) { + this.saveInProgress = true; + } else if ( event.args[ 0 ] === ViewEditorProgressChangeId.COMPLETED_SUCCESS ) { + this.editorService.updatePreviewResults(); + this.saveInProgress = false; + } else if ( event.args[ 0 ] === ViewEditorProgressChangeId.COMPLETED_FAILED ) { + this.editorService.setPreviewResults(null, null, ViewEditorPart.EDITOR); + this.saveInProgress = false; + } + } + } + } + private canAddComposition(): boolean { - return !this.fatalErrorOccurred && !this.editorService.isReadOnly() && this.isShowingCanvas && this.canvasSingleSourceSelected; + return this.hasSelectedView && + !this.editorService.isReadOnly() && + this.isShowingCanvas && + this.canvasSingleSourceSelected; } private canAddSource(): boolean { - return !this.fatalErrorOccurred && !this.editorService.isReadOnly() && this.isShowingCanvas; + return this.hasSelectedView && + !this.editorService.isReadOnly() && + this.isShowingCanvas; } private canDelete(): boolean { - return !this.fatalErrorOccurred && - !this.editorService.isReadOnly() && - this.isShowingCanvas && - this.editorService.hasSelection(); + return this.hasSelectedView && + !this.editorService.isReadOnly() && + this.isShowingCanvas && + this.editorService.hasSelection(); } private canRedo(): boolean { - return !this.fatalErrorOccurred && this.editorService.canRedo(); + return this.hasSelectedView && + this.editorService.canRedo(); } private canSave(): boolean { - return !this.fatalErrorOccurred + return this.hasSelectedView && !this.editorService.isReadOnly() && this.editorService.canSaveView() && this.editorService.hasChanges() @@ -143,7 +292,8 @@ export class ViewEditorComponent implements DoCheck, OnDestroy, OnInit, AfterVie } private canUndo(): boolean { - return !this.fatalErrorOccurred && this.editorService.canUndo(); + return this.hasSelectedView && + this.editorService.canUndo(); } private doAddComposition(sourcePaths: string[]): void { @@ -461,55 +611,6 @@ export class ViewEditorComponent implements DoCheck, OnDestroy, OnInit, AfterVie } } - /** - * @param {ViewEditorEvent} event the event being processed - */ - public handleEditorEvent( event: ViewEditorEvent ): void { - this.logger.debug( "ViewEditorComponent received event: " + event.toString() ); - - if ( event.typeIsShowEditorPart() ) { - if ( event.args.length !== 0 ) { - // make sure the bottom area is showing if part is an additional editor view - if ( ( event.args[ 0 ] === ViewEditorPart.PREVIEW || event.args[ 0 ] === ViewEditorPart.MESSAGE_LOG ) - && !this.isShowingAdditionalViews ) { - this.editorService.setEditorConfig( this.fullEditorCssType ); - } - } - } - else if (event.typeIsCreateSource()) { - if (event.sourceIsCanvas()) { - alert("Multiple compositions not yet supported"); - } else { - this.doAddSource(); - } - } - else if (event.typeIsCreateComposition()) { - this.doAddComposition(event.args); - } - else if (event.typeIsDeleteNode()) { - const selection = []; - selection.push(event.args[0]); - this.doDelete(selection); - } - else if (event.typeIsCanvasSelectionChanged()) { - this.doSelection(event.args); - } - else if ( event.typeIsEditorViewSaveProgressChanged() ) { - if ( event.args.length !== 0 ) { - // Detect changes in view editor save progress - if ( event.args[ 0 ] === ViewEditorProgressChangeId.IN_PROGRESS ) { - this.saveInProgress = true; - } else if ( event.args[ 0 ] === ViewEditorProgressChangeId.COMPLETED_SUCCESS ) { - this.editorService.updatePreviewResults(); - this.saveInProgress = false; - } else if ( event.args[ 0 ] === ViewEditorProgressChangeId.COMPLETED_FAILED ) { - this.editorService.setPreviewResults(null, null, ViewEditorPart.EDITOR); - this.saveInProgress = false; - } - } - } - } - private hasErrors(): boolean { return this.errorMsgCount !== 0; } @@ -576,115 +677,4 @@ export class ViewEditorComponent implements DoCheck, OnDestroy, OnInit, AfterVie return false; } - /** - * Executed by javascript framework when something changes. Used to set then enable state of the toolbar buttons. - */ - public ngDoCheck(): void { - if (this.actionConfig ) { - this.actionConfig.primaryActions[ this.addCompositionActionIndex ].disabled = !this.canAddComposition(); - this.actionConfig.primaryActions[ this.addSourceActionIndex ].disabled = !this.canAddSource(); - this.actionConfig.primaryActions[ this.deleteActionIndex ].disabled = !this.canDelete(); - this.actionConfig.primaryActions[ this.errorsActionIndex ].disabled = !this.hasErrors(); - this.actionConfig.primaryActions[ this.infosActionIndex ].disabled = !this.hasInfos(); - this.actionConfig.primaryActions[ this.redoActionIndex ].disabled = !this.canRedo(); - this.actionConfig.primaryActions[ this.redoActionIndex ].tooltip = this.editorService.getRedoActionTooltip(); - this.actionConfig.primaryActions[ this.saveActionIndex ].disabled = !this.canSave(); - this.actionConfig.primaryActions[ this.undoActionIndex ].disabled = !this.canUndo(); - this.actionConfig.primaryActions[ this.undoActionIndex ].tooltip = this.editorService.getUndoActionTooltip(); - this.actionConfig.primaryActions[ this.warningsActionIndex ].disabled = !this.hasWarnings(); - } - } - - /** - * Cleanup code when destroying the editor. - */ - public ngOnDestroy(): void { - this.subscription.unsubscribe(); - } - - /** - * Initialization code run after construction. - */ - public ngOnInit(): void { - this.editorService.setEditorConfig( this.fullEditorCssType ); // this could be set via preference or last used config - this.subscription = this.editorService.editorEvent.subscribe( ( event ) => this.handleEditorEvent( event ) ); - - this.toolbarConfig = { - views: [ - { - id: this.fullEditorCssType, - iconStyleClass: "fa fa-file-text-o", - tooltip: ViewEditorI18n.showEditorCanvasAndViewsActionTooltip - }, - { - id: this.canvasOnlyCssType, - iconStyleClass: "fa fa-file-image-o", - tooltip: ViewEditorI18n.showEditorCanvasOnlyActionTooltip - }, - { - id: this.viewsOnlyCssType, - iconStyleClass: "fa fa-table", - tooltip: ViewEditorI18n.showEditorViewsOnlyActionTooltip - } - ] - } as ToolbarConfig; - - // Load the connections - const self = this; - this.connectionService - .getConnections(true, true) - .subscribe( - (connectionSummaries) => { - const conns = []; - for ( const connectionSummary of connectionSummaries ) { - const connStatus = connectionSummary.getStatus(); - const conn = connectionSummary.getConnection(); - conn.setStatus(connStatus); - conns.push(conn); - self.connections = conns; - } - }, - (error) => { - // self.logger.error("[ConnectionSchemaTreeComponent] Error getting connections: %o", error); - // self.connectionLoadingState = LoadingState.LOADED_INVALID; - } - ); - - } - - /** - * Lifecycle hook after the component is fully initialized. Need to set viewDefinition here to ensure that the - * child components have been fully intiialized and can receive events - */ - public ngAfterViewInit(): void { - const virtualization = this.editorService.getEditorVirtualization(); - - let selectedViewDefn = this.selectionService.getSelectedViewDefinition(); - if (!selectedViewDefn) { - this.isNewView = true; - selectedViewDefn = new ViewDefinition(); - } - - this.editorService.setOriginalViewName(selectedViewDefn.getName()); - - // must have a virtualization parent - if ( virtualization ) { - this.editorService.setEditorView(selectedViewDefn, ViewEditorPart.EDITOR); - if (!this.isNewView) { - this.editorService.updatePreviewResults(); - } - } else { - // must have a virtualization selected - this.editorService.addMessage( Message.create( Problem.ERR0100 ), ViewEditorPart.EDITOR ); - this.fatalErrorOccurred = true; - } - } - - /** - * @returns {string} the router link of the virtualization - */ - public get virtualizationLink(): string { - return this.editorService.getVirtualizationLink(); - } - } diff --git a/ngapp/src/app/dataservices/virtualization/view-editor/view-editor.service.ts b/ngapp/src/app/dataservices/virtualization/view-editor/view-editor.service.ts index c4de7bb6..9d14a675 100644 --- a/ngapp/src/app/dataservices/virtualization/view-editor/view-editor.service.ts +++ b/ngapp/src/app/dataservices/virtualization/view-editor/view-editor.service.ts @@ -17,7 +17,6 @@ import { EventEmitter, Injectable, Output } from "@angular/core"; import { LoggerService } from "@core/logger.service"; -import { DataservicesConstants } from "@dataservices/shared/dataservices-constants"; import { Dataservice } from "@dataservices/shared/dataservice.model"; import { QueryResults } from "@dataservices/shared/query-results.model"; import { VdbService } from "@dataservices/shared/vdb.service"; @@ -64,12 +63,12 @@ export class ViewEditorService { private _previewSql = null; private _readOnly = false; private _shouldFireEvents = true; - private readonly _undoMgr: UndoManager; + private _undoMgr: UndoManager; private readonly _dataserviceService: DataserviceService; private readonly _vdbService: VdbService; private _warningMsgCount = 0; private _selection: string[] = []; - private _originalViewName = null; + private _originalView: ViewDefinition = null; constructor( logger: LoggerService, dataserviceService: DataserviceService, @@ -303,13 +302,6 @@ export class ViewEditorService { return this._undoMgr.undoLabel(); } - /** - * @returns {string} the router link of the virtualization - */ - public getVirtualizationLink(): string { - return DataservicesConstants.virtualizationPath; - } - /** * @returns {number} the number of warning messages */ @@ -328,8 +320,11 @@ export class ViewEditorService { * @returns {boolean} `true` if the editor has unsaved changes */ public hasChanges(): boolean { - // TODO implement hasChanges - return true; + let hasChanged = false; + if ( this._editorView && this._editorView !== null ) { + hasChanged = !this._editorView.isEqual(this._originalView); + } + return hasChanged; } /** @@ -411,7 +406,7 @@ export class ViewEditorService { } /** - * Saves the current editor state. If a previous state exists, it is deleted first + * Saves the current editor state. */ public saveEditorState(): void { // fire save in progress event @@ -419,35 +414,9 @@ export class ViewEditorService { ViewEditorEventType.EDITOR_VIEW_SAVE_PROGRESS_CHANGED, [ ViewEditorProgressChangeId.IN_PROGRESS ] ) ); - // If a previous view state needs to be deleted, do that first. Then save the new state - if (this._originalViewName && this._originalViewName !== null && this._originalViewName !== this._editorView.getName()) { - // Delete the 'old' view editorState and view first - const oldEditorStateId = this.getEditorStateId(this._originalViewName); - - const self = this; - this._dataserviceService.deleteViewEditorState( oldEditorStateId ).subscribe( () => { - self.saveCurrentState(); - }, () => { - // fire save editor state failed event - this.fire( ViewEditorEvent.create( ViewEditorPart.EDITOR, - ViewEditorEventType.EDITOR_VIEW_SAVE_PROGRESS_CHANGED, - [ ViewEditorProgressChangeId.COMPLETED_FAILED ] ) ); - } - ); - } else { - this.saveCurrentState(); - } - } - - /** - * Saves the current editor's state. - * The ViewEditor state consists of the Undoables array (the current redo stack is not saved), - * plus the current ViewEditorDefinition - */ - private saveCurrentState(): void { // Save the current editorState - this._originalViewName = this._editorView.getName(); - const editorId = this.getEditorStateId(this._originalViewName); + const viewName = this._editorView.getName(); + const editorId = this.getEditorStateId(viewName); // ViewEditorState contains Undoables array plus current ViewDefinition const editorState = new ViewEditorState(); @@ -484,14 +453,6 @@ export class ViewEditorService { } } - /** - * Set the original view name (when editor initialized or last save) - * @param {string} viewName the view name - */ - public setOriginalViewName( viewName: string ): void { - this._originalViewName = viewName; - } - /** * Sets the view being edited. This should only be called once. Subsequent calls are ignored. Fires a * `ViewEditorEventType.VIEW_CHANGED` event having the view definition as an argument. @@ -501,13 +462,14 @@ export class ViewEditorService { */ public setEditorView( viewDefn: ViewDefinition, source: ViewEditorPart ): void { - if ( !this._editorView ) { - this._editorView = viewDefn; - this.fire( ViewEditorEvent.create( source, ViewEditorEventType.EDITED_VIEW_SET, [ this._editorView ] ) ); - // this.restoreUndoables(); + this._editorView = viewDefn; + if ( viewDefn !== null ) { + this._originalView = ViewDefinition.create(viewDefn.toJSON()); } else { - this._logger.debug( "setEditorView called more than once" ); + this._originalView = null; } + this.resetUndoManager(); + this.fire( ViewEditorEvent.create( source, ViewEditorEventType.EDITED_VIEW_SET, [ this._editorView ] ) ); } /** @@ -710,4 +672,11 @@ export class ViewEditorService { public hasSelection(): boolean { return this._selection.length > 0; } + + /** + * Reset the UndoManager + */ + private resetUndoManager(): void { + this._undoMgr = new UndoManager(); + } } diff --git a/ngapp/src/app/dataservices/virtualization/view-editor/view-validator.ts b/ngapp/src/app/dataservices/virtualization/view-editor/view-validator.ts index d74fdf0d..1a2a6d75 100644 --- a/ngapp/src/app/dataservices/virtualization/view-editor/view-validator.ts +++ b/ngapp/src/app/dataservices/virtualization/view-editor/view-validator.ts @@ -30,7 +30,10 @@ export class ViewValidator { public static validate( viewDefn: ViewDefinition ): Message[] { const messages: Message[] = []; - if ( viewDefn ) { + if ( !viewDefn || viewDefn === null ) { + messages.push(Message.create( Problem.ERR0110)); + } + else { // View must have a name if ( !viewDefn.getName() || viewDefn.getName().length === 0 ) { messages.push( Message.create( Problem.ERR0110 ) ); diff --git a/ngapp/src/app/dataservices/virtualization/virtualization.component.css b/ngapp/src/app/dataservices/virtualization/virtualization.component.css deleted file mode 100644 index 4d4d2aee..00000000 --- a/ngapp/src/app/dataservices/virtualization/virtualization.component.css +++ /dev/null @@ -1,3 +0,0 @@ -.description-label .control-label { - text-align: left; -} diff --git a/ngapp/src/app/dataservices/virtualization/virtualization.component.html b/ngapp/src/app/dataservices/virtualization/virtualization.component.html deleted file mode 100644 index 64329653..00000000 --- a/ngapp/src/app/dataservices/virtualization/virtualization.component.html +++ /dev/null @@ -1,125 +0,0 @@ -
    -
    - -
  • -
  • -
    -
    -
    -
    -

    Virtualization

    -
    - -
    -
    - -
    - -
    {{ nameValidationError }}
    -
    - - - - - - - -
    -
    - -
    - -
    -
    -
    - -
    -
    - Add View -
    -
    - - -
    - - -
    -
    -
    - - -
    -
    -
    - - -
    -
    -
    - - -
    -
    -
    - - -
    -
    -
    - - -
    -
    -
    - -
    -
    -
    - - -
    -
    -
    - - The virtualization views failed to load! -
    -
    -
    - - -
    - - -
    -
    - -
    -
    diff --git a/ngapp/src/app/dataservices/virtualization/virtualization.component.spec.ts b/ngapp/src/app/dataservices/virtualization/virtualization.component.spec.ts deleted file mode 100644 index cb05a908..00000000 --- a/ngapp/src/app/dataservices/virtualization/virtualization.component.spec.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { RouterTestingModule } from "@angular/router/testing"; -import { CoreModule } from "@core/core.module"; -import { MockAppSettingsService } from "@core/mock-app-settings.service"; -import { AppSettingsService } from "@core/app-settings.service"; -import { SelectionService } from "@core/selection.service"; -import { DataserviceService } from "@dataservices/shared/dataservice.service"; -import { MockDataserviceService } from "@dataservices/shared/mock-dataservice.service"; -import { MockVdbService } from "@dataservices/shared/mock-vdb.service"; -import { NotifierService } from "@dataservices/shared/notifier.service"; -import { VdbService } from "@dataservices/shared/vdb.service"; -import { VirtualizationComponent } from "@dataservices/virtualization/virtualization.component"; -import { ViewCardsComponent } from "@dataservices/virtualization/view-cards/view-cards.component"; -import { ViewCardComponent } from "@dataservices/virtualization/view-cards/view-card/view-card.component"; -import { PropertyFormPropertyComponent } from "@shared/property-form/property-form-property/property-form-property.component"; -import { - ActionModule, - CardModule, - EmptyStateModule, - FilterModule, - ListModule, - NotificationModule, - SortModule, - TableModule, - WizardModule } from "patternfly-ng"; - -describe("VirtualizationComponent", () => { - let component: VirtualizationComponent; - let fixture: ComponentFixture; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ - CoreModule, - FormsModule, - ActionModule, - CardModule, - EmptyStateModule, - FilterModule, - ListModule, - NotificationModule, - SortModule, - TableModule, - WizardModule, - ReactiveFormsModule, - RouterTestingModule - ], - declarations: [ - PropertyFormPropertyComponent, - ViewCardComponent, - ViewCardsComponent, - VirtualizationComponent - ], - providers: [ - { provide: AppSettingsService, useClass: MockAppSettingsService }, - { provide: DataserviceService, useClass: MockDataserviceService }, - NotifierService, - SelectionService, - { provide: VdbService, useClass: MockVdbService } - ] - }) - .compileComponents().then(() => { - // nothing to do - }); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(VirtualizationComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it("should be created", () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/ngapp/src/app/dataservices/virtualization/virtualization.component.ts b/ngapp/src/app/dataservices/virtualization/virtualization.component.ts deleted file mode 100644 index 7643f792..00000000 --- a/ngapp/src/app/dataservices/virtualization/virtualization.component.ts +++ /dev/null @@ -1,456 +0,0 @@ -import { Component, OnInit } from "@angular/core"; -import { AbstractControl, FormControl, FormGroup } from "@angular/forms"; -import { Router } from "@angular/router"; -import { LoggerService } from "@core/logger.service"; -import { SelectionService } from "@core/selection.service"; -import { Dataservice } from "@dataservices/shared/dataservice.model"; -import { DataserviceService } from "@dataservices/shared/dataservice.service"; -import { DataservicesConstants } from "@dataservices/shared/dataservices-constants"; -import { ConfirmDialogComponent } from "@shared/confirm-dialog/confirm-dialog.component"; -import { BsModalService } from "ngx-bootstrap"; -import { ActionConfig, EmptyStateConfig } from "patternfly-ng"; -import { NewDataservice } from "@dataservices/shared/new-dataservice.model"; -import { VdbService } from "@dataservices/shared/vdb.service"; -import { LoadingState } from "@shared/loading-state.enum"; -import { ViewDefinition } from "@dataservices/shared/view-definition.model"; - -@Component({ - selector: "app-virtualization", - templateUrl: "./virtualization.component.html", - styleUrls: ["./virtualization.component.css"] -}) -export class VirtualizationComponent implements OnInit { - - public readonly virtualizationsLink = DataservicesConstants.dataservicesRootPath; - - public viewPropertyForm: FormGroup; - public nameValidationError = ""; - public viewDefinitions: ViewDefinition[] = []; - public selectedViewDefinitions: ViewDefinition[] = []; - public viewCreateInProgress = false; - public showDescription = false; - - private selectionService: SelectionService; - private dataserviceService: DataserviceService; - private modalService: BsModalService; - private vdbService: VdbService; - private router: Router; - private logger: LoggerService; - private noViewsConfig: EmptyStateConfig; - private enterNameConfig: EmptyStateConfig; - private saveNameConfig: EmptyStateConfig; - private currentVirtualization: Dataservice = null; - private originalName: string; - private newVirtualization: NewDataservice = null; - private viewsLoadingState: LoadingState = LoadingState.LOADING; - - constructor( selectionService: SelectionService, dataserviceService: DataserviceService, - vdbService: VdbService, modalService: BsModalService, router: Router, logger: LoggerService ) { - this.selectionService = selectionService; - this.dataserviceService = dataserviceService; - this.vdbService = vdbService; - this.modalService = modalService; - this.router = router; - this.logger = logger; - this.createViewPropertyForm(); - } - - public ngOnInit(): void { - // If there is a virtualization selection, edit it. Otherwise create a new virtualization - if (this.selectionService.hasSelectedVirtualization) { - this.currentVirtualization = this.selectionService.getSelectedVirtualization(); - this.originalName = this.currentVirtualization.getId(); - this.initForm(this.currentVirtualization.getId(), this.currentVirtualization.getDescription(), false); - // Init views - this.initViews(); - } else { - this.originalName = ""; - this.newVirtualization = this.dataserviceService.newDataserviceInstance(this.originalName, ""); - this.initForm(this.newVirtualization.getId(), this.newVirtualization.getDescription(), true); - // Init Views - this.viewDefinitions = []; - this.viewsLoadingState = LoadingState.LOADED_VALID; - } - } - - /** - * Get new virtualization status - * @returns {boolean} true if the virtualization has not yet been named - */ - public get isNew( ): boolean { - return this.newVirtualization && this.newVirtualization !== null; - } - - /** - * Get the virtualization view definitions - * @returns {ViewDefinition[]} the view definitions - */ - public get allViewDefinitions( ): ViewDefinition[] { - return this.viewDefinitions; - } - - /** - * Determine if the views are loading - */ - public get viewsLoading( ): boolean { - return ( this.viewsLoadingState === LoadingState.LOADING ); - } - - /** - * Determine if view loading finished successfully - */ - public get viewsLoadedSuccess( ): boolean { - return ( this.viewsLoadingState === LoadingState.LOADED_VALID ); - } - - /** - * Determine if view loading finished but with error - */ - public get viewsLoadedFailed( ): boolean { - return ( this.viewsLoadingState === LoadingState.LOADED_INVALID ); - } - - /** - * Determine if the virtualization has a pending name change - * @returns {boolean} 'true' if pending name change - */ - public get hasPendingNameChange( ): boolean { - return this.originalName !== this.viewPropertyForm.controls["name"].value.toString(); - } - - /** - * Save the dataservice using the current name value. This cannot be invoked unless there are pending changes, - * and the name is valid. - */ - public onSaveName( ): void { - const theName = this.viewPropertyForm.controls["name"].value.toString(); - let theDescription = ""; - const descr = this.viewPropertyForm.controls["description"].value; - if (descr != null) { - theDescription = descr.toString(); - } - - // If this is a brand new dataservice, create it - if (this.isNew) { - const self = this; - this.viewCreateInProgress = true; - this.newVirtualization.setId(theName); - this.newVirtualization.setDescription(theDescription); - this.dataserviceService - .createDataservice(this.newVirtualization) - .subscribe( - (wasSuccess) => { - // After create of dataservice, remove 'newVirtualization' - self.newVirtualization = null; - // Set the current virtualization to the newly created virtualization - self.selectDataservice(theName); - }, - (error) => { - self.logger.error("[VirtualizationComponent] Error creating virtualization: %o", error); - self.viewCreateInProgress = false; - } - ); - // Existing dataservice - update it - } else { - // TODO: Determine action for rename of existing - } - } - - /** - * Handler for dataservice name changes. - * @param {AbstractControl} input - */ - public handleNameChanged( input: AbstractControl ): void { - const self = this; - - this.dataserviceService.isValidName( input.value ).subscribe( - ( errorMsg ) => { - if ( errorMsg ) { - // only update if error has changed - if ( errorMsg !== self.nameValidationError ) { - self.nameValidationError = errorMsg; - } - self.setViewDefinitionsEditableState(false); - } else { // name is valid - self.nameValidationError = ""; - if (self.hasPendingNameChange) { - self.setViewDefinitionsEditableState(false); - } else { - self.setViewDefinitionsEditableState(true); - } - } - }, - ( error ) => { - self.logger.error( "[handleNameChanged] Error: %o", error ); - } ); - } - - /* - * Return the name valid state - */ - public get nameValid(): boolean { - return this.nameValidationError == null || this.nameValidationError.length === 0; - } - - /** - * The configuration for empty state (no views) - * @returns {EmptyStateConfig} the empty state config - */ - public get viewsEmptyConfig(): EmptyStateConfig { - if ( !this.noViewsConfig ) { - const actionConfig = { - primaryActions: [ - { - id: "createViewActionId", - title: "Add View", - tooltip: "Add a view" - } - ] - } as ActionConfig; - - this.noViewsConfig = { - actions: actionConfig, - iconStyleClass: "pficon-warning-triangle-o", - info: "No views are defined for this virtualization. Please click below to create a view.", - title: "No Views Defined" - } as EmptyStateConfig; - } - - return this.noViewsConfig; - } - - /** - * Empty state config which prompts the user to name the virtualization. - * @returns {EmptyStateConfig} the empty state config - */ - public get enterVirtualizationNameConfig(): EmptyStateConfig { - if ( !this.enterNameConfig ) { - this.enterNameConfig = { - iconStyleClass: "pficon-warning-triangle-o", - info: "Please enter a name for the virtualization", - title: "Enter Virtualization Name" - } as EmptyStateConfig; - } - - return this.enterNameConfig; - } - - /** - * Empty state config which prompts the user to save the virtualization. - * @returns {EmptyStateConfig} the empty state config - */ - public get saveVirtualizationNameConfig(): EmptyStateConfig { - if ( !this.saveNameConfig ) { - this.saveNameConfig = { - iconStyleClass: "pficon-warning-triangle-o", - info: "Click save icon to save the virtualization name", - title: "Save Virtualization Name" - } as EmptyStateConfig; - } - - return this.saveNameConfig; - } - - /** - * Handle Delete of the specified View - * @param {string} viewName - */ - public onDelete(viewName: string): void { - // Dialog Content - const message = "Do you really want to delete View '" + viewName + "'?"; - const initialState = { - title: "Confirm Delete", - bodyContent: message, - cancelButtonText: "Cancel", - confirmButtonText: "Delete" - }; - - // Show Dialog, act upon confirmation click - const modalRef = this.modalService.show(ConfirmDialogComponent, {initialState}); - modalRef.content.confirmAction.take(1).subscribe((value) => { - this.onDeleteView(viewName); - }); - } - - /** - * Handle request for new View - */ - public onNew(): void { - // Setting the selected view definition null indicates new view - this.selectionService.setSelectedViewDefinition( this.currentVirtualization, null ); - - const link: string[] = [ DataservicesConstants.viewPath ]; - this.logger.debug("[VirtualizationComponent] Navigating to: %o", link); - this.router.navigate(link).then(() => { - // nothing to do - }); - } - - /** - * Handle Edit of the specified View - * @param {string} viewName - */ - public onEdit(viewName: string): void { - // Sets the selected view in the service - const selectedViewDefn = this.viewDefinitions.find((x) => x.getName() === viewName); - this.selectionService.setSelectedViewDefinition( this.currentVirtualization, selectedViewDefn ); - - const link: string[] = [ DataservicesConstants.viewPath ]; - this.logger.debug("[VirtualizationComponent] Navigating to: %o", link); - this.router.navigate(link).then(() => { - // nothing to do - }); - } - - public onSelected( viewDefn: ViewDefinition ): void { - this.selectionService.setSelectedViewDefinition( this.currentVirtualization, viewDefn ); - } - - // ---------------- - // Private Methods - // ---------------- - - /** - * Deletes the specified ViewEditorState from the userProfile, and removes ViewDefinition from the current list. - * @param {string} viewDefnName the name of the view - */ - private onDeleteView(viewDefnName: string): void { - const selectedViewDefn = this.viewDefinitions.find((x) => x.getName() === viewDefnName); - const vdbName = this.currentVirtualization.getServiceVdbName(); - const editorStateId = vdbName.toLowerCase() + "." + viewDefnName; - const dataserviceName = this.currentVirtualization.getId(); - // Note: we can only doDelete selected items that we can see in the UI. - this.logger.debug("[VirtualizationComponent] Deleting selected Virtualization View."); - const self = this; - this.dataserviceService - .deleteViewEditorStateRefreshViews(editorStateId, dataserviceName) - .subscribe( - (wasSuccess) => { - self.removeViewDefinitionFromList(selectedViewDefn); - }, - (error) => { - self.logger.error("[VirtualizationComponent] Error deleting the editor state: %o", error); - } - ); - } - - /* - * Creates the view property form - */ - private createViewPropertyForm(): void { - // New Virtualization is allowed to edit the name - handle name changes - if (!this.selectionService.hasSelectedVirtualization) { - this.viewPropertyForm = new FormGroup({ - name: new FormControl( "", this.handleNameChanged.bind( this ) ), - description: new FormControl("") - }); - // Responds to basic property changes - updates the page status - this.viewPropertyForm.valueChanges.subscribe((val) => { - // this.updatePage2aValidStatus( ); - }); - // Edit Virtualization is not allowed to edit the name - } else { - this.viewPropertyForm = new FormGroup({ - name: new FormControl( "" ), - description: new FormControl("") - }); - } - } - - /* - * Init the form values - * @param {string} name the dataservice name - * @param {string} description the dataservice description - * @param {boolean} nameEditable 'true' if can edit the name - */ - private initForm(name: string, descr: string, nameEditable: boolean): void { - if (!nameEditable) { - this.viewPropertyForm.get("name").disable(); - } - this.viewPropertyForm.controls["name"].setValue(name); - this.viewPropertyForm.controls["description"].setValue(descr); - } - - /* - * Select the specified Dataservice. - * @param {string} dsName the name of the dataservice - */ - private selectDataservice(dsName: string): void { - const self = this; - this.dataserviceService - .getAllDataservices() - .subscribe( - (dataservices) => { - for (const ds of dataservices) { - if (ds.getId() === dsName) { - self.currentVirtualization = ds; - self.dataserviceService.setSelectedDataservice(ds); - self.originalName = this.currentVirtualization.getId(); - self.initForm(this.currentVirtualization.getId(), this.currentVirtualization.getDescription(), false); - } - } - self.viewCreateInProgress = false; - }, - (error) => { - self.logger.error("[VirtualizationComponent] Error selecting the virtualization: %o", error); - self.viewCreateInProgress = false; - } - ); - } - - /* - * Initialize the views for the current dataservice. Makes a rest call to get the ViewEditorStates for the serviceVdb - */ - private initViews( ): void { - this.viewsLoadingState = LoadingState.LOADING; - const vdbName = this.currentVirtualization.getServiceVdbName(); - const statesPattern = vdbName.toLowerCase() + "*"; - this.setViewDefinitionsEditableState(true); - - const self = this; - this.dataserviceService - .getViewEditorStates(statesPattern) - .subscribe( - (viewEditorStates) => { - const viewDefns: ViewDefinition[] = []; - for ( const viewState of viewEditorStates ) { - const viewDefn = viewState.getViewDefinition(); - if ( viewDefn ) { - viewDefns.push( viewDefn ); - } - } - self.viewDefinitions = viewDefns.sort( (left, right): number => { - if (left.getName() < right.getName()) return -1; - if (left.getName() > right.getName()) return 1; - return 0; - }); - self.setViewDefinitionsEditableState(true); - this.viewsLoadingState = LoadingState.LOADED_VALID; - }, - (error) => { - self.logger.error("[VirtualizationComponent] Error updating the views for the virtualization: %o", error); - self.viewCreateInProgress = false; - this.viewsLoadingState = LoadingState.LOADED_INVALID; - } - ); - } - - /* - * Set the editable state of all view definitions - * @param {boolean} isEditable the editable state - */ - private setViewDefinitionsEditableState(isEditable: boolean): void { - for (const viewDefn of this.viewDefinitions) { - viewDefn.setEditable(isEditable); - } - } - - /* - * Remove the specified ViewDefinition from the list of view definitions - * @param {ViewDefinition} viewDefn the view definition to remove - */ - private removeViewDefinitionFromList(viewDefn: ViewDefinition): void { - this.viewDefinitions.splice(this.viewDefinitions.indexOf(viewDefn), 1); - } - -} diff --git a/ngapp/src/app/shared/confirm-dialog/confirm-dialog.component.ts b/ngapp/src/app/shared/confirm-dialog/confirm-dialog.component.ts index 29b4253c..6f52c40d 100644 --- a/ngapp/src/app/shared/confirm-dialog/confirm-dialog.component.ts +++ b/ngapp/src/app/shared/confirm-dialog/confirm-dialog.component.ts @@ -27,6 +27,7 @@ import { BsModalRef } from "ngx-bootstrap"; export class ConfirmDialogComponent implements OnInit { @Output() public confirmAction = new EventEmitter(); + @Output() public cancelAction = new EventEmitter(); public title = "Title"; public bodyContent = "Confirmation Message"; @@ -48,6 +49,7 @@ export class ConfirmDialogComponent implements OnInit { } public onCancelSelected(): void { + this.cancelAction.emit(true); this.bsModalRef.hide(); } diff --git a/ngapp/src/app/shared/test-data.service.ts b/ngapp/src/app/shared/test-data.service.ts index a77501a9..62145d16 100644 --- a/ngapp/src/app/shared/test-data.service.ts +++ b/ngapp/src/app/shared/test-data.service.ts @@ -791,9 +791,7 @@ export class TestDataService { "serviceVdbName": TestDataService.accountsVdb.getName(), "serviceVdbVersion": TestDataService.accountsVdb.getVersion(), "serviceViewModel": "views", - "serviceViews": [ - "tbl1View", - "tbl2View" + "serviceViewDefinitions": [ ], "serviceViewTables": [ "connection=" + TestDataService.conn1.getId().toLowerCase() + "/schema=public/table=tbl1", @@ -837,11 +835,9 @@ export class TestDataService { "serviceVdbName": TestDataService.employeesVdb.getName(), "serviceVdbVersion": TestDataService.employeesVdb.getVersion(), "serviceViewModel": "views", - "serviceViews": [ - "tbl1View", - "tbl2View", - "tbl3View", - "tbl4View" + "serviceViewDefinitions": [ + "employeesView1", + "employeesView2" ], "serviceViewTables": [ "connection=" + TestDataService.conn2.getId().toLowerCase() + "/schema=public/table=tbl1", @@ -883,17 +879,14 @@ export class TestDataService { "keng__dataPath": "/tko:komodo/tko:workspace/admin/Products", "keng__kType": "Dataservice", "keng__hasChildren": true, - "tko__description": "A dataservice for products.", + "tko__description": "A dataservice for products. Make this a much longer description, to see what happens... Make this a much longer description, to see what happens... Make this a much longer description, to see what happens... Make this a much longer description, to see what happens... Make this a much longer description, to see what happens... Make it even longer now and longer", "serviceVdbName": TestDataService.productsVdb.getName(), "serviceVdbVersion": TestDataService.productsVdb.getVersion(), "serviceViewModel": "views", - "serviceViews": [ - "tbl1View", - "tbl2View", - "tbl3View", - "tbl4View", - "tbl5View", - "tbl6View" + "serviceViewDefinitions": [ + "productsView1", + "productsView2", + "productsView3" ], "serviceViewTables": [ "connection=" + TestDataService.conn3.getId().toLowerCase() + "/schema=public/table=tbl1",