diff --git a/ngapp/src/app/activities/shared/mock-activity.service.ts b/ngapp/src/app/activities/shared/mock-activity.service.ts index cac16331..03304dcc 100644 --- a/ngapp/src/app/activities/shared/mock-activity.service.ts +++ b/ngapp/src/app/activities/shared/mock-activity.service.ts @@ -52,15 +52,13 @@ export class MockActivityService extends ActivityService { this.newAct1.setName("newActivity1"); const srcConn = new NewConnection(); srcConn.setName("new1Src"); - srcConn.setJndiName("new1SrcJndi"); - srcConn.setDriverName("new1SrcDriver"); - srcConn.setJdbc(true); + srcConn.setDescription("new1SrcDescription"); + srcConn.setServiceCatalogSource("new1SvcCatSrc"); this.newAct1.setSourceConnection(srcConn); const tgtConn = new NewConnection(); tgtConn.setName("new1Tgt"); - tgtConn.setJndiName("new1TgtJndi"); - tgtConn.setDriverName("new1TgtDriver"); - tgtConn.setJdbc(false); + tgtConn.setDescription("new1TgtDescription"); + tgtConn.setServiceCatalogSource("new1SvcCatSrc"); this.newAct1.setTargetConnection(tgtConn); } diff --git a/ngapp/src/app/connections/add-connection-wizard/add-connection-wizard.component.html b/ngapp/src/app/connections/add-connection-wizard/add-connection-wizard.component.html index db4d0ce1..d7ce7ccd 100644 --- a/ngapp/src/app/connections/add-connection-wizard/add-connection-wizard.component.html +++ b/ngapp/src/app/connections/add-connection-wizard/add-connection-wizard.component.html @@ -4,14 +4,14 @@ (onNext)="nextClicked($event)" (onStepChange)="stepChanged($event)"> - + -
+
- -
+ +

@@ -19,120 +19,106 @@

-

+

Could not load the connection types. Please Try relaunching the wizard or check the console log.

-

{{ step1InstructionMessage }}

-
-
- -
- -
{{ getBasicPropertyErrorMessage("driver") }}
-
-
- -
- -
- -
{{ getBasicPropertyErrorMessage("name") }}
-
-
-
- -
- -
{{ getBasicPropertyErrorMessage("jndi") }}
-
-
-
+

{{ step1InstructionMessage }}

+ + - - - + + + -
-
-
- -
-
-

- - Step Initialization Error -

+ + + +
+
-
-

- Could not load the detail properties. Please Try relaunching the wizard or check the console log. -

+
+ +
-
-

{{ step2InstructionMessage }}

-
- -
- - - - - - - -

{{ step3InstructionMessage }}

-

Connection Properties:

-
-
- - -
-
- - -
-
- - -
-
-

Connection Detail Required Properties:

-
-
-
- - +
+

{{ step2InstructionMessage }}

+ +
+ +
+ +
{{ nameValidationError }}
+
-
- +
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+ +
{{ selectServiceCatalogSourceErrorMsg }}
+
+
+ +
+
+ +
- - + +
-

Creation in progress

-

The connection is being created.

+

{{ finalPageTitle }}

+

{{ finalPageMessage }}

-

Creation was successful

-

The connection was created successfully. Click on the button to see all connections.

+

{{ finalPageTitle }}

+

{{ finalPageMessage }}

View All Connections
-

Creation failed

-

The connection creation failed. Correct any properties and retry.

+

{{ finalPageTitle }}

