diff --git a/package-lock.json b/package-lock.json index 995be62926..6761ba0c35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -365,6 +365,14 @@ "tslib": "1.9.2" } }, + "@angular/flex-layout": { + "version": "6.0.0-beta.16", + "resolved": "https://registry.npmjs.org/@angular/flex-layout/-/flex-layout-6.0.0-beta.16.tgz", + "integrity": "sha512-0AYtIBGrEJshdFMc6TXGloCkD19YTCRKVJl6xZHX4H5dLnUn+daqXcbh4UsWhayevnLp85HEf2ViHLmTa6jv3g==", + "requires": { + "tslib": "1.9.2" + } + }, "@angular/forms": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-6.0.3.tgz", @@ -1014,6 +1022,14 @@ "@tweenjs/tween.js": "17.2.0" } }, + "angular6-json-schema-form": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/angular6-json-schema-form/-/angular6-json-schema-form-1.0.3.tgz", + "integrity": "sha512-uskG3UR92PC1A/I2UYYpqGykhSiDbDVV6glCeJFNPMplZetcrKqs1ul0JUVfjbdGmVZtjol9LorfAU/9RgrqEA==", + "requires": { + "tslib": "1.9.2" + } + }, "ansi-colors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", diff --git a/package.json b/package.json index 5dc7cf3c28..d197cd8d3d 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@angular/common": "6.0.3", "@angular/compiler": "6.0.3", "@angular/core": "6.0.3", + "@angular/flex-layout": "^6.0.0-beta.16", "@angular/forms": "6.0.3", "@angular/http": "6.0.3", "@angular/material": "^6.1.0", @@ -59,6 +60,7 @@ "@ngrx/store-devtools": "^6.0.1", "@swimlane/ngx-charts": "^8.1.0", "angular2-virtual-scroll": "^0.3.1", + "angular6-json-schema-form": "1.0.3", "core-js": "^2.5.7", "hammerjs": "^2.0.8", "hyperlist": "^1.0.0-beta", diff --git a/src/frontend/app/core/cf-api-svc.types.ts b/src/frontend/app/core/cf-api-svc.types.ts index 13a2c06a7c..27cecad182 100644 --- a/src/frontend/app/core/cf-api-svc.types.ts +++ b/src/frontend/app/core/cf-api-svc.types.ts @@ -67,12 +67,24 @@ export interface IServicePlan { service?: APIResource; guid?: string; cfGuid?: string; + schemas?: any; +} + +export interface ServicePlanSchemas { + service_instance: ServicePlanSchema; + service_binding: ServicePlanSchema; +} + +export interface ServicePlanSchema { + create?: object; + update?: object; } export interface IServicePlanExtra { displayName: string; bullets: string[]; } + export interface IService { label: string; description: string; diff --git a/src/frontend/app/features/service-catalog/services-helper.ts b/src/frontend/app/features/service-catalog/services-helper.ts index 19c9f6f556..983023c084 100644 --- a/src/frontend/app/features/service-catalog/services-helper.ts +++ b/src/frontend/app/features/service-catalog/services-helper.ts @@ -19,6 +19,7 @@ import { createEntityRelationPaginationKey } from '../../store/helpers/entity-re import { getPaginationObservables } from '../../store/reducers/pagination-reducer/pagination-reducer.helper'; import { APIResource } from '../../store/types/api.types'; import { getIdFromRoute } from '../cloud-foundry/cf.helpers'; +import { JsonPointer } from 'angular6-json-schema-form'; export const getSvcAvailability = (servicePlan: APIResource, @@ -90,23 +91,34 @@ export const getServicePlans = ( cfGuid: string, store: Store, paginationMonitorFactory: PaginationMonitorFactory -): Observable[]> => { +): Observable[]> => { return service$.pipe( filter(p => !!p), switchMap(service => { - if (service.entity.service_plans && service.entity.service_plans.length > 0) { - return observableOf(service.entity.service_plans); - } else { - const guid = service.metadata.guid; - const paginationKey = createEntityRelationPaginationKey(servicePlanSchemaKey, guid); - const getServicePlansAction = new GetServicePlansForService(guid, cfGuid, paginationKey); - // Could be a space-scoped service, make a request to fetch the plan - return getPaginationObservables>({ - store: store, - action: getServicePlansAction, - paginationMonitor: paginationMonitorFactory.create(getServicePlansAction.paginationKey, entityFactory(servicePlanSchemaKey)) - }, true) - .entities$.pipe(share(), first()); - } - })); + if (service.entity.service_plans && service.entity.service_plans.length > 0) { + return observableOf(service.entity.service_plans); + } else { + const guid = service.metadata.guid; + const paginationKey = createEntityRelationPaginationKey(servicePlanSchemaKey, guid); + const getServicePlansAction = new GetServicePlansForService(guid, cfGuid, paginationKey); + // Could be a space-scoped service, make a request to fetch the plan + return getPaginationObservables>({ + store: store, + action: getServicePlansAction, + paginationMonitor: paginationMonitorFactory.create(getServicePlansAction.paginationKey, entityFactory(servicePlanSchemaKey)) + }, true) + .entities$.pipe(share(), first()); + } + })); +}; + +export const prettyValidationErrors = (formValidationErrors: any[]): string => { + if (!formValidationErrors) { return null; } + return formValidationErrors.reduce((a, c) => { + const arrMessage = JsonPointer.parse(c.dataPath).reduce((aa, cc) => { + const dd = /^\d+$/.test(cc) ? `[${cc}]` : `.${cc}`; + return aa + dd; + }, ''); + return `${a} ${arrMessage} ${c.message}
`; + }, ''); }; diff --git a/src/frontend/app/shared/components/add-service-instance/add-service-instance/add-service-instance.component.html b/src/frontend/app/shared/components/add-service-instance/add-service-instance/add-service-instance.component.html index ef99279575..b5b8c0ded8 100644 --- a/src/frontend/app/shared/components/add-service-instance/add-service-instance/add-service-instance.component.html +++ b/src/frontend/app/shared/components/add-service-instance/add-service-instance/add-service-instance.component.html @@ -12,7 +12,7 @@ - + diff --git a/src/frontend/app/shared/components/add-service-instance/add-service-instance/add-service-instance.component.spec.ts b/src/frontend/app/shared/components/add-service-instance/add-service-instance/add-service-instance.component.spec.ts index fcf63f313d..63ad27ecf1 100644 --- a/src/frontend/app/shared/components/add-service-instance/add-service-instance/add-service-instance.component.spec.ts +++ b/src/frontend/app/shared/components/add-service-instance/add-service-instance/add-service-instance.component.spec.ts @@ -20,6 +20,7 @@ import { SelectPlanStepComponent } from '../select-plan-step/select-plan-step.co import { SelectServiceComponent } from '../select-service/select-service.component'; import { SpecifyDetailsStepComponent } from '../specify-details-step/specify-details-step.component'; import { AddServiceInstanceComponent } from './add-service-instance.component'; +import { MaterialDesignFrameworkModule } from 'angular6-json-schema-form'; describe('AddServiceInstanceComponent', () => { let component: AddServiceInstanceComponent; @@ -40,6 +41,7 @@ describe('AddServiceInstanceComponent', () => { imports: [ PageHeaderModule, SteppersModule, + MaterialDesignFrameworkModule, // CoreModule, BaseTestModulesNoShared ], diff --git a/src/frontend/app/shared/components/add-service-instance/bind-apps-step/bind-apps-step.component.html b/src/frontend/app/shared/components/add-service-instance/bind-apps-step/bind-apps-step.component.html index 57e6e33efa..34828aae02 100644 --- a/src/frontend/app/shared/components/add-service-instance/bind-apps-step/bind-apps-step.component.html +++ b/src/frontend/app/shared/components/add-service-instance/bind-apps-step/bind-apps-step.component.html @@ -14,4 +14,25 @@ +
+ + + Generated Form + + + + + +
+
+
+ + + Json schema + + +
{{schema | json}} 
+
+
+
diff --git a/src/frontend/app/shared/components/add-service-instance/bind-apps-step/bind-apps-step.component.scss b/src/frontend/app/shared/components/add-service-instance/bind-apps-step/bind-apps-step.component.scss index 5f81e6c60e..c63e680e6b 100644 --- a/src/frontend/app/shared/components/add-service-instance/bind-apps-step/bind-apps-step.component.scss +++ b/src/frontend/app/shared/components/add-service-instance/bind-apps-step/bind-apps-step.component.scss @@ -1,3 +1,31 @@ +@import '../../../../../sass/mixins'; :host { flex: 1; } + +@mixin json-schema-component() { + .data-bad { background-color: #fcc; } + .display-json { + white-space: -moz-pre-wrap; + white-space: -pre-wrap; + white-space: -o-pre-wrap; + white-space: pre-wrap; + word-wrap: break-word; + } + .json-schema { + display: flex; + flex-direction: column; + @include breakpoint(laptop) { + flex-direction: row; + } + } + .json-schema-data, + .json-schema-form { + width: 100%; + @include breakpoint(laptop) { + width: 50%; + } + } +} + +@include json-schema-component(); diff --git a/src/frontend/app/shared/components/add-service-instance/bind-apps-step/bind-apps-step.component.spec.ts b/src/frontend/app/shared/components/add-service-instance/bind-apps-step/bind-apps-step.component.spec.ts index c4cc82eedc..e72e1b8494 100644 --- a/src/frontend/app/shared/components/add-service-instance/bind-apps-step/bind-apps-step.component.spec.ts +++ b/src/frontend/app/shared/components/add-service-instance/bind-apps-step/bind-apps-step.component.spec.ts @@ -6,6 +6,7 @@ import { ServicesService } from '../../../../features/service-catalog/services.s import { ServicesServiceMock } from '../../../../features/service-catalog/services.service.mock'; import { CsiGuidsService } from '../csi-guids.service'; import { PaginationMonitorFactory } from '../../../monitors/pagination-monitor.factory'; +import { MaterialDesignFrameworkModule } from 'angular6-json-schema-form'; describe('BindAppsStepComponent', () => { let component: BindAppsStepComponent; @@ -14,7 +15,7 @@ describe('BindAppsStepComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [BindAppsStepComponent], - imports: [BaseTestModulesNoShared], + imports: [BaseTestModulesNoShared , MaterialDesignFrameworkModule], providers: [ { provide: ServicesService, useClass: ServicesServiceMock }, CsiGuidsService, diff --git a/src/frontend/app/shared/components/add-service-instance/bind-apps-step/bind-apps-step.component.ts b/src/frontend/app/shared/components/add-service-instance/bind-apps-step/bind-apps-step.component.ts index c24b579bc9..537ec8af8b 100644 --- a/src/frontend/app/shared/components/add-service-instance/bind-apps-step/bind-apps-step.component.ts +++ b/src/frontend/app/shared/components/add-service-instance/bind-apps-step/bind-apps-step.component.ts @@ -18,6 +18,7 @@ import { PaginationMonitorFactory } from '../../../monitors/pagination-monitor.f import { StepOnNextResult } from '../../stepper/step/step.component'; import { CsiGuidsService } from '../csi-guids.service'; import { SpecifyDetailsStepComponent } from '../specify-details-step/specify-details-step.component'; +import { safeUnsubscribe, prettyValidationErrors } from '../../../../features/service-catalog/services-helper'; @Component({ selector: 'app-bind-apps-step', @@ -35,6 +36,14 @@ export class BindAppsStepComponent implements OnDestroy, AfterContentInit { stepperForm: FormGroup; apps$: Observable[]>; guideText = 'Specify the application to bind (Optional)'; + + selectedFramework = 'material-design'; + schema: any; + showJsonSchema: boolean; + jsonFormOptions: any = { addSubmit: false }; + selectedServiceSubscription: Subscription; + formValidationErrors: any; + selectedService$: any; constructor( private store: Store, private paginationMonitorFactory: PaginationMonitorFactory @@ -53,6 +62,23 @@ export class BindAppsStepComponent implements OnDestroy, AfterContentInit { } } + onFormChange(jsonData) { + if (!!jsonData) { + try { + const stringData = JSON.stringify(jsonData); + this.stepperForm.get('params').setValue(stringData); + } catch { } + } + } + + validationErrors(data: any): void { + this.formValidationErrors = data; + } + + get prettyValidationErrors() { + return prettyValidationErrors(this.formValidationErrors); + } + ngAfterContentInit() { this.validateSubscription = this.stepperForm.statusChanges.pipe( map(() => { @@ -80,9 +106,27 @@ export class BindAppsStepComponent implements OnDestroy, AfterContentInit { this.setBoundApp(); } + onEnter = (selectedService$?) => { + this.selectedService$ = selectedService$; + if (selectedService$ instanceof Observable) { + this.selectedServiceSubscription = selectedService$.pipe( + first() + ).subscribe(selectedService => { + this.schema = this.filterSchema(selectedService.entity.entity.schemas.service_binding.create.parameters); + }); + } + } + + private filterSchema = (schema: any): any => { + return Object.keys(schema).reduce((obj, key) => { + if (key !== '$schema') { obj[key] = schema[key]; } + return obj; + }, {}); + } + submit = (): Observable => { this.setApp(); - return observableOf({ success: true }); + return observableOf({ success: true, data: this.selectedService$ }); } setApp = () => this.store.dispatch( @@ -91,6 +135,7 @@ export class BindAppsStepComponent implements OnDestroy, AfterContentInit { ngOnDestroy(): void { this.validateSubscription.unsubscribe(); + safeUnsubscribe(this.selectedServiceSubscription); } } diff --git a/src/frontend/app/shared/components/add-service-instance/select-plan-step/select-plan-step.component.ts b/src/frontend/app/shared/components/add-service-instance/select-plan-step/select-plan-step.component.ts index 9b39613211..014531c714 100644 --- a/src/frontend/app/shared/components/add-service-instance/select-plan-step/select-plan-step.component.ts +++ b/src/frontend/app/shared/components/add-service-instance/select-plan-step/select-plan-step.component.ts @@ -169,7 +169,7 @@ export class SelectPlanStepComponent implements OnDestroy { onNext = (): Observable => { this.store.dispatch(new SetCreateServiceInstanceServicePlan(this.stepperForm.controls.servicePlans.value)); - return observableOf({ success: true }); + return observableOf({ success: true, data: this.selectedService$ }); } ngOnDestroy(): void { diff --git a/src/frontend/app/shared/components/add-service-instance/specify-details-step/specify-details-step.component.html b/src/frontend/app/shared/components/add-service-instance/specify-details-step/specify-details-step.component.html index c6da3d2a0b..c6eb17d8b4 100644 --- a/src/frontend/app/shared/components/add-service-instance/specify-details-step/specify-details-step.component.html +++ b/src/frontend/app/shared/components/add-service-instance/specify-details-step/specify-details-step.component.html @@ -37,6 +37,26 @@ {{ sI.entity.name }} - +
+ + + Generated Form + + + + + +
+
+
+ + + Json schema + + +
{{schema | json}} 
+
+
+
diff --git a/src/frontend/app/shared/components/add-service-instance/specify-details-step/specify-details-step.component.scss b/src/frontend/app/shared/components/add-service-instance/specify-details-step/specify-details-step.component.scss index 356c6033ab..c29e27e552 100644 --- a/src/frontend/app/shared/components/add-service-instance/specify-details-step/specify-details-step.component.scss +++ b/src/frontend/app/shared/components/add-service-instance/specify-details-step/specify-details-step.component.scss @@ -1,3 +1,4 @@ +@import '../bind-apps-step/bind-apps-step.component'; :host { flex: 1; } @@ -23,3 +24,5 @@ } } } + +@include json-schema-component(); diff --git a/src/frontend/app/shared/components/add-service-instance/specify-details-step/specify-details-step.component.spec.ts b/src/frontend/app/shared/components/add-service-instance/specify-details-step/specify-details-step.component.spec.ts index a6731a2d45..ed838bd1f3 100644 --- a/src/frontend/app/shared/components/add-service-instance/specify-details-step/specify-details-step.component.spec.ts +++ b/src/frontend/app/shared/components/add-service-instance/specify-details-step/specify-details-step.component.spec.ts @@ -7,6 +7,7 @@ import { CsiGuidsService } from '../csi-guids.service'; import { PaginationMonitorFactory } from '../../../monitors/pagination-monitor.factory'; import { EntityMonitorFactory } from '../../../monitors/entity-monitor.factory.service'; import { CsiModeService } from '../csi-mode.service'; +import { MaterialDesignFrameworkModule } from 'angular6-json-schema-form'; describe('SpecifyDetailsStepComponent', () => { let component: SpecifyDetailsStepComponent; @@ -15,7 +16,7 @@ describe('SpecifyDetailsStepComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [SpecifyDetailsStepComponent], - imports: [BaseTestModulesNoShared], + imports: [BaseTestModulesNoShared, MaterialDesignFrameworkModule], providers: [ CreateServiceInstanceHelperServiceFactory, CsiGuidsService, diff --git a/src/frontend/app/shared/components/add-service-instance/specify-details-step/specify-details-step.component.ts b/src/frontend/app/shared/components/add-service-instance/specify-details-step/specify-details-step.component.ts index 81961efc47..7962a87e53 100644 --- a/src/frontend/app/shared/components/add-service-instance/specify-details-step/specify-details-step.component.ts +++ b/src/frontend/app/shared/components/add-service-instance/specify-details-step/specify-details-step.component.ts @@ -19,7 +19,7 @@ import { tap } from 'rxjs/operators'; import { IServiceInstance } from '../../../../core/cf-api-svc.types'; -import { getServiceJsonParams } from '../../../../features/service-catalog/services-helper'; +import { getServiceJsonParams, safeUnsubscribe, prettyValidationErrors } from '../../../../features/service-catalog/services-helper'; import { GetAppEnvVarsAction } from '../../../../store/actions/app-metadata.actions'; import { SetCreateServiceInstanceOrg, SetServiceInstanceGuid } from '../../../../store/actions/create-service-instance.actions'; import { RouterNav } from '../../../../store/actions/router.actions'; @@ -41,7 +41,6 @@ import { CreateServiceInstanceHelper } from '../create-service-instance-helper.s import { CsiGuidsService } from '../csi-guids.service'; import { CsiModeService } from '../csi-mode.service'; - const enum FormMode { CreateServiceInstance = 'create-service-instance', BindServiceInstance = 'bind-service-instance', @@ -90,8 +89,14 @@ export class SpecifyDetailsStepComponent implements OnDestroy, AfterContentInit separatorKeysCodes = [ENTER, COMMA, SPACE]; tags = []; spaceScopeSub: Subscription; + selectedServiceSubscription: Subscription; bindExistingInstance = false; subscriptions: Subscription[] = []; + selectedFramework = 'material-design'; + schema: any; + showJsonSchema: boolean; + jsonFormOptions: any = { addSubmit: false }; + formValidationErrors: any; static isValidJsonValidatorFn = (): ValidatorFn => { return (formField: AbstractControl): { [key: string]: any } => { @@ -179,7 +184,15 @@ export class SpecifyDetailsStepComponent implements OnDestroy, AfterContentInit ); } - onEnter = () => { + onEnter = (selectedService$?) => { + if (selectedService$ instanceof Observable) { + this.selectedServiceSubscription = selectedService$ + .subscribe(selectedService => { + if (!!this.modeService.isEditServiceInstanceMode()) { + this.schema = this.filterSchema(selectedService.entity.entity.schemas.service_instance.create.parameters); + } else { this.schema = this.filterSchema(selectedService.entity.entity.schemas.service_instance.update.parameters); } + }); + } this.formMode = FormMode.CreateServiceInstance; this.allServiceInstances$ = this.cSIHelperService.getServiceInstancesForService(null, null, this.csiGuidsService.cfGuid); if (this.modeService.isEditServiceInstanceMode()) { @@ -200,6 +213,13 @@ export class SpecifyDetailsStepComponent implements OnDestroy, AfterContentInit this.subscriptions.push(this.setupFormValidatorData()); } + private filterSchema = (schema: any): any => { + return Object.keys(schema).reduce((obj, key) => { + if (key !== '$schema') { obj[key] = schema[key]; } + return obj; + }, {}); + } + resetForms = (mode: FormMode) => { this.validate.next(false); this.createNewInstanceForm.reset(); @@ -247,12 +267,30 @@ export class SpecifyDetailsStepComponent implements OnDestroy, AfterContentInit ngOnDestroy(): void { this.subscriptions.forEach(s => s.unsubscribe()); + safeUnsubscribe(this.selectedServiceSubscription); } ngAfterContentInit() { this.setupValidate(); } + onFormChange(jsonData) { + if (!!jsonData) { + try { + const stringData = JSON.stringify(jsonData); + this.createNewInstanceForm.get('params').setValue(stringData); + } catch { } + } + } + + validationErrors(data: any): void { + this.formValidationErrors = data; + } + + get prettyValidationErrors() { + return prettyValidationErrors(this.formValidationErrors); + } + onNext = (): Observable => { return this.store.select(selectCreateServiceInstance).pipe( filter(p => !!p), @@ -410,8 +448,8 @@ export class SpecifyDetailsStepComponent implements OnDestroy, AfterContentInit filter(a => !a.creating), switchMap(a => { const updating = a.updating ? a.updating[UpdateServiceInstance.updateServiceInstance] : null; - if ( (isEditMode && !!updating && updating.error) || (a.error) ) { - return create$; + if ((isEditMode && !!updating && updating.error) || (a.error)) { + return create$; } const guid = getIdFromResponse(a.response as NormalizedResponse); diff --git a/src/frontend/app/shared/shared.module.ts b/src/frontend/app/shared/shared.module.ts index d34a8a754f..f26aef5008 100644 --- a/src/frontend/app/shared/shared.module.ts +++ b/src/frontend/app/shared/shared.module.ts @@ -133,7 +133,7 @@ import { UserPermissionDirective } from './user-permission.directive'; import { CfEndpointsMissingComponent } from './components/cf-endpoints-missing/cf-endpoints-missing.component'; import { CapitalizeFirstPipe } from './pipes/capitalizeFirstLetter.pipe'; import { RoutingIndicatorComponent } from './components/routing-indicator/routing-indicator.component'; - +import { MaterialDesignFrameworkModule } from 'angular6-json-schema-form'; @NgModule({ imports: [ CommonModule, @@ -144,6 +144,7 @@ import { RoutingIndicatorComponent } from './components/routing-indicator/routin CfAuthModule, CdkTableModule, NgxChartsModule, + MaterialDesignFrameworkModule ], declarations: [ LoadingPageComponent,