+
+ {{ finalPageMessage }} +
+
{{ errorDetails }}
+
diff --git a/ngapp/src/app/connections/add-connection-wizard/add-connection-wizard.component.spec.ts b/ngapp/src/app/connections/add-connection-wizard/add-connection-wizard.component.spec.ts index 44f83e18..225e2d8a 100644 --- a/ngapp/src/app/connections/add-connection-wizard/add-connection-wizard.component.spec.ts +++ b/ngapp/src/app/connections/add-connection-wizard/add-connection-wizard.component.spec.ts @@ -2,11 +2,14 @@ import { async, ComponentFixture, TestBed } from "@angular/core/testing"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { RouterTestingModule } from "@angular/router/testing"; +import { ConnectionTypeCardComponent } from "@connections/connection-type-cards/connection-type-card/connection-type-card.component"; +import { ConnectionTypeCardsComponent } from "@connections/connection-type-cards/connection-type-cards.component"; import { ConnectionService } from "@connections/shared/connection.service"; import { MockConnectionService } from "@connections/shared/mock-connection.service"; import { AppSettingsService } from "@core/app-settings.service"; import { CoreModule } from "@core/core.module"; import { MockAppSettingsService } from "@core/mock-app-settings.service"; +import { WizardService } from "@dataservices/shared/wizard.service"; import { PropertyFormPropertyComponent } from "@shared/property-form/property-form-property/property-form-property.component"; import { PropertyFormComponent } from "@shared/property-form/property-form.component"; import { PatternFlyNgModule } from "patternfly-ng"; @@ -19,8 +22,10 @@ describe("AddConnectionWizardComponent", () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ CoreModule, FormsModule, PatternFlyNgModule, ReactiveFormsModule, RouterTestingModule ], - declarations: [ AddConnectionWizardComponent, PropertyFormComponent, PropertyFormPropertyComponent ], + declarations: [ AddConnectionWizardComponent, PropertyFormComponent, PropertyFormPropertyComponent, + ConnectionTypeCardComponent, ConnectionTypeCardsComponent ], providers: [ + WizardService, { provide: AppSettingsService, useClass: MockAppSettingsService }, { provide: ConnectionService, useClass: MockConnectionService }, ] 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 9fd61db0..9256c393 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 @@ -24,19 +24,18 @@ import { import { FormControl, FormGroup } from "@angular/forms"; import { AbstractControl } from "@angular/forms"; -import { Validators } from "@angular/forms"; import { Router } from "@angular/router"; +import { ConnectionType } from "@connections/shared/connection-type.model"; import { ConnectionService } from "@connections/shared/connection.service"; import { ConnectionsConstants } from "@connections/shared/connections-constants"; import { NewConnection } from "@connections/shared/new-connection.model"; -import { TemplateDefinition } from "@connections/shared/template-definition.model"; +import { ServiceCatalogSource } from "@connections/shared/service-catalog-source.model"; import { LoggerService } from "@core/logger.service"; -import { PropertyDefinition } from "@shared/property-form/property-definition.model"; -import { PropertyFormComponent } from "@shared/property-form/property-form.component"; -import { WizardComponent } from "patternfly-ng"; -import { WizardEvent } from "patternfly-ng"; +import { WizardService } from "@dataservices/shared/wizard.service"; +import { NotificationType, WizardEvent } from "patternfly-ng"; import { WizardStepConfig } from "patternfly-ng"; import { WizardConfig } from "patternfly-ng"; +import { WizardComponent } from "patternfly-ng"; @Component({ encapsulation: ViewEncapsulation.None, @@ -46,45 +45,56 @@ import { WizardConfig } from "patternfly-ng"; }) export class AddConnectionWizardComponent implements OnInit { public readonly connectionSummaryLink: string = ConnectionsConstants.connectionsRootPath; + public emptyServiceCatalogSource = new ServiceCatalogSource(); // a bogus service catalog source used in drop down to give instructions + public readonly selectServiceCatalogSourceErrorMsg = "A service catalog source must be selected"; // Wizard Config public wizardConfig: WizardConfig; - public basicPropertyForm: FormGroup; + public connectionBasicPropertyForm: FormGroup; public createComplete = true; public createSuccessful = false; - public detailPropertiesLoading = true; - public detailPropertiesLoadSuccess = false; - public detailPropertiesLoadedType = ""; - public requiredPropValues: Array<[string, string]> = []; - public templatesLoading = true; - public templatesLoadSuccess = false; + public connectionTypesLoading = true; + public connectionTypesLoadSuccess = false; + public serviceCatalogSourcesLoading = true; + public serviceCatalogSourcesLoadSuccess = false; + public nameValidationError = ""; + public selectedServiceCatalogSource: ServiceCatalogSource; // Wizard Step 1 public step1Config: WizardStepConfig; // Wizard Step 2 public step2Config: WizardStepConfig; - - // Wizard Step 3 - public step3Config: WizardStepConfig; - public step3aConfig: WizardStepConfig; - public step3bConfig: WizardStepConfig; + public step2aConfig: WizardStepConfig; + public step2bConfig: WizardStepConfig; + public noSourcesNotificationDismissable = false; + public noSourcesNotificationHeader = "No Sources Available"; + public noSourcesNotificationMessage = "No Sources of the correct type to select. "; + public noSourcesNotificationType = NotificationType.DANGER; @ViewChild("wizard") public wizard: WizardComponent; - @ViewChild(PropertyFormComponent) public detailPropForm: PropertyFormComponent; private connectionService: ConnectionService; - private allTemplates: TemplateDefinition[] = []; - private detailProperties: Array> = []; + private wizardService: WizardService; + private selectedConnTypes: ConnectionType[] = []; + private connTypes: ConnectionType[] = []; + private serviceCatSources: ServiceCatalogSource[] = []; private logger: LoggerService; private router: Router; + private errorDetailMessage: string; + private theFinalPageTitle = ""; + private theFinalPageMessage = ""; - constructor( router: Router, connectionService: ConnectionService, logger: LoggerService ) { + constructor( router: Router, connectionService: ConnectionService, + wizardService: WizardService, logger: LoggerService ) { this.connectionService = connectionService; + this.wizardService = wizardService; this.router = router; this.logger = logger; - this.createBasicPropertyForm(); + this.emptyServiceCatalogSource.setId( " -- select catalog source -- " ); + this.selectedServiceCatalogSource = this.emptyServiceCatalogSource; + this.createConnectionBasicPropertyForm(); } /* @@ -95,35 +105,27 @@ export class AddConnectionWizardComponent implements OnInit { this.step1Config = { id: "step1", priority: 0, - title: "Basic Properties", + title: "Connection Type", allowClickNav: false } as WizardStepConfig; - // Step 2 - Advanced Properties + // Step 3 - Review and Create this.step2Config = { id: "step2", priority: 0, - title: "Detail Properties", + title: "Connection Definition", allowClickNav: false } as WizardStepConfig; - - // Step 3 - Review and Create - this.step3Config = { - id: "step3", + this.step2aConfig = { + id: "step2a", priority: 0, - title: "Review and Create", + title: "Define", allowClickNav: false } as WizardStepConfig; - this.step3aConfig = { - id: "step3a", - priority: 0, - title: "Review", - allowClickNav: false - } as WizardStepConfig; - this.step3bConfig = { - id: "step3b", + this.step2bConfig = { + id: "step2b", priority: 1, - title: "Create", + title: "Review", allowClickNav: false } as WizardStepConfig; @@ -137,24 +139,22 @@ export class AddConnectionWizardComponent implements OnInit { done: false } as WizardConfig; - // Load the templates for the first step - this.templatesLoading = true; - this.templatesLoadSuccess = true; - const self = this; - this.connectionService - .getConnectionTemplates() - .subscribe( - (templates) => { - self.allTemplates = templates; - self.templatesLoading = false; - self.templatesLoadSuccess = true; - }, - (error) => { - self.logger.error("[AddConnectionWizardComponent] Error getting templates: %o", error); - self.templatesLoading = false; - self.templatesLoadSuccess = false; + // Load the available connection types for the first step + this.connTypes = this.connectionService.getConnectionTypes(); + this.connectionTypesLoadSuccess = true; + this.connectionTypesLoading = false; + + // Select connection type if editing + if (this.wizardService.isEdit()) { + const selectedConnection = this.wizardService.getSelectedConnection(); + const connType = selectedConnection.getDriverName(); + for (const cType of this.connectionTypes) { + if (connType === cType.getName()) { + this.onConnectionTypeSelected(cType); + break; } - ); + } + } this.setNavAway(false); } @@ -162,61 +162,128 @@ export class AddConnectionWizardComponent implements OnInit { // ---------------- // Public Methods // ---------------- + + public handleNameChanged( input: AbstractControl ): void { + const self = this; + + this.connectionService.isValidName( 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.updatePage2ValidStatus(); + }, + ( error ) => { + self.logger.error( "[handleNameChanged] Error: %o", error ); + } ); + } + /* * Return the name valid state */ public get nameValid(): boolean { - return this.basicPropertyForm.controls["name"].valid; + if (this.wizardService.isEdit()) { + return true; + } + return this.nameValidationError == null || this.nameValidationError.length === 0; } - /* - * Return the driver valid state + /** + * Handles service catalog source change + * @param {string} newValue the new serviceCatalog source */ - public get driverValid(): boolean { - return this.basicPropertyForm.controls["driver"].valid; + public selectedServiceCatalogSourceChanged( newValue ): void { + this.selectedServiceCatalogSource = this.serviceCatSources.find((src) => src.getId() === newValue); + this.updatePage2ValidStatus(); } /* - * Return the jndi valid state + * Determine if there are one or more service catalog sources */ - public get jndiValid(): boolean { - return this.basicPropertyForm.controls["jndi"].valid; + public get hasServiceCatalogSources(): boolean { + return this.serviceCatSources.length > 0; } - /* - * Step 1 instruction message + /** + * @returns {boolean} `true` if a service catalog source has been selected */ - public get step1InstructionMessage(): string { - if (!this.driverValid) { - return "Please select a Connection type"; - } else if (!this.nameValid) { - return "Please enter a name for the Connection"; - } else if (!this.jndiValid) { - return "Please enter a JNDI identifier for the Connection"; - } else { - return "When finished entering properties, click Next to continue"; + public get hasSelectedServiceCatalogSource(): boolean { + return ( this.selectedServiceCatalogSource != null ) && ( this.selectedServiceCatalogSource !== this.emptyServiceCatalogSource ); + } + + /** + * @returns {boolean} `true` if the serviceCatalogSource is selected, but type is different than + * the selected connection type. + */ + public get serviceCatalogSelectedWrongType(): boolean { + if (!this.hasSelectedServiceCatalogSource) { + return false; } + const selectedSvcCatSourceType = this.selectedServiceCatalogSource.getType(); + const selectedConnType = this.selectedConnTypes[0]; + + return selectedConnType.getName() !== selectedSvcCatSourceType; + } + + /** + * Gets the Title to be displayed on the final wizard page + * @returns {string} + */ + public get finalPageTitle(): string { + return this.theFinalPageTitle; + } + + /** + * Gets the message to be displayed on the final wizard page + * @returns {string} + */ + public get finalPageMessage(): string { + return this.theFinalPageMessage; + } + + /** + * @returns {string} the error details message + */ + public get errorDetails(): string { + return this.errorDetailMessage; } /* - * Step 2 instruction message + * Step 1 instruction message */ - public get step2InstructionMessage(): string { - return "Enter advanced properties for the Connection, then click Next to continue"; + public get step1InstructionMessage(): string { + return "Please select a Connection type"; } /* - * Step 3 instruction message + * Step 2 instruction message */ - public get step3InstructionMessage(): string { - return "Review your entries. When finished, click Create to create the Connection"; + public get step2InstructionMessage(): string { + if (this.serviceCatSources.length === 0) { + return "No sources available"; + } else if (!this.nameValid) { + return "Please enter a name for the Connection"; + } else if (!this.hasSelectedServiceCatalogSource) { + return "Please select a catalog source for the Connection"; + } else { + if (this.wizardService.isEdit()) { + return "Review selections. Click Update to update the Connection"; + } else { + return "When finished, click Create to create the Connection"; + } + } } /* * Return the name error message if invalid */ - public getBasicPropertyErrorMessage( name: string ): string { - const control: AbstractControl = this.basicPropertyForm.controls[name]; + public getConnectionBasicPropertyErrorMessage( name: string ): string { + const control: AbstractControl = this.connectionBasicPropertyForm.controls[name]; if (control.invalid) { // The first error found is returned if (control.errors.required) { @@ -227,23 +294,53 @@ export class AddConnectionWizardComponent implements OnInit { } /* - * Return the array of template names + * Return the array of ConnectionTypes */ - public get templateNames(): string[] { - const templateNames: string[] = []; - for ( const templ of this.allTemplates ) { - templateNames.push(templ.getId()); - } - return templateNames.sort(); + public get connectionTypes(): ConnectionType[] { + return this.connTypes; + } + + /* + * Return the currently selected ConnectionType + */ + public get selectedConnectionTypes(): ConnectionType[] { + return this.selectedConnTypes; + } + + /** + * Handles connection type selection + * @param {ConnectionType} connectionType the connection type + */ + public onConnectionTypeSelected(connectionType: ConnectionType): void { + // Only allow one item to be selected + this.selectedConnTypes.shift(); + this.selectedConnTypes.push(connectionType); + // Selecting type clears the serviceCatalog source selection + this.selectedServiceCatalogSource = this.emptyServiceCatalogSource; + this.updatePage1ValidStatus(); + } + + /** + * Handles connection type de-selection + * @param {ConnectionType} connectionType the connection type + */ + public onConnectionTypeDeselected(connectionType: ConnectionType): void { + // Only one item is selected at a time + this.selectedConnTypes.shift(); + } + + /* + * Return the array of ServiceCatalogSources + */ + public get serviceCatalogSources(): ServiceCatalogSource[] { + return this.serviceCatSources; } public nextClicked($event: WizardEvent): void { - // When leaving page 1, load the driver-specific property definitions + // When leaving page 1, load the available service catalog sources for the selected type if ($event.step.config.id === "step1") { - const selectedDriver = this.basicPropertyForm.controls["driver"].value; - if (!this.detailPropertiesLoadSuccess || (this.detailPropertiesLoadedType !== selectedDriver)) { - this.loadPropertyDefinitions(selectedDriver); - } + // load the available catalog sources + this.loadServiceCatalogSources(this.selectedConnTypes[0]); } } @@ -255,14 +352,6 @@ export class AddConnectionWizardComponent implements OnInit { }); } - /* - * Return the array of [name,value] for the required properties. - * So that the current required property entries can be shown on page 3 (review) - */ - public get requiredPropertyValues(): Array<[string, string]> { - return this.requiredPropValues; - } - /* * Create the Connection via komodo REST interface, * using the currently entered properties @@ -273,189 +362,261 @@ export class AddConnectionWizardComponent implements OnInit { const connection: NewConnection = new NewConnection(); - // Connection basic properties from step 1 + // Connection basic properties connection.setName(this.connectionName); - connection.setJndiName(this.connectionJndiName); - connection.setDriverName(this.connectionDriverName); - connection.setJdbc(this.isJdbc); - - // Connection advanced properties from step 2 - const propMap: Map = this.detailPropForm.propertyValuesNonDefault; - connection.setProperties(propMap); + connection.setDescription(this.connectionDescription); + connection.setServiceCatalogSource(this.selectedServiceCatalogSource.getId()); const self = this; - this.connectionService - .createAndDeployConnection(connection) - .subscribe( - (wasSuccess) => { - self.createComplete = true; - self.createSuccessful = wasSuccess; - self.step3bConfig.nextEnabled = false; - }, - (error) => { - self.logger.error("[AddConnectionWizardComponent] Error: %o", error); - self.createComplete = true; - self.createSuccessful = false; - self.step3bConfig.nextEnabled = false; - } - ); + if (this.wizardService.isEdit()) { + this.updateAndBindConnection(connection); + } else { + this.createAndBindConnection(connection); + } } public stepChanged($event: WizardEvent): void { if ($event.step.config.id === "step1") { + this.wizardConfig.nextTitle = "Next >"; this.updatePage1ValidStatus(); - } else if ($event.step.config.id === "step3a") { - this.wizardConfig.nextTitle = "Create"; - this.updateRequiredPropertyValues(); - } else if ($event.step.config.id === "step3b") { - // Note: The next button is not disabled by default when wizard is done - this.step3Config.nextEnabled = false; + } else if ($event.step.config.id === "step2a") { + if (this.wizardService.isEdit()) { + this.wizardConfig.nextTitle = "Update"; + } else { + this.wizardConfig.nextTitle = "Create"; + } + this.updatePage2ValidStatus(); + } else if ($event.step.config.id === "step2b") { + this.step2Config.nextEnabled = false; } else { this.wizardConfig.nextTitle = "Next >"; } } - /** - * Handler for property form initialization - * @param {boolean} isValid form valid state - */ - public onDetailPropertyInit(isValid: boolean): void { - this.updatePage2ValidStatus(isValid); - } - - /** - * Handler for property form changes - * @param {boolean} isValid form valid state - */ - public onDetailPropertyChanged(isValid: boolean): void { - this.updatePage2ValidStatus(isValid); - } - /** * @returns {string} the name of the connection */ public get connectionName(): string { - return this.basicPropertyForm.controls["name"].value; - } - - /** - * @returns {string} the driver name of the connection - */ - public get connectionDriverName(): string { - return this.basicPropertyForm.controls["driver"].value; - } - - /** - * @returns {string} the JNDI name of the connection - */ - public get connectionJndiName(): string { - return this.basicPropertyForm.controls["jndi"].value; + return this.connectionBasicPropertyForm.controls["name"].value; } /** - * @returns {boolean} 'true' if connection is JDBC + * @returns {string} the description of the connection */ - public get isJdbc(): boolean { - const driver = this.connectionDriverName; - let jdbc = true; - // TODO: this needs to be a hooked up to komodo call instead - if (driver === null || driver === "cassandra" || driver === "file" || driver === "google" - || driver === "ldap" || driver === "mongodb" || driver === "salesforce" - || driver === "salesforce-34" || driver === "solr" || driver === "webservice") { - jdbc = false; - } - return jdbc; + public get connectionDescription(): string { + return this.connectionBasicPropertyForm.controls["description"].value; } // ---------------- // Private Methods // ---------------- - /* - * Create the BasicProperty form (page 1) + /** + * Create the Connection Basic properties form */ - private createBasicPropertyForm(): void { - this.basicPropertyForm = new FormGroup({ - name: new FormControl("", Validators.required), - jndi: new FormControl("", Validators.required), - driver: new FormControl("", Validators.required) + private createConnectionBasicPropertyForm(): void { + this.connectionBasicPropertyForm = new FormGroup({ + name: new FormControl( "", this.handleNameChanged.bind( this ) ), + description: new FormControl(""), }); + + // Initialize form values + if (!this.wizardService.isEdit()) { + this.connectionBasicPropertyForm.controls["name"].setValue(null); + this.connectionBasicPropertyForm.controls["description"].setValue(null); + } else { + const selectedConnection = this.wizardService.getSelectedConnection(); + this.connectionBasicPropertyForm.controls["name"].setValue(selectedConnection.name); + this.connectionBasicPropertyForm.controls["description"].setValue(selectedConnection.getDescription()); + this.connectionBasicPropertyForm.get("name").disable(); + } + // Responds to basic property changes - updates the page status - const self = this; - this.basicPropertyForm.valueChanges.subscribe((val) => { - self.updatePage1ValidStatus( ); + this.connectionBasicPropertyForm.valueChanges.subscribe((val) => { + this.updatePage2ValidStatus( ); }); } + /** + * Load the available service catalog sources for the supplied connection type + * @param {ConnectionType} connType the connection type + */ + private loadServiceCatalogSources(connType: ConnectionType): void { + // Load the available service catalog sources for the second step + this.serviceCatalogSourcesLoading = true; + this.serviceCatalogSourcesLoadSuccess = false; + + const self = this; + this.connectionService + .getAllServiceCatalogSources() + .subscribe( + (sources) => { + // Only keep the service catalog sources whose type matches the connectionType. empty source is always included. + self.serviceCatSources = []; + for ( const source of sources ) { + if ( source.getType() === connType.getName() ) { + self.serviceCatSources.push(source); + } + } + + // Edit mode select the service catalog source + if (self.wizardService.isEdit() && (!self.hasSelectedServiceCatalogSource || self.serviceCatalogSelectedWrongType)) { + const selectedConnection = self.wizardService.getSelectedConnection(); + const connSvcSourceName = selectedConnection.getServiceCatalogSourceName(); + self.selectedServiceCatalogSource = this.emptyServiceCatalogSource; + for (const svcSource of self.serviceCatSources) { + if (svcSource.getId() === connSvcSourceName) { + self.selectedServiceCatalogSource = svcSource; + break; + } + } + } + + self.updatePage2ValidStatus(); + self.serviceCatalogSourcesLoading = false; + self.serviceCatalogSourcesLoadSuccess = true; + }, + (error) => { + // Creates the connection property form + this.createConnectionBasicPropertyForm(); + + self.logger.error("[AddConnectionWizardComponent] Error getting service catalog sources: %o", error); + self.updatePage2ValidStatus(); + self.serviceCatalogSourcesLoading = false; + self.serviceCatalogSourcesLoadSuccess = false; + } + ); + } + private setNavAway(allow: boolean): void { this.step1Config.allowNavAway = allow; } + /** + * Updates the page 1 status + */ private updatePage1ValidStatus( ): void { - this.step1Config.nextEnabled = this.basicPropertyForm.valid; + this.step1Config.nextEnabled = this.selectedConnTypes.length > 0; this.setNavAway(this.step1Config.nextEnabled); } - private updatePage2ValidStatus(formValid: boolean): void { - this.step2Config.nextEnabled = formValid; - this.setNavAway(this.step2Config.nextEnabled); + /** + * Updates the page 2 status + */ + private updatePage2ValidStatus( ): void { + if (!this.step2aConfig) { + return; + } + if (this.wizardService.isEdit()) { + this.step2aConfig.nextEnabled = this.connectionBasicPropertyForm.valid; + } else { + this.step2aConfig.nextEnabled = this.nameValid && this.hasSelectedServiceCatalogSource; + } + this.setNavAway(this.step2aConfig.nextEnabled); } /** - * Load the driver-specific property definitions + * Creates the connection ensuring it is bound + * @param {Connection} connection the new connection */ - private loadPropertyDefinitions( driverName ): void { - this.detailPropertiesLoading = false; - this.detailPropertiesLoadSuccess = false; + private createAndBindConnection(connection: NewConnection): void { const self = this; this.connectionService - .getConnectionTemplateProperties(driverName) + .createAndBindConnection(connection) .subscribe( - (props) => { - // Sort the properties. (Required properties first) - const firstProps: any[] = []; - const nextProps: any[] = []; - for (const prop of props) { - if (prop.isRequired()) { - firstProps.push(prop); - } else { - nextProps.push(prop); - } - } + (wasSuccess) => { + self.setFinalPageComplete(wasSuccess); + }, + (error) => { + self.logger.error("[AddConnectionWizardComponent] Error creating connection: %o", error); + self.setErrorDetails(error); + self.setFinalPageComplete(false); + } + ); + } - self.detailProperties = firstProps.concat(nextProps); - self.detailPropertiesLoading = false; - self.detailPropertiesLoadSuccess = true; - self.detailPropertiesLoadedType = driverName; + /** + * Updates the connection ensuring it is bound + * @param {Connection} connection the new connection + */ + private updateAndBindConnection(connection: NewConnection): void { + const self = this; + this.connectionService + .updateAndBindConnection(connection) + .subscribe( + (wasSuccess) => { + self.setFinalPageComplete(wasSuccess); }, (error) => { - self.logger.error("[AddConnectionWizardComponent] Error: %o", error); - self.detailPropertiesLoading = false; - self.detailPropertiesLoadSuccess = false; + self.logger.error("[AddConnectionWizardComponent] Error updating connection: %o", error); + self.setErrorDetails(error); + self.setFinalPageComplete(false); } ); } /** - * @returns {PropertyDefinition[]} the property definitions (can be null) + * Sets the final page in progress status */ - private getPropertyDefinitions(): Array> { - return this.detailProperties; + private setFinalPageInProgress(): void { + this.createComplete = false; + this.createSuccessful = false; + if (this.wizardService.isEdit()) { + this.theFinalPageTitle = "Update in progress"; + this.theFinalPageMessage = "The connection is being updated."; + } else { + this.theFinalPageTitle = "Creation in progress"; + this.theFinalPageMessage = "The connection is being created."; + } + this.step2bConfig.nextEnabled = false; + this.step2bConfig.previousEnabled = false; } /** - * Generates array of required property values when page 3 (Review) is shown + * Sets the final page completion status + * @param {boolean} wasSuccessful 'true' if the create or update was successful */ - private updateRequiredPropertyValues(): void { - const propMap: Map = new Map(); - for (const property of this.detailProperties) { - if (property.isRequired()) { - const name = property.getId(); - const theValue = this.detailPropForm.getPropertyValue(name); - propMap.set(name, theValue); + private setFinalPageComplete(wasSuccessful: boolean): void { + this.createComplete = true; + this.createSuccessful = wasSuccessful; + this.step2bConfig.nextEnabled = false; + this.step2bConfig.previousEnabled = true; + if (wasSuccessful) { + if (this.wizardService.isEdit()) { + this.theFinalPageTitle = "Update was successful"; + this.theFinalPageMessage = "The connection was updated successfully. Click on the button to see all connections."; + } else { + this.theFinalPageTitle = "Creation was successful"; + this.theFinalPageMessage = "The connection was created successfully. Click on the button to see all connections."; + } + } else { + if (this.wizardService.isEdit()) { + this.theFinalPageTitle = "Update failed"; + this.theFinalPageMessage = "The connection update failed!"; + } else { + this.theFinalPageTitle = "Creation failed"; + this.theFinalPageMessage = "The connection creation failed!"; } } - this.requiredPropValues = Array.from(propMap); } + /** + * Sets the error details for the response + * @param resp the rest call response + */ + private setErrorDetails( resp: any ): void { + // Get the error from the response json + this.errorDetailMessage = ""; + if (resp) { + try { + this.errorDetailMessage = resp.json().error; + } catch ( e ) { + this.errorDetailMessage = resp.text(); + } + } + // Error visible if message has content + if (this.errorDetailMessage.length === 0) { + this.errorDetailMessage = "Please check dataservice entries and retry"; + } + } } diff --git a/ngapp/src/app/connections/add-connection/add-connection.component.spec.ts b/ngapp/src/app/connections/add-connection/add-connection.component.spec.ts index a0184d52..195c79bb 100644 --- a/ngapp/src/app/connections/add-connection/add-connection.component.spec.ts +++ b/ngapp/src/app/connections/add-connection/add-connection.component.spec.ts @@ -3,11 +3,14 @@ import { async, ComponentFixture, TestBed } from "@angular/core/testing"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { RouterTestingModule } from "@angular/router/testing"; import { AddConnectionWizardComponent } from "@connections/add-connection-wizard/add-connection-wizard.component"; +import { ConnectionTypeCardComponent } from "@connections/connection-type-cards/connection-type-card/connection-type-card.component"; +import { ConnectionTypeCardsComponent } from "@connections/connection-type-cards/connection-type-cards.component"; import { ConnectionService } from "@connections/shared/connection.service"; import { MockConnectionService } from "@connections/shared/mock-connection.service"; import { AppSettingsService } from "@core/app-settings.service"; import { CoreModule } from "@core/core.module"; import { MockAppSettingsService } from "@core/mock-app-settings.service"; +import { WizardService } from "@dataservices/shared/wizard.service"; import { SharedModule } from "@shared/shared.module"; import { PatternFlyNgModule } from "patternfly-ng"; import { AddConnectionComponent } from "./add-connection.component"; @@ -19,8 +22,10 @@ describe("AddConnectionComponent", () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ CoreModule, PatternFlyNgModule, FormsModule, ReactiveFormsModule, RouterTestingModule, SharedModule ], - declarations: [ AddConnectionComponent, AddConnectionWizardComponent ], + declarations: [ AddConnectionComponent, AddConnectionWizardComponent, + ConnectionTypeCardComponent, ConnectionTypeCardsComponent ], providers: [ + WizardService, { provide: AppSettingsService, useClass: MockAppSettingsService }, { provide: ConnectionService, useClass: MockConnectionService }, ] diff --git a/ngapp/src/app/connections/connection-type-cards/connection-type-card/connection-type-card.component.css b/ngapp/src/app/connections/connection-type-cards/connection-type-card/connection-type-card.component.css new file mode 100644 index 00000000..8ca16ced --- /dev/null +++ b/ngapp/src/app/connections/connection-type-cards/connection-type-card/connection-type-card.component.css @@ -0,0 +1,3 @@ +.object-card-selected .card-pf .card-pf-heading { + border-bottom: var(--card-border-width) var(--card-border-style) var(--card-border-color) !important; +} diff --git a/ngapp/src/app/connections/connection-type-cards/connection-type-card/connection-type-card.component.html b/ngapp/src/app/connections/connection-type-cards/connection-type-card/connection-type-card.component.html new file mode 100644 index 00000000..ad20e208 --- /dev/null +++ b/ngapp/src/app/connections/connection-type-cards/connection-type-card/connection-type-card.component.html @@ -0,0 +1,16 @@ + + +
+ {{imageAlt}} +
+
+ {{ name }} +
+
+
{{ description }}
+
+
+
diff --git a/ngapp/src/app/connections/connection-type-cards/connection-type-card/connection-type-card.component.spec.ts b/ngapp/src/app/connections/connection-type-cards/connection-type-card/connection-type-card.component.spec.ts new file mode 100644 index 00000000..311fdcc1 --- /dev/null +++ b/ngapp/src/app/connections/connection-type-cards/connection-type-card/connection-type-card.component.spec.ts @@ -0,0 +1,35 @@ +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; + +import { RouterTestingModule } from "@angular/router/testing"; +import { ConnectionType } from "@connections/shared/connection-type.model"; +import { PatternFlyNgModule } from "patternfly-ng"; +import { ConnectionTypeCardComponent } from "./connection-type-card.component"; + +describe("ConnectionTypeCardComponent", () => { + let component: ConnectionTypeCardComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ RouterTestingModule, PatternFlyNgModule ], + declarations: [ ConnectionTypeCardComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ConnectionTypeCardComponent); + component = fixture.componentInstance; + + const connType = new ConnectionType(); + connType.setName("connType1"); + component.connectionType = connType; + + component.selectedConnectionTypes = []; + fixture.detectChanges(); + }); + + it("should be created", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ngapp/src/app/connections/connection-type-cards/connection-type-card/connection-type-card.component.ts b/ngapp/src/app/connections/connection-type-cards/connection-type-card/connection-type-card.component.ts new file mode 100644 index 00000000..d3cb6f56 --- /dev/null +++ b/ngapp/src/app/connections/connection-type-cards/connection-type-card/connection-type-card.component.ts @@ -0,0 +1,74 @@ +import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from "@angular/core"; +import { CardConfig } from "patternfly-ng"; +import { ConnectionType } from "../../shared/connection-type.model"; + +@Component({ + encapsulation: ViewEncapsulation.None, + selector: "app-connection-type-card", + templateUrl: "./connection-type-card.component.html", + styleUrls: ["./connection-type-card.component.css"] +}) +export class ConnectionTypeCardComponent implements OnInit { + + @Input() public connectionType: ConnectionType; + @Input() public selectedConnectionTypes: ConnectionType[]; + @Output() public cardEvent: EventEmitter< {} > = new EventEmitter< {} >(); + @Output() public selectEvent: EventEmitter< ConnectionType > = new EventEmitter< ConnectionType >(); + + public cardConfig: CardConfig; + + constructor() { + // nothing to do + } + + public ngOnInit(): void { + this.cardConfig = { + titleBorder: true, + noPadding: true, + topBorder: false + } as CardConfig; + } + + /** + * @returns {string} the ConnectionType name + */ + public get name(): string { + return this.connectionType.getName(); + } + + /** + * @returns {string} the ConnectionType description + */ + public get description(): string { + return this.connectionType.getDescription(); + } + + /** + * @returns {string} the ConnectionType image source + */ + public get imageSrc(): string { + return this.connectionType.getImageSrc(); + } + + /** + * @returns {string} the ConnectionType image alt text + */ + public get imageAlt(): string { + return this.connectionType.getImageAlt(); + } + + /** + * @returns {boolean} `true` if the ConnectionType represented by this card is selected + */ + public isSelected(): boolean { + return this.selectedConnectionTypes.indexOf( this.connectionType ) !== -1; + } + + /** + * An event handler for when the card is clicked. + */ + public onSelect(): void { + this.selectEvent.emit( this.connectionType ); + } + +} diff --git a/ngapp/src/app/connections/connection-type-cards/connection-type-cards.component.css b/ngapp/src/app/connections/connection-type-cards/connection-type-cards.component.css new file mode 100644 index 00000000..8abeffb9 --- /dev/null +++ b/ngapp/src/app/connections/connection-type-cards/connection-type-cards.component.css @@ -0,0 +1,8 @@ +.container-cards-pf { + padding: 0; + margin-top: 0; +} + +.row-cards-pf { + padding: 0; +} diff --git a/ngapp/src/app/connections/connection-type-cards/connection-type-cards.component.html b/ngapp/src/app/connections/connection-type-cards/connection-type-cards.component.html new file mode 100644 index 00000000..68cc5c79 --- /dev/null +++ b/ngapp/src/app/connections/connection-type-cards/connection-type-cards.component.html @@ -0,0 +1,7 @@ +
+
+ +
+
diff --git a/ngapp/src/app/connections/connection-type-cards/connection-type-cards.component.spec.ts b/ngapp/src/app/connections/connection-type-cards/connection-type-cards.component.spec.ts new file mode 100644 index 00000000..ed9a8f53 --- /dev/null +++ b/ngapp/src/app/connections/connection-type-cards/connection-type-cards.component.spec.ts @@ -0,0 +1,29 @@ +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; + +import { RouterTestingModule } from "@angular/router/testing"; +import { ConnectionTypeCardComponent } from "@connections/connection-type-cards/connection-type-card/connection-type-card.component"; +import { PatternFlyNgModule } from "patternfly-ng"; +import { ConnectionTypeCardsComponent } from "./connection-type-cards.component"; + +describe("ConnectionTypeCardsComponent", () => { + let component: ConnectionTypeCardsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ RouterTestingModule, PatternFlyNgModule ], + declarations: [ ConnectionTypeCardComponent, ConnectionTypeCardsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ConnectionTypeCardsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should be created", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ngapp/src/app/connections/connection-type-cards/connection-type-cards.component.ts b/ngapp/src/app/connections/connection-type-cards/connection-type-cards.component.ts new file mode 100644 index 00000000..42a02a65 --- /dev/null +++ b/ngapp/src/app/connections/connection-type-cards/connection-type-cards.component.ts @@ -0,0 +1,51 @@ +/** + * @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, ViewEncapsulation } from "@angular/core"; +import { ConnectionType } from "@connections/shared/connection-type.model"; + +@Component({ + moduleId: module.id, + encapsulation: ViewEncapsulation.None, + selector: "app-connection-type-cards", + templateUrl: "./connection-type-cards.component.html", + styleUrls: ["./connection-type-cards.component.css"] +}) + +export class ConnectionTypeCardsComponent { + + @Input() public connectionTypes: ConnectionType[]; + @Input() public selectedConnectionTypes: ConnectionType[]; + @Output() public connectionTypeSelected: EventEmitter = new EventEmitter(); + @Output() public connectionTypeDeselected: EventEmitter = new EventEmitter(); + + /** + * constructor + */ + constructor( ) { + // nothing to do + } + + public isSelected( connectionType: ConnectionType ): boolean { + return this.selectedConnectionTypes.indexOf( connectionType ) !== -1; + } + + public onSelectEvent( connectionType: ConnectionType ): void { + this.connectionTypeSelected.emit( connectionType ); + } + +} diff --git a/ngapp/src/app/connections/connections.component.html b/ngapp/src/app/connections/connections.component.html index b3845c97..eaa6e22e 100644 --- a/ngapp/src/app/connections/connections.component.html +++ b/ngapp/src/app/connections/connections.component.html @@ -23,7 +23,7 @@

Connections

diff --git a/ngapp/src/app/connections/connections.component.spec.ts b/ngapp/src/app/connections/connections.component.spec.ts index e2572fa5..188a65bc 100644 --- a/ngapp/src/app/connections/connections.component.spec.ts +++ b/ngapp/src/app/connections/connections.component.spec.ts @@ -13,6 +13,7 @@ import { MockConnectionService } from "@connections/shared/mock-connection.servi import { AppSettingsService } from "@core/app-settings.service"; import { CoreModule } from "@core/core.module"; import { MockAppSettingsService } from "@core/mock-app-settings.service"; +import { WizardService } from "@dataservices/shared/wizard.service"; import { SharedModule } from "@shared/shared.module"; import { ModalModule } from "ngx-bootstrap"; import { PatternFlyNgModule } from "patternfly-ng"; @@ -38,6 +39,9 @@ describe("ConnectionsComponent", () => { ConnectionsListComponent, ConnectionCardComponent, ConnectionsCardsComponent + ], + providers: [ + WizardService ] }); diff --git a/ngapp/src/app/connections/connections.component.ts b/ngapp/src/app/connections/connections.component.ts index 442ae426..3e98847d 100644 --- a/ngapp/src/app/connections/connections.component.ts +++ b/ngapp/src/app/connections/connections.component.ts @@ -22,6 +22,7 @@ import { ConnectionService } from "@connections/shared/connection.service"; import { ConnectionsConstants } from "@connections/shared/connections-constants"; import { AppSettingsService } from "@core/app-settings.service"; import { LoggerService } from "@core/logger.service"; +import { WizardService } from "@dataservices/shared/wizard.service"; import { AbstractPageComponent } from "@shared/abstract-page.component"; import { ConfirmDeleteComponent } from "@shared/confirm-delete/confirm-delete.component"; import { LayoutType } from "@shared/layout-type.enum"; @@ -34,7 +35,6 @@ import { SortConfig } from "patternfly-ng"; import { SortField } from "patternfly-ng"; import { SortEvent } from "patternfly-ng"; - @Component({ moduleId: module.id, selector: "app-connections", @@ -43,8 +43,6 @@ import { SortEvent } from "patternfly-ng"; }) export class ConnectionsComponent extends AbstractPageComponent implements OnInit{ - public readonly addConnectionLink: string = ConnectionsConstants.addConnectionPath; - public connectionNameForDelete: string; public filterConfig: FilterConfig; @@ -63,15 +61,17 @@ export class ConnectionsComponent extends AbstractPageComponent implements OnIni private appSettingsService: AppSettingsService; private connectionService: ConnectionService; private layout: LayoutType = LayoutType.CARD; + private wizardService: WizardService; @ViewChild(ConfirmDeleteComponent) private confirmDeleteDialog: ConfirmDeleteComponent; constructor(router: Router, route: ActivatedRoute, appSettingsService: AppSettingsService, - connectionService: ConnectionService, logger: LoggerService) { + wizardService: WizardService, connectionService: ConnectionService, logger: LoggerService) { super(route, logger); this.router = router; this.appSettingsService = appSettingsService; this.connectionService = connectionService; + this.wizardService = wizardService; } public ngOnInit(): void { @@ -160,12 +160,6 @@ export class ConnectionsComponent extends AbstractPageComponent implements OnIni return this.selectedConns; } - public onEdit( connectionName: string ): void { - const connection = this.filteredConns.find( ( conn ) => conn.getId() === connectionName ); - // TODO: implement onEdit - alert( "Edit '" + connectionName + "' connection (not yet implemented)" ); - } - public onPing( connName: string ): void { // TODO: implement onEdit alert( "Ping the '" + connName + "' connection (not yet implemented)" ); @@ -196,6 +190,37 @@ export class ConnectionsComponent extends AbstractPageComponent implements OnIni this.appSettingsService.connectionsPageLayout = LayoutType.CARD; } + /** + * Handle request for new Connection + */ + public onNew(): void { + this.wizardService.setEdit(false); + + const link: string[] = [ ConnectionsConstants.addConnectionPath ]; + this.logger.log("[ConnectionsComponent] Navigating to: %o", link); + this.router.navigate(link).then(() => { + // nothing to do + }); + } + + /** + * Handle Edit of the specified Connection + * @param {string} connName + */ + public onEdit(connName: string): void { + const selectedConnection = this.filteredConnections.find((x) => x.getId() === connName); + + // Sets the selected dataservice and edit mode before transferring + this.wizardService.setSelectedConnection(selectedConnection); + this.wizardService.setEdit(true); + + const link: string[] = [ ConnectionsConstants.addConnectionPath ]; + this.logger.log("[ConnectionsComponent] Navigating to: %o", link); + this.router.navigate(link).then(() => { + // nothing to do + }); + } + /** * Called to doDelete all selected APIs. */ diff --git a/ngapp/src/app/connections/connections.module.ts b/ngapp/src/app/connections/connections.module.ts index 09325dcb..771334ab 100644 --- a/ngapp/src/app/connections/connections.module.ts +++ b/ngapp/src/app/connections/connections.module.ts @@ -32,6 +32,8 @@ import { CoreModule } from "@core/core.module"; import { LoggerService } from "@core/logger.service"; import { SharedModule } from "@shared/shared.module"; import { PatternFlyNgModule } from "patternfly-ng"; +import { ConnectionTypeCardComponent } from "./connection-type-cards/connection-type-card/connection-type-card.component"; +import { ConnectionTypeCardsComponent } from "./connection-type-cards/connection-type-cards.component"; @NgModule({ imports: [ @@ -52,7 +54,9 @@ import { PatternFlyNgModule } from "patternfly-ng"; ConnectionsListComponent, AddConnectionWizardComponent, AddConnectionComponent, - ConnectionCardComponent + ConnectionCardComponent, + ConnectionTypeCardsComponent, + ConnectionTypeCardComponent ], providers: [ ConnectionService, diff --git a/ngapp/src/app/connections/shared/connection-type.model.ts b/ngapp/src/app/connections/shared/connection-type.model.ts new file mode 100644 index 00000000..b6d45a04 --- /dev/null +++ b/ngapp/src/app/connections/shared/connection-type.model.ts @@ -0,0 +1,89 @@ +/** + * @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 { ConnectionsConstants } from "@connections/shared/connections-constants"; + +export class ConnectionType { + private name: string; + private description: string; + + constructor() { + // nothing to do + } + + /** + * @returns {string} the ConnectionType name + */ + public getName(): string { + return this.name; + } + + /** + * @returns {string} the ConnectionType description + */ + public getDescription(): string { + return this.description; + } + + /** + * @returns {string} the alternate image text for the ConnectionType + */ + public getImageAlt(): string { + const name = this.getName(); + if (name === ConnectionsConstants.connectionType_mysql) { + return "MySQL database img"; + } else if (name === ConnectionsConstants.connectionType_postgresql) { + return "PostgeSQL database img"; + } else if (name === ConnectionsConstants.connectionType_mongodb) { + return "MongoDB database img"; + } else if (name === ConnectionsConstants.connectionType_mariadb) { + return "MariaDB database img"; + } + return "unknown image"; + } + + /** + * @returns {string} the image location for the ConnectionType + */ + public getImageSrc(): string { + const name = this.getName(); + if (name === ConnectionsConstants.connectionType_mysql) { + return "assets/MySQL_70x40.png"; + } else if (name === ConnectionsConstants.connectionType_postgresql) { + return "assets/PostgresSql_70x40.png"; + } else if (name === ConnectionsConstants.connectionType_mongodb) { + return "assets/MongoDB_70x40.png"; + } else if (name === ConnectionsConstants.connectionType_mariadb) { + return "assets/MongoDB_70x40.png"; + } + return ""; + } + + /** + * @param {string} name the ConnectionType name + */ + public setName( name?: string ): void { + this.name = name ? name : null; + } + + /** + * @param {string} description the ConnectionType description + */ + public setDescription( description?: string ): void { + this.description = description ? description : null; + } + +} diff --git a/ngapp/src/app/connections/shared/connection.model.ts b/ngapp/src/app/connections/shared/connection.model.ts index 02fee4ab..2a656cd9 100644 --- a/ngapp/src/app/connections/shared/connection.model.ts +++ b/ngapp/src/app/connections/shared/connection.model.ts @@ -20,11 +20,14 @@ import { SortDirection } from "@shared/sort-direction.enum"; export class Connection implements Identifiable< string > { + public static descriptionProp = "description"; + public static serviceCatalogSourceProp = "serviceCatalogSource"; + private keng__id: string; private dv__jndiName: string; private dv__driverName: string; private dv__type: boolean; - private keng__properties: Map< string, string > = new Map< string, string >(); + private keng__properties: object[] = []; /** * @param {Object} json the JSON representation of a Connection @@ -79,12 +82,25 @@ export class Connection implements Identifiable< string > { return result; } + /** + * @returns {string} the connection name + */ + public get name(): string { + return this.keng__id; + } + /** * @returns {string} the connection description */ public getDescription(): string { - // TODO do connections have a description - return "This is a connection description. So if you're looking for the description you found it."; + let description: string = null; + for (const propMap of this.keng__properties) { + if (propMap["name"] === Connection.descriptionProp) { + description = propMap["value"]; + break; + } + } + return description; } /** @@ -108,14 +124,6 @@ export class Connection implements Identifiable< string > { return this.dv__jndiName; } - /** - * @returns {string} the service catalog source name of this connection - */ - public getServiceCatalogSourceName(): string { - // TODO: finish implementing getServiceCatalogSourceName() - return "TBD"; - } - /** * @returns {boolean} the jdbc status (true == jdbc) */ @@ -124,17 +132,17 @@ export class Connection implements Identifiable< string > { } /** - * @returns {string} the connection name + * @returns {string} the service catalog source name */ - public get name(): string { - return this.keng__id; - } - - /** - * @returns {Map} the connection properties (never null) - */ - public getProperties(): Map< string, string > { - return this.keng__properties; + public getServiceCatalogSourceName(): string { + let serviceCatalogName: string = null; + for (const propMap of this.keng__properties) { + if (propMap["name"] === Connection.serviceCatalogSourceProp) { + serviceCatalogName = propMap["value"]; + break; + } + } + return serviceCatalogName; } /** @@ -166,10 +174,18 @@ export class Connection implements Identifiable< string > { } /** - * @param {Map} props the connection properties (optional) + * @param {string} serviceCatalog the service catalog source name */ - public setProperties( props?: Map< string, string > ): void { - this.keng__properties = props ? props : new Map< string, string >(); + public setServiceCatalogSourceName( serviceCatalog: string ): void { + interface IProp { + name?: string; + value?: string; + } + const prop: IProp = {}; + prop.name = Connection.serviceCatalogSourceProp; + prop.value = serviceCatalog; + + this.keng__properties.push(prop); } /** diff --git a/ngapp/src/app/connections/shared/connection.service.ts b/ngapp/src/app/connections/shared/connection.service.ts index 49291ded..586c4be9 100644 --- a/ngapp/src/app/connections/shared/connection.service.ts +++ b/ngapp/src/app/connections/shared/connection.service.ts @@ -17,11 +17,13 @@ import { Injectable } from "@angular/core"; import { Http } from "@angular/http"; +import { ConnectionType } from "@connections/shared/connection-type.model"; import { Connection } from "@connections/shared/connection.model"; import { ConnectionsConstants } from "@connections/shared/connections-constants"; import { JdbcTableFilter } from "@connections/shared/jdbc-table-filter.model"; import { NewConnection } from "@connections/shared/new-connection.model"; import { SchemaInfo } from "@connections/shared/schema-info.model"; +import { ServiceCatalogSource } from "@connections/shared/service-catalog-source.model"; import { TemplateDefinition } from "@connections/shared/template-definition.model"; import { ApiService } from "@core/api.service"; import { AppSettingsService } from "@core/app-settings.service"; @@ -34,6 +36,10 @@ import { Observable } from "rxjs/Observable"; @Injectable() export class ConnectionService extends ApiService { + private static readonly nameValidationUrl = environment.komodoWorkspaceUrl + + ConnectionsConstants.connectionsRootPath + + "/nameValidation/"; + private http: Http; constructor( http: Http, appSettings: AppSettingsService, logger: LoggerService ) { @@ -41,6 +47,32 @@ export class ConnectionService extends ApiService { this.http = http; } + /** + * Validates the specified connection name. 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} name the connection name + * @returns {Observable} + */ + public isValidName( name: string ): Observable< string > { + if ( !name || name.length === 0 ) { + return Observable.of( "Connection name cannot be empty" ); + } + + const url = ConnectionService.nameValidationUrl + encodeURIComponent( name ); + + return this.http.get( url, this.getAuthRequestOptions() ) + .map( ( response ) => { + if ( response.ok ) { + if ( response.text() ) { + return response.text(); + } + + return ""; + } } ) + .catch( ( error ) => this.handleError( error ) ); + } + /** * Get the connections from the komodo rest interface * @returns {Observable} @@ -56,14 +88,15 @@ export class ConnectionService extends ApiService { } /** - * Create a connection via the komodo rest interface - * @param {NewConnection} connection + * Deploy a connection via the komodo rest interface + * @param {string} connectionName * @returns {Observable} */ - public createConnection(connection: NewConnection): Observable { + public deployConnection(connectionName: string): Observable { + const connectionPath = this.getKomodoUserWorkspacePath() + "/" + connectionName; return this.http - .post(environment.komodoWorkspaceUrl + ConnectionsConstants.connectionsRootPath + "/" + connection.getName(), - connection, this.getAuthRequestOptions()) + .post(environment.komodoTeiidUrl + ConnectionsConstants.connectionRootPath, + { path: connectionPath}, this.getAuthRequestOptions()) .map((response) => { return response.ok; }) @@ -71,15 +104,14 @@ export class ConnectionService extends ApiService { } /** - * Deploy a connection via the komodo rest interface - * @param {string} connectionName + * Bind a service catalog source via the komodo rest interface + * @param {string} serviceCatalogSourceName * @returns {Observable} */ - public deployConnection(connectionName: string): Observable { - const connectionPath = this.getKomodoUserWorkspacePath() + "/" + connectionName; + public bindServiceCatalogSource(serviceCatalogSourceName: string): Observable { return this.http - .post(environment.komodoTeiidUrl + ConnectionsConstants.connectionRootPath, - { path: connectionPath}, this.getAuthRequestOptions()) + .post(environment.komodoTeiidUrl + ConnectionsConstants.serviceCatalogSourcesRootPath, + { name: serviceCatalogSourceName}, this.getAuthRequestOptions()) .map((response) => { return response.ok; }) @@ -101,6 +133,47 @@ export class ConnectionService extends ApiService { .catch( ( error ) => this.handleError( error ) ); } + /** + * Get the connection types from the komodo rest interface + * @returns {ConnectionType[]} + */ + public getConnectionTypes(): ConnectionType[] { + const connectionTypes: ConnectionType[] = []; + const connType1: ConnectionType = new ConnectionType(); + connType1.setName(ConnectionsConstants.connectionType_postgresql); + connType1.setDescription(ConnectionsConstants.connectionTypeDescription_postgresql); + const connType2: ConnectionType = new ConnectionType(); + connType2.setName(ConnectionsConstants.connectionType_mysql); + connType2.setDescription(ConnectionsConstants.connectionTypeDescription_mysql); + // const connType3: ConnectionType = new ConnectionType(); + // connType3.setName(ConnectionsConstants.connectionType_mongodb); + // connType3.setDescription(ConnectionsConstants.connectionTypeDescription_mongodb); + // const connType4: ConnectionType = new ConnectionType(); + // connType4.setName(ConnectionsConstants.connectionType_mariadb); + // connType4.setDescription(ConnectionsConstants.connectionTypeDescription_mariadb); + + connectionTypes.push(connType1); + connectionTypes.push(connType2); + // connectionTypes.push(connType3); + // connectionTypes.push(connType4); + + return connectionTypes; + } + + /** + * Get the available ServiceCatalogSources from the komodo rest interface + * @returns {Observable} + */ + public getAllServiceCatalogSources(): Observable { + return this.http + .get(environment.komodoTeiidUrl + ConnectionsConstants.serviceCatalogSourcesRootPath, this.getAuthRequestOptions()) + .map((response) => { + const catalogSources = response.json(); + return catalogSources.map((catSource) => ServiceCatalogSource.create( catSource )); + }) + .catch( ( error ) => this.handleError( error ) ); + } + /** * Get the connection templates from the komodo rest interface * @returns {Observable} @@ -171,14 +244,33 @@ export class ConnectionService extends ApiService { } /** - * Create a connection and deploy it to server via the komodo rest interface - * @param {NewConnection} connection + * Create a connection in the Komodo repo - also binds the specified serviceCatalogSource + * @param {NewConnection} connection the connection object * @returns {Observable} */ - public createAndDeployConnection(connection: NewConnection): Observable { - // Chain the individual calls together in series to build the DataService - return this.createConnection(connection) - .flatMap((res) => this.deployConnection(connection.getName())); + public createAndBindConnection(connection: NewConnection): Observable { + return this.http + .post(environment.komodoWorkspaceUrl + ConnectionsConstants.connectionsRootPath + "/" + connection.getName(), + connection, this.getAuthRequestOptions()) + .map((response) => { + return response.ok; + }) + .catch( ( error ) => this.handleError( error ) ); + } + + /** + * Update a connection in the Komodo repo - also binds the specified serviceCatalogSource. + * @param {NewConnection} connection the connection object + * @returns {Observable} + */ + public updateAndBindConnection(connection: NewConnection): Observable { + return this.http + .put(environment.komodoWorkspaceUrl + ConnectionsConstants.connectionsRootPath + "/" + connection.getName(), + connection, this.getAuthRequestOptions()) + .map((response) => { + return response.ok; + }) + .catch( ( error ) => this.handleError( error ) ); } } diff --git a/ngapp/src/app/connections/shared/connections-constants.ts b/ngapp/src/app/connections/shared/connections-constants.ts index 0f5e5b6a..fc9adea3 100644 --- a/ngapp/src/app/connections/shared/connections-constants.ts +++ b/ngapp/src/app/connections/shared/connections-constants.ts @@ -23,6 +23,19 @@ export class ConnectionsConstants { public static readonly addConnectionRoute = ConnectionsConstants.connectionsRootRoute + "/add-connection"; public static readonly addConnectionPath = ConnectionsConstants.connectionsRootPath + "/add-connection"; + public static readonly serviceCatalogSourcesRootRoute = "serviceCatalogSources"; + public static readonly serviceCatalogSourcesRootPath = "/" + ConnectionsConstants.serviceCatalogSourcesRootRoute; + + public static readonly connectionType_postgresql = "postgresql"; + public static readonly connectionType_mysql = "mysql"; + public static readonly connectionType_mongodb = "mongodb"; + public static readonly connectionType_mariadb = "mariadb"; + + public static readonly connectionTypeDescription_postgresql = "PostgreSQL database"; + public static readonly connectionTypeDescription_mysql = "MySQL database"; + public static readonly connectionTypeDescription_mongodb = "MongoDB database"; + public static readonly connectionTypeDescription_mariadb = "MariaDB database"; + public static driverNamePropertyLabel = "Driver Name"; public static jndiNamePropertyLabel = "JNDI Name"; public static serviceCatalogSourceNameLabel = "Service Catalog Source"; diff --git a/ngapp/src/app/connections/shared/mock-connection.service.ts b/ngapp/src/app/connections/shared/mock-connection.service.ts index 47dda730..571d26db 100644 --- a/ngapp/src/app/connections/shared/mock-connection.service.ts +++ b/ngapp/src/app/connections/shared/mock-connection.service.ts @@ -79,6 +79,7 @@ export class MockConnectionService extends ConnectionService { const newConn = new Connection(); newConn.setId( id ); newConn.setJdbc( true ); + newConn.setServiceCatalogSourceName(id); return newConn; } @@ -94,15 +95,6 @@ export class MockConnectionService extends ConnectionService { return Observable.of(this.conns); } - /** - * Create a connection via the komodo rest interface - * @param {NewConnection} connection - * @returns {Observable} - */ - public createConnection(connection: NewConnection): Observable { - return Observable.of(true); - } - /** * Delete a connection via the komodo rest interface * @param {string} connectionId diff --git a/ngapp/src/app/connections/shared/new-connection.model.ts b/ngapp/src/app/connections/shared/new-connection.model.ts index e2240322..56371500 100644 --- a/ngapp/src/app/connections/shared/new-connection.model.ts +++ b/ngapp/src/app/connections/shared/new-connection.model.ts @@ -18,10 +18,8 @@ export class NewConnection { private name: string; - private jndiName: string; - private driverName: string; - private jdbc: boolean; - private properties: Map< string, string > = new Map< string, string >(); + private description = ""; + private serviceCatalogSource: string; /** * Constructor @@ -38,31 +36,17 @@ export class NewConnection { } /** - * @returns {string} the connection jndi name (can be null) + * @returns {string} the connection description (can be null) */ - public getJndiName(): string { - return this.jndiName; + public getDescription(): string { + return this.description; } /** - * @returns {string} the connection driver name (can be null) + * @returns {string} the connection serviceCatalog source name (can be null) */ - public getDriverName(): string { - return this.driverName; - } - - /** - * @returns {boolean} the jdbc status - */ - public isJdbc(): boolean { - return this.jdbc; - } - - /** - * @returns {Map} the connection properties (never null) - */ - public getProperties(): Map< string, string > { - return this.properties; + public getServiceCatalogSource(): string { + return this.serviceCatalogSource; } /** @@ -73,50 +57,25 @@ export class NewConnection { } /** - * @param {string} jndiName the connection JNDI name (optional) + * @param {string} description the connection description (optional) */ - public setJndiName( jndiName?: string ): void { - this.jndiName = jndiName ? jndiName : null; + public setDescription( description?: string ): void { + this.description = description ? description : ""; } /** - * @param {string} driverName the connection driver name (optional) + * @param {string} serviceCatalogSource the serviceCatalogSource */ - public setDriverName( driverName?: string ): void { - this.driverName = driverName ? driverName : null; - } - - /** - * @param {boolean} isJdbc the jdbc state - */ - public setJdbc( isJdbc ): void { - this.jdbc = isJdbc; - } - - /** - * @param {Map} props the connection properties (optional) - */ - public setProperties( props?: Map< string, string > ): void { - this.properties = props ? props : new Map< string, string >(); + public setServiceCatalogSource( serviceCatalogSource?: string ): void { + this.serviceCatalogSource = serviceCatalogSource ? serviceCatalogSource : null; } // overrides toJSON - we do not want the name supplied in the json body. public toJSON(): {} { return { - jndiName: this.jndiName, - driverName: this.driverName, - jdbc: this.jdbc, - parameters: this.strMapToObj(this.properties) + description: this.description, + serviceCatalogSource: this.serviceCatalogSource, }; } - private strMapToObj(strMap: Map): any { - const obj = Object.create(null); - for (const [k, v] of Array.from(strMap)) { - // We don’t escape the key '__proto__' - // which can cause problems on older engines - obj[k] = v; - } - return obj; - } } diff --git a/ngapp/src/app/connections/shared/service-catalog-source.model.ts b/ngapp/src/app/connections/shared/service-catalog-source.model.ts new file mode 100644 index 00000000..840f9def --- /dev/null +++ b/ngapp/src/app/connections/shared/service-catalog-source.model.ts @@ -0,0 +1,155 @@ +/** + * @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 { Identifiable } from "@shared/identifiable"; +import { SortDirection } from "@shared/sort-direction.enum"; + +export class ServiceCatalogSource implements Identifiable< string > { + + private keng__id: string; + private sc__name: string; + private sc__type: string; + private sc__bound: boolean; + + /** + * Create a ServiceCatalogSource from its json representation + * @param {Object} json the JSON representation of a ServiceCatalogSource + * @returns {ServiceCatalogSource} the new ServiceCatalogSource (never null) + */ + public static create( json: object = {} ): ServiceCatalogSource { + const conn = new ServiceCatalogSource(); + conn.setValues( json ); + return conn; + } + + /** + * Sorts the provided catalog sources in the specified sort direction + * @param {ServiceCatalogSource[]} catalogSources the catalog sources being sorted + * @param {SortDirection} sortDirection the sort direction + */ + public static sort( catalogSources: ServiceCatalogSource[], + sortDirection: SortDirection ): void { + catalogSources.sort( ( thisSource: ServiceCatalogSource, thatSource: ServiceCatalogSource ) => { + const result = thisSource.compareTo( thatSource ); + + if ( sortDirection === SortDirection.DESC ) { + return result * -1; + } + + return result; + } ); + } + + constructor() { + // nothing to do + } + + /** + * See {Identifiable}. + */ + public compareTo( that: ServiceCatalogSource ): number { + let result = 0; + + if ( this.getId() ) { + if ( that.getId() ) { + // both have an ID + result = this.getId().localeCompare( that.getId() ); + } else { + // thatItem does not have an ID + result = 1; + } + } else if ( that.getId() ) { + // thisItem does not have an ID and thatItem does + result = -1; + } + + return result; + } + + /** + * Get the catalog source type + * @returns {string} the catalog source type name (can be null) + */ + public getType(): string { + return this.sc__type; + } + + /** + * Get the catalog source id + * @returns {string} the catalog source identifier (can be null) + */ + public getId(): string { + return this.keng__id; + } + + /** + * Get the catalog source name + * @returns {string} the catalog source name (can be null) + */ + public getName(): string { + return this.sc__name; + } + + /** + * Get the bound status of the catalog source + * @returns {boolean} the bound status (true == bound) + */ + public isBound(): boolean { + return this.sc__bound; + } + + /** + * Set the catalog source type + * @param {string} typeName the catalog source type (optional) + */ + public setType( typeName?: string ): void { + this.sc__type = typeName ? typeName : null; + } + + /** + * Set the catalog source id + * @param {string} id the catalog source identifier (optional) + */ + public setId( id?: string ): void { + this.keng__id = id ? id : null; + } + + /** + * Set the catalog source name + * @param {string} name the catalog source name (optional) + */ + public setName( name?: string ): void { + this.sc__name = name ? name : null; + } + + /** + * Set the bound state of the catalog source + * @param {boolean} bound the bound state + */ + public setBound( bound: boolean ): void { + this.sc__bound = bound; + } + + /** + * Set all object values using the supplied catalog source json + * @param {Object} values + */ + public setValues(values: object = {}): void { + Object.assign(this, values); + } + +} diff --git a/ngapp/src/app/dataservices/add-dataservice-wizard/add-dataservice-wizard.component.ts b/ngapp/src/app/dataservices/add-dataservice-wizard/add-dataservice-wizard.component.ts index ba073334..da7fdafd 100644 --- a/ngapp/src/app/dataservices/add-dataservice-wizard/add-dataservice-wizard.component.ts +++ b/ngapp/src/app/dataservices/add-dataservice-wizard/add-dataservice-wizard.component.ts @@ -349,6 +349,11 @@ export class AddDataserviceWizardComponent implements OnInit, OnDestroy { this.createDataserviceForSingleSourceTables(); } } else if (status.isFailed()) { + // Set error message to first error, if there is one. + const errors = status.getErrors(); + if (errors.length > 0) { + this.errorDetailMessage = errors[0]; + } this.setFinalPageComplete(false); } } diff --git a/ngapp/src/app/dataservices/jdbc-table-selector/jdbc-table-selector.component.ts b/ngapp/src/app/dataservices/jdbc-table-selector/jdbc-table-selector.component.ts index 57901bcb..01ec4fe8 100644 --- a/ngapp/src/app/dataservices/jdbc-table-selector/jdbc-table-selector.component.ts +++ b/ngapp/src/app/dataservices/jdbc-table-selector/jdbc-table-selector.component.ts @@ -86,7 +86,7 @@ export class JdbcTableSelectorComponent implements OnInit, TableSelector { this.schemaLoadingState = LoadingState.LOADING; const self = this; this.connectionService - .getConnectionSchemaInfos(this.connection.getId()) + .getConnectionSchemaInfos(conn.getServiceCatalogSourceName()) .subscribe( (infos) => { self.generateSchemaNames(infos); @@ -149,9 +149,19 @@ export class JdbcTableSelectorComponent implements OnInit, TableSelector { this.currentSchema = schema; const filterInfo = new JdbcTableFilter(); - filterInfo.setConnectionName(this.connection.getId()); - filterInfo.setCatalogFilter(schema.getCatalogName()); - filterInfo.setSchemaFilter(schema.getName()); + filterInfo.setConnectionName(this.connection.getServiceCatalogSourceName()); + if (schema.getType() === "Catalog") { + filterInfo.setCatalogFilter(schema.getName()); + filterInfo.setSchemaFilter("%"); + } else { + filterInfo.setSchemaFilter(schema.getName()); + const catName = schema.getCatalogName(); + if (catName && catName.length > 0) { + filterInfo.setCatalogFilter(catName); + } else { + filterInfo.setCatalogFilter("%"); + } + } filterInfo.setTableFilter("%"); this.loadTablesForSchema(filterInfo); } diff --git a/ngapp/src/app/dataservices/shared/wizard.service.ts b/ngapp/src/app/dataservices/shared/wizard.service.ts index 191b4674..caef55e3 100644 --- a/ngapp/src/app/dataservices/shared/wizard.service.ts +++ b/ngapp/src/app/dataservices/shared/wizard.service.ts @@ -10,6 +10,7 @@ export class WizardService { private edit = false; private currentConnections: Connection[] = []; private selectedDataservice: Dataservice; + private selectedConnection: Connection; constructor() { // Nothing to do @@ -31,6 +32,22 @@ export class WizardService { return this.edit; } + /** + * Gets the selected connection + * @returns {Connection} the selected connection + */ + public getSelectedConnection(): Connection { + return this.selectedConnection; + } + + /** + * Sets the selected connection + * @param {Connection} connection the selected connection + */ + public setSelectedConnection(connection: Connection): void { + this.selectedConnection = connection; + } + /** * Gets the selected dataservice * @returns {Dataservice} the selected dataservice diff --git a/ngapp/src/assets/MongoDB_70x40.png b/ngapp/src/assets/MongoDB_70x40.png new file mode 100644 index 00000000..f7afb192 Binary files /dev/null and b/ngapp/src/assets/MongoDB_70x40.png differ diff --git a/ngapp/src/assets/MySQL_70x40.png b/ngapp/src/assets/MySQL_70x40.png new file mode 100644 index 00000000..f5e58dba Binary files /dev/null and b/ngapp/src/assets/MySQL_70x40.png differ diff --git a/ngapp/src/assets/PostgresSql_70x40.png b/ngapp/src/assets/PostgresSql_70x40.png new file mode 100644 index 00000000..3751bd5c Binary files /dev/null and b/ngapp/src/assets/PostgresSql_70x40.png differ