diff --git a/src/frontend/packages/core/sass/_all-theme.scss b/src/frontend/packages/core/sass/_all-theme.scss index 8b73b61d31..c4707aeed1 100644 --- a/src/frontend/packages/core/sass/_all-theme.scss +++ b/src/frontend/packages/core/sass/_all-theme.scss @@ -39,6 +39,7 @@ @import '../src/features/applications/application-wall/application-wall.component.theme'; @import '../src/features/services/services-wall/services-wall.component.theme'; @import '../src/features/service-catalog/service-catalog-page/service-catalog-page.component.theme'; +@import '../src/shared/components/tile-selector/tile-selector.component.theme'; @import '../src/shared/components/multiline-title/multiline-title.component.theme'; @import './components/mat-tabs.theme'; @import './components/text-status.theme'; @@ -127,6 +128,7 @@ $side-nav-light-active: #484848; @include metrics-chart-theme($theme, $app-theme); @include metrics-range-selector-theme($theme, $app-theme); @include app-multiline-title-theme($theme, $app-theme); + @include tile-selector-theme($theme, $app-theme); } @function app-generate-nav-theme($theme, $nav-theme: null) { diff --git a/src/frontend/packages/core/src/core/cf-api-svc.types.ts b/src/frontend/packages/core/src/core/cf-api-svc.types.ts index 767e7a82ff..09c312ed03 100644 --- a/src/frontend/packages/core/src/core/cf-api-svc.types.ts +++ b/src/frontend/packages/core/src/core/cf-api-svc.types.ts @@ -1,5 +1,5 @@ import { APIResource } from '../../../store/src/types/api.types'; -import { IApp, IOrganization, ISpace } from './cf-api.types'; +import { IApp, IOrganization, IRoute, ISpace } from './cf-api.types'; export interface ILastOperation { type: string; @@ -21,7 +21,7 @@ export interface IServiceBinding { app_url: string; app?: APIResource; service_instance_url: string; - service_instance?: APIResource; + service_instance?: APIResource | APIResource; guid?: string; cfGuid?: string; } @@ -147,3 +147,20 @@ export interface IServiceBroker { guid?: string; cfGuid?: string; } + +export interface IUserProvidedServiceInstance { + name: string; + credentials: { [name: string]: string }; + space_guid: string; + space: APIResource; + space_url: string; + type: string; + syslog_drain_url: string; + tags: string[]; + service_bindings: APIResource[]; + service_bindings_url: string; + routes: APIResource[]; + routes_url: string; + route_service_url: string; + cfGuid?: string; +} diff --git a/src/frontend/packages/core/src/features/applications/app-name-unique.directive/app-name-unique.directive.ts b/src/frontend/packages/core/src/features/applications/app-name-unique.directive/app-name-unique.directive.ts deleted file mode 100644 index e78b9c7e3a..0000000000 --- a/src/frontend/packages/core/src/features/applications/app-name-unique.directive/app-name-unique.directive.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { Directive, forwardRef, Input, OnInit } from '@angular/core'; -import { AbstractControl, AsyncValidator, NG_ASYNC_VALIDATORS } from '@angular/forms'; -import { Headers, Http, Request, RequestOptions, URLSearchParams } from '@angular/http'; -import { Store } from '@ngrx/store'; -import { Observable, of as observableOf, throwError as observableThrowError, timer as observableTimer } from 'rxjs'; -import { catchError, combineLatest, map, switchMap, take } from 'rxjs/operators'; - -import { AppState } from '../../../../../store/src/app-state'; -import { selectNewAppState } from '../../../../../store/src/effects/create-app-effects'; -import { environment } from '../../../environments/environment.prod'; - - -/* tslint:disable:no-use-before-declare */ -const APP_UNIQUE_NAME_PROVIDER = { - provide: NG_ASYNC_VALIDATORS, useExisting: forwardRef(() => AppNameUniqueDirective), multi: true -}; -/* tslint:enable */ - -// See: https://medium.com/@kahlil/asynchronous-validation-with-angular-reactive-forms-1a392971c062 - -const { proxyAPIVersion, cfAPIVersion } = environment; - -export class AppNameUniqueChecking { - busy: boolean; - taken: boolean; - status: string; - - set(busy: boolean, taken?: boolean) { - this.busy = busy; - this.taken = taken; - - if (this.busy) { - this.status = 'busy'; - } else if (this.taken === undefined) { - this.status = ''; - } else { - this.status = this.taken ? 'error' : 'done'; - } - } -} - -@Directive({ - selector: '[appApplicationNameUnique][formControlName],[appApplicationNameUnique][formControl],[appApplicationNameUnique][ngModel]', - providers: [APP_UNIQUE_NAME_PROVIDER] -}) -export class AppNameUniqueDirective implements AsyncValidator, OnInit { - - @Input() appApplicationNameUnique: AppNameUniqueChecking; - - constructor( - private store: Store, - private http: Http, - ) { - if (!this.appApplicationNameUnique) { - this.appApplicationNameUnique = new AppNameUniqueChecking(); - } - } - - ngOnInit(): void { - this.appApplicationNameUnique.set(false); - } - - public validate(control: AbstractControl): Observable<{ appNameTaken: boolean } | null> { - if (!control.dirty) { - return observableOf(null); - } - this.appApplicationNameUnique.set(true); - return observableTimer(500).pipe(take(1), - combineLatest(this.store.select(selectNewAppState).pipe(take(1))), - switchMap(newAppState => { - const cfGuid = newAppState[1].cloudFoundryDetails.cloudFoundry; - const spaceGuid = newAppState[1].cloudFoundryDetails.space; - const currentName = newAppState[1].name; - return this.appNameTaken(cfGuid, spaceGuid, currentName, control.value); - }), - map(appNameTaken => { - this.appApplicationNameUnique.set(false, appNameTaken); - return appNameTaken ? { appNameTaken } : null; - }), - catchError(err => { - this.appApplicationNameUnique.set(false); - return observableThrowError(err); - })); - } - - private appNameTaken(cfGuid, spaceGuid, currentName, name): Observable { - if (name.length === 0) { - return observableOf(undefined); - } - const options = new RequestOptions(); - options.url = `/pp/${proxyAPIVersion}/proxy/${cfAPIVersion}/apps`; - options.params = new URLSearchParams(''); - options.params.set('q', 'name:' + name); - options.params.append('q', 'space_guid:' + spaceGuid); - options.method = 'get'; - options.headers = new Headers(); - options.headers.set('x-cap-cnsi-list', cfGuid); - options.headers.set('x-cap-passthrough', 'true'); - return this.http.request(new Request(options)).pipe( - map(response => { - let resData; - try { - resData = response.json(); - } catch (e) { - resData = {}; - } - return resData.total_results > 0; - })); - } -} diff --git a/src/frontend/packages/core/src/features/applications/application-delete/application-delete.component.html b/src/frontend/packages/core/src/features/applications/application-delete/application-delete.component.html index cd75e0d1f9..96e7ef964e 100644 --- a/src/frontend/packages/core/src/features/applications/application-delete/application-delete.component.html +++ b/src/frontend/packages/core/src/features/applications/application-delete/application-delete.component.html @@ -7,32 +7,50 @@

Delete application

Please select any attached application routes you would like to delete.

- + +

Please select any attached application service instances you would like to delete.

- + +
- +
-

Please confirm that you would like to delete the following application and related entities:

+

Please confirm that you would like to delete the following application + and related entities:

Application

- +

Routes

- +
-

Services

- + +

Services

+ + +
+ +

User Provided Services

+ +
- + \ No newline at end of file diff --git a/src/frontend/packages/core/src/features/applications/application-delete/application-delete.component.ts b/src/frontend/packages/core/src/features/applications/application-delete/application-delete.component.ts index 34a808bc31..73309d7601 100644 --- a/src/frontend/packages/core/src/features/applications/application-delete/application-delete.component.ts +++ b/src/frontend/packages/core/src/features/applications/application-delete/application-delete.component.ts @@ -4,6 +4,21 @@ import { Store } from '@ngrx/store'; import { combineLatest, Observable, ReplaySubject } from 'rxjs'; import { filter, first, map, pairwise, shareReplay, startWith, switchMap, tap } from 'rxjs/operators'; +import { GetAppRoutes } from '../../../../../store/src/actions/application-service-routes.actions'; +import { DeleteApplication, GetApplication } from '../../../../../store/src/actions/application.actions'; +import { DeleteRoute } from '../../../../../store/src/actions/route.actions'; +import { RouterNav } from '../../../../../store/src/actions/router.actions'; +import { DeleteServiceInstance } from '../../../../../store/src/actions/service-instances.actions'; +import { DeleteUserProvidedInstance } from '../../../../../store/src/actions/user-provided-service.actions'; +import { AppState } from '../../../../../store/src/app-state'; +import { + applicationSchemaKey, + entityFactory, + routeSchemaKey, + serviceInstancesSchemaKey, + userProvidedServiceInstanceSchemaKey, +} from '../../../../../store/src/helpers/entity-factory'; +import { APIResource } from '../../../../../store/src/types/api.types'; import { IServiceBinding } from '../../../core/cf-api-svc.types'; import { IApp, IRoute } from '../../../core/cf-api.types'; import { @@ -14,12 +29,6 @@ import { ITableColumn } from '../../../shared/components/list/list-table/table.t import { CfAppRoutesListConfigService, } from '../../../shared/components/list/list-types/app-route/cf-app-routes-list-config.service'; -import { - TableCellRouteComponent, -} from '../../../shared/components/list/list-types/cf-routes/table-cell-route/table-cell-route.component'; -import { - TableCellTCPRouteComponent, -} from '../../../shared/components/list/list-types/cf-routes/table-cell-tcproute/table-cell-tcproute.component'; import { AppServiceBindingDataSource, } from '../../../shared/components/list/list-types/app-sevice-bindings/app-service-binding-data-source'; @@ -32,24 +41,18 @@ import { import { TableCellAppStatusComponent, } from '../../../shared/components/list/list-types/app/table-cell-app-status/table-cell-app-status.component'; +import { + TableCellRouteComponent, +} from '../../../shared/components/list/list-types/cf-routes/table-cell-route/table-cell-route.component'; +import { + TableCellTCPRouteComponent, +} from '../../../shared/components/list/list-types/cf-routes/table-cell-tcproute/table-cell-tcproute.component'; import { EntityMonitor } from '../../../shared/monitors/entity-monitor'; import { EntityMonitorFactory } from '../../../shared/monitors/entity-monitor.factory.service'; import { PaginationMonitor } from '../../../shared/monitors/pagination-monitor'; import { PaginationMonitorFactory } from '../../../shared/monitors/pagination-monitor.factory'; +import { isServiceInstance, isUserProvidedServiceInstance } from '../../cloud-foundry/cf.helpers'; import { ApplicationService } from '../application.service'; -import { GetApplication, DeleteApplication } from '../../../../../store/src/actions/application.actions'; -import { AppState } from '../../../../../store/src/app-state'; -import { RouterNav } from '../../../../../store/src/actions/router.actions'; -import { GetAppRoutes } from '../../../../../store/src/actions/application-service-routes.actions'; -import { DeleteRoute } from '../../../../../store/src/actions/route.actions'; -import { DeleteServiceInstance } from '../../../../../store/src/actions/service-instances.actions'; -import { APIResource } from '../../../../../store/src/types/api.types'; -import { - applicationSchemaKey, - entityFactory, - routeSchemaKey, - serviceInstancesSchemaKey, -} from '../../../../../store/src/helpers/entity-factory'; @Component({ @@ -77,7 +80,10 @@ export class ApplicationDeleteComponent { columnId: 'service', headerCell: () => 'Service', cellDefinition: { - getValue: (row) => row.entity.service_instance.entity.service.entity.label + getValue: (row) => { + const si = isServiceInstance(row.entity.service_instance.entity); + return si ? si.service.entity.label : 'User Service'; + } }, cellFlex: '2' }, @@ -148,9 +154,11 @@ export class ApplicationDeleteComponent { public selectedApplication$: Observable[]>; public selectedRoutes$ = new ReplaySubject[]>(1); public selectedServiceInstances$ = new ReplaySubject[]>(1); + public selectedUserServiceInstances$ = new ReplaySubject[]>(1); public fetchingApplicationData$: Observable; public serviceInstancesSchemaKey = serviceInstancesSchemaKey; + public userProvidedServiceInstanceSchemaKey = userProvidedServiceInstanceSchemaKey; public routeSchemaKey = routeSchemaKey; public applicationSchemaKey = applicationSchemaKey; public deletingState = AppMonitorComponentTypes.DELETE; @@ -262,12 +270,21 @@ export class ApplicationDeleteComponent { ); } - private setSelectedServiceInstances(selected: APIResource[]) { + public setSelectedServiceInstances(selected: APIResource[]) { this.selectedServiceInstances = selected; - this.selectedServiceInstances$.next(selected); + const selectedServices = selected.reduce((res, binding) => { + if (isUserProvidedServiceInstance(binding.entity.service_instance.entity)) { + res.upsi.push(binding); + } else { + res.si.push(binding); + } + return res; + }, { si: [], upsi: [] }); + this.selectedServiceInstances$.next(selectedServices.si); + this.selectedUserServiceInstances$.next(selectedServices.upsi); } - private setSelectedRoutes(selected: APIResource[]) { + public setSelectedRoutes(selected: APIResource[]) { this.selectedRoutes = selected; this.selectedRoutes$.next(selected); } @@ -302,7 +319,11 @@ export class ApplicationDeleteComponent { } if (this.selectedServiceInstances && this.selectedServiceInstances.length) { this.selectedServiceInstances.forEach(instance => { - this.store.dispatch(new DeleteServiceInstance(this.applicationService.cfGuid, instance.entity.service_instance_guid)); + if (isUserProvidedServiceInstance(instance.entity.service_instance.entity)) { + this.store.dispatch(new DeleteUserProvidedInstance(this.applicationService.cfGuid, instance.entity.service_instance_guid)); + } else { + this.store.dispatch(new DeleteServiceInstance(this.applicationService.cfGuid, instance.entity.service_instance_guid)); + } }); } } diff --git a/src/frontend/packages/core/src/features/applications/application-delete/delete-app-instances/app-delete-instances-routes-list-config.service.ts b/src/frontend/packages/core/src/features/applications/application-delete/delete-app-instances/app-delete-instances-routes-list-config.service.ts index 986b462669..fa84909bc0 100644 --- a/src/frontend/packages/core/src/features/applications/application-delete/delete-app-instances/app-delete-instances-routes-list-config.service.ts +++ b/src/frontend/packages/core/src/features/applications/application-delete/delete-app-instances/app-delete-instances-routes-list-config.service.ts @@ -41,11 +41,12 @@ export class AppDeleteServiceInstancesListConfigService extends AppServiceBindin return action; } - constructor(store: Store, - appService: ApplicationService, - datePipe: DatePipe, - currentUserPermissionService: CurrentUserPermissionsService, - private paginationMonitorFactory: PaginationMonitorFactory + constructor( + store: Store, + appService: ApplicationService, + datePipe: DatePipe, + currentUserPermissionService: CurrentUserPermissionsService, + private paginationMonitorFactory: PaginationMonitorFactory ) { super(store, appService, datePipe, currentUserPermissionService); diff --git a/src/frontend/packages/core/src/features/applications/application-wall/application-wall.component.ts b/src/frontend/packages/core/src/features/applications/application-wall/application-wall.component.ts index c633a0c6a4..5278130cac 100644 --- a/src/frontend/packages/core/src/features/applications/application-wall/application-wall.component.ts +++ b/src/frontend/packages/core/src/features/applications/application-wall/application-wall.component.ts @@ -4,15 +4,14 @@ import { Store } from '@ngrx/store'; import { Observable, Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; +import { AppState } from '../../../../../store/src/app-state'; +import { applicationSchemaKey } from '../../../../../store/src/helpers/entity-factory'; import { CurrentUserPermissions } from '../../../core/current-user-permissions.config'; -import { CardAppComponent } from '../../../shared/components/list/list-types/app/card/card-app.component'; import { CfAppConfigService } from '../../../shared/components/list/list-types/app/cf-app-config.service'; import { CfAppsDataSource } from '../../../shared/components/list/list-types/app/cf-apps-data-source'; import { ListConfig } from '../../../shared/components/list/list.component.types'; import { CfOrgSpaceDataService, initCfOrgSpaceService } from '../../../shared/data-services/cf-org-space-service.service'; import { CloudFoundryService } from '../../../shared/data-services/cloud-foundry.service'; -import { AppState } from '../../../../../store/src/app-state'; -import { applicationSchemaKey } from '../../../../../store/src/helpers/entity-factory'; @Component({ selector: 'app-application-wall', @@ -66,8 +65,6 @@ export class ApplicationWallComponent implements OnDestroy { CfAppsDataSource.paginationKey).subscribe(); } - cardComponent = CardAppComponent; - ngOnDestroy(): void { this.initCfOrgSpaceService.unsubscribe(); } diff --git a/src/frontend/packages/core/src/features/applications/applications.routing.ts b/src/frontend/packages/core/src/features/applications/applications.routing.ts index 18157ca6b7..b7fc5477f9 100644 --- a/src/frontend/packages/core/src/features/applications/applications.routing.ts +++ b/src/frontend/packages/core/src/features/applications/applications.routing.ts @@ -4,6 +4,9 @@ import { RouterModule, Routes } from '@angular/router'; import { DynamicExtensionRoutes } from '../../core/extension/dynamic-extension-routes'; import { StratosActionType, StratosTabType } from '../../core/extension/extension-service'; import { PageNotFoundComponentComponent } from '../../core/page-not-found-component/page-not-found-component.component'; +import { + AddServiceInstanceBaseStepComponent, +} from '../../shared/components/add-service-instance/add-service-instance-base-step/add-service-instance-base-step.component'; import { AddServiceInstanceComponent, } from '../../shared/components/add-service-instance/add-service-instance/add-service-instance.component'; @@ -71,7 +74,14 @@ const applicationsRoutes: Routes = [ }, { path: 'bind', - component: AddServiceInstanceComponent + component: AddServiceInstanceBaseStepComponent, + data: { + bind: true + } + }, + { + path: 'bind/:type', + component: AddServiceInstanceComponent, }, { path: '', diff --git a/src/frontend/packages/core/src/features/applications/create-application/create-application-step2/create-application-step2.component.spec.ts b/src/frontend/packages/core/src/features/applications/create-application/create-application-step2/create-application-step2.component.spec.ts index 58b67e04f0..298d9e8676 100644 --- a/src/frontend/packages/core/src/features/applications/create-application/create-application-step2/create-application-step2.component.spec.ts +++ b/src/frontend/packages/core/src/features/applications/create-application/create-application-step2/create-application-step2.component.spec.ts @@ -7,9 +7,10 @@ import { CoreModule } from '../../../../core/core.module'; import { SharedModule } from '../../../../shared/shared.module'; import { appReducers } from '../../../../../../store/src/reducers.module'; import { CreateApplicationStep2Component } from './create-application-step2.component'; -import { AppNameUniqueDirective } from '../../app-name-unique.directive/app-name-unique.directive'; import { HttpModule, ConnectionBackend, Http } from '@angular/http'; import { MockBackend } from '@angular/http/testing'; +import { HttpClientModule } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; describe('CreateApplicationStep2Component', () => { let component: CreateApplicationStep2Component; @@ -18,8 +19,7 @@ describe('CreateApplicationStep2Component', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ - CreateApplicationStep2Component, - AppNameUniqueDirective, + CreateApplicationStep2Component ], imports: [ CommonModule, @@ -30,6 +30,8 @@ describe('CreateApplicationStep2Component', () => { appReducers ), HttpModule, + HttpClientModule, + HttpClientTestingModule, ], providers: [ { diff --git a/src/frontend/packages/core/src/features/applications/create-application/create-application-step2/create-application-step2.component.ts b/src/frontend/packages/core/src/features/applications/create-application/create-application-step2/create-application-step2.component.ts index 2dbac4c4e5..57d1f022d6 100644 --- a/src/frontend/packages/core/src/features/applications/create-application/create-application-step2/create-application-step2.component.ts +++ b/src/frontend/packages/core/src/features/applications/create-application/create-application-step2/create-application-step2.component.ts @@ -8,9 +8,9 @@ import { ErrorStateMatcher, ShowOnDirtyErrorStateMatcher } from '@angular/materi import { Store } from '@ngrx/store'; import { StepOnNextFunction } from '../../../../shared/components/stepper/step/step.component'; -import { AppNameUniqueChecking } from '../../app-name-unique.directive/app-name-unique.directive'; import { AppState } from '../../../../../../store/src/app-state'; import { SetNewAppName } from '../../../../../../store/src/actions/create-applications-page.actions'; +import { AppNameUniqueChecking } from '../../../../shared/app-name-unique.directive/app-name-unique.directive'; @Component({ selector: 'app-create-application-step2', diff --git a/src/frontend/packages/core/src/features/applications/create-application/create-application.component.spec.ts b/src/frontend/packages/core/src/features/applications/create-application/create-application.component.spec.ts index 9251d76fa6..3e753187f9 100644 --- a/src/frontend/packages/core/src/features/applications/create-application/create-application.component.spec.ts +++ b/src/frontend/packages/core/src/features/applications/create-application/create-application.component.spec.ts @@ -7,22 +7,18 @@ import { RouterTestingModule } from '@angular/router/testing'; import { StoreModule } from '@ngrx/store'; import { CoreModule } from '../../../core/core.module'; -import { - CreateApplicationStep1Component, -} from '../../../shared/components/create-application/create-application-step1/create-application-step1.component'; -import { FocusDirective } from '../../../shared/components/focus.directive'; -import { PageHeaderModule } from '../../../shared/components/page-header/page-header.module'; -import { StatefulIconComponent } from '../../../shared/components/stateful-icon/stateful-icon.component'; -import { SteppersModule } from '../../../shared/components/stepper/steppers.module'; + import { CloudFoundryService } from '../../../shared/data-services/cloud-foundry.service'; import { EntityMonitorFactory } from '../../../shared/monitors/entity-monitor.factory.service'; import { InternalEventMonitorFactory } from '../../../shared/monitors/internal-event-monitor.factory'; import { PaginationMonitorFactory } from '../../../shared/monitors/pagination-monitor.factory'; -import { AppNameUniqueDirective } from '../app-name-unique.directive/app-name-unique.directive'; import { CreateApplicationStep2Component } from './create-application-step2/create-application-step2.component'; import { CreateApplicationStep3Component } from './create-application-step3/create-application-step3.component'; import { CreateApplicationComponent } from './create-application.component'; import { appReducers } from '../../../../../store/src/reducers.module'; +import { SharedModule } from '../../../shared/shared.module'; +import { HttpClientModule } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; describe('CreateApplicationComponent', () => { let component: CreateApplicationComponent; @@ -32,12 +28,8 @@ describe('CreateApplicationComponent', () => { TestBed.configureTestingModule({ declarations: [ CreateApplicationComponent, - CreateApplicationStep1Component, CreateApplicationStep2Component, - CreateApplicationStep3Component, - AppNameUniqueDirective, - StatefulIconComponent, - FocusDirective + CreateApplicationStep3Component ], imports: [ CommonModule, @@ -45,18 +37,14 @@ describe('CreateApplicationComponent', () => { HttpModule, RouterTestingModule, BrowserAnimationsModule, - PageHeaderModule, - SteppersModule, + SharedModule, + HttpClientModule, + HttpClientTestingModule, StoreModule.forRoot( appReducers ) ], providers: [ - Http, - { - provide: ConnectionBackend, - useClass: MockBackend - }, PaginationMonitorFactory, EntityMonitorFactory, InternalEventMonitorFactory, diff --git a/src/frontend/packages/core/src/features/applications/create-application/create-application.module.ts b/src/frontend/packages/core/src/features/applications/create-application/create-application.module.ts index 397c971256..41950ac5fd 100644 --- a/src/frontend/packages/core/src/features/applications/create-application/create-application.module.ts +++ b/src/frontend/packages/core/src/features/applications/create-application/create-application.module.ts @@ -3,7 +3,6 @@ import { NgModule } from '@angular/core'; import { CoreModule } from '../../../core/core.module'; import { SharedModule } from '../../../shared/shared.module'; -import { AppNameUniqueDirective } from '../app-name-unique.directive/app-name-unique.directive'; import { CreateApplicationStep2Component } from './create-application-step2/create-application-step2.component'; import { CreateApplicationStep3Component } from './create-application-step3/create-application-step3.component'; import { CreateApplicationComponent } from './create-application.component'; @@ -17,8 +16,7 @@ import { CreateApplicationComponent } from './create-application.component'; declarations: [ CreateApplicationComponent, CreateApplicationStep2Component, - CreateApplicationStep3Component, - AppNameUniqueDirective + CreateApplicationStep3Component ], exports: [ CreateApplicationComponent, diff --git a/src/frontend/packages/core/src/features/applications/deploy-application/deploy-application.component.html b/src/frontend/packages/core/src/features/applications/deploy-application/deploy-application.component.html index e00320d945..f54b45ae60 100644 --- a/src/frontend/packages/core/src/features/applications/deploy-application/deploy-application.component.html +++ b/src/frontend/packages/core/src/features/applications/deploy-application/deploy-application.component.html @@ -3,22 +3,30 @@

{{ getTitle() }}

- + - + - + - + - + - + \ No newline at end of file diff --git a/src/frontend/packages/core/src/features/applications/edit-application/edit-application.component.ts b/src/frontend/packages/core/src/features/applications/edit-application/edit-application.component.ts index 4cf3d41502..95863ad739 100644 --- a/src/frontend/packages/core/src/features/applications/edit-application/edit-application.component.ts +++ b/src/frontend/packages/core/src/features/applications/edit-application/edit-application.component.ts @@ -1,19 +1,17 @@ - -import { of as observableOf, Observable, Subscription } from 'rxjs'; - -import { map, filter, take } from 'rxjs/operators'; +import { HttpClient } from '@angular/common/http'; import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { Http } from '@angular/http'; import { ErrorStateMatcher, ShowOnDirtyErrorStateMatcher } from '@angular/material'; import { Store } from '@ngrx/store'; +import { Observable, of as observableOf, Subscription } from 'rxjs'; +import { filter, map, take } from 'rxjs/operators'; +import { AppMetadataTypes } from '../../../../../store/src/actions/app-metadata.actions'; +import { SetCFDetails, SetNewAppName } from '../../../../../store/src/actions/create-applications-page.actions'; +import { AppState } from '../../../../../store/src/app-state'; import { StepOnNextFunction } from '../../../shared/components/stepper/step/step.component'; -import { AppNameUniqueChecking, AppNameUniqueDirective } from '../app-name-unique.directive/app-name-unique.directive'; import { ApplicationService } from '../application.service'; -import { AppState } from '../../../../../store/src/app-state'; -import { SetCFDetails, SetNewAppName } from '../../../../../store/src/actions/create-applications-page.actions'; -import { AppMetadataTypes } from '../../../../../store/src/actions/app-metadata.actions'; +import { AppNameUniqueDirective, AppNameUniqueChecking } from '../../../shared/app-name-unique.directive/app-name-unique.directive'; @Component({ @@ -36,7 +34,7 @@ export class EditApplicationComponent implements OnInit, OnDestroy { public applicationService: ApplicationService, private store: Store, private fb: FormBuilder, - private http: Http, + private http: HttpClient, ) { this.uniqueNameValidator = new AppNameUniqueDirective(this.store, this.http); this.editAppForm = this.fb.group({ diff --git a/src/frontend/packages/core/src/features/cloud-foundry/cf.helpers.ts b/src/frontend/packages/core/src/features/cloud-foundry/cf.helpers.ts index 4087ef50ac..571cd6228d 100644 --- a/src/frontend/packages/core/src/features/cloud-foundry/cf.helpers.ts +++ b/src/frontend/packages/core/src/features/cloud-foundry/cf.helpers.ts @@ -26,12 +26,16 @@ import { UserRoleInSpace, } from '../../../../store/src/types/user.types'; import { UserRoleLabels } from '../../../../store/src/types/users-roles.types'; +import { IServiceInstance, IUserProvidedServiceInstance } from '../../core/cf-api-svc.types'; import { CurrentUserPermissions } from '../../core/current-user-permissions.config'; import { CurrentUserPermissionsService } from '../../core/current-user-permissions.service'; import { pathGet } from '../../core/utils.service'; +import { extractActualListEntity } from '../../shared/components/list/data-sources-controllers/local-filtering-sorting'; +import { MultiActionListEntity } from '../../shared/monitors/pagination-monitor'; import { PaginationMonitorFactory } from '../../shared/monitors/pagination-monitor.factory'; import { ActiveRouteCfCell, ActiveRouteCfOrgSpace } from './cf-page.types'; + export interface IUserRole { string: string; key: T; @@ -164,7 +168,12 @@ function hasRole(user: CfUser, guid: string, roleType: string) { return false; } -export const getRowMetadata = (entity: APIResource) => entity.metadata ? entity.metadata.guid : null; +export const getRowMetadata = (entity: APIResource | MultiActionListEntity) => { + if (entity instanceof MultiActionListEntity) { + return entity.entity.metadata ? entity.entity.metadata.guid : null; + } + return entity.metadata ? entity.metadata.guid : null; +}; export function getIdFromRoute(activatedRoute: ActivatedRoute, id: string) { if (activatedRoute.snapshot.params[id]) { @@ -317,6 +326,7 @@ export const cfOrgSpaceFilter = (entities: APIResource[], paginationState: Pagin const orgGuid = paginationState.clientPagination.filter.items.org; const spaceGuid = paginationState.clientPagination.filter.items.space; return !cfGuid && !orgGuid && !spaceGuid ? entities : entities.filter(e => { + e = extractActualListEntity(e); const validCF = !(cfGuid && cfGuid !== e.entity.cfGuid); const validOrg = !(orgGuid && orgGuid !== e.entity.space.entity.organization_guid); const validSpace = !(spaceGuid && spaceGuid !== e.entity.space_guid); @@ -340,3 +350,11 @@ export function createCfOrgSpaceSteppersUrl( route += stepperPath; return route; } + +export function isServiceInstance(obj: any): IServiceInstance { + return !!obj && !!obj.service_plan_url ? obj as IServiceInstance : null; +} + +export function isUserProvidedServiceInstance(obj: any): IUserProvidedServiceInstance { + return !!obj && (obj.route_service_url !== null && obj.route_service_url !== undefined) ? obj as IUserProvidedServiceInstance : null; +} diff --git a/src/frontend/packages/core/src/features/service-catalog/service-instances/service-instances.component.spec.ts b/src/frontend/packages/core/src/features/service-catalog/service-instances/service-instances.component.spec.ts index 158171b48e..b90fcb2b9d 100644 --- a/src/frontend/packages/core/src/features/service-catalog/service-instances/service-instances.component.spec.ts +++ b/src/frontend/packages/core/src/features/service-catalog/service-instances/service-instances.component.spec.ts @@ -10,7 +10,7 @@ describe('ServiceInstancesComponent', () => { let component: ServiceInstancesComponent; let fixture: ComponentFixture; - beforeEach(async(() => { + beforeEach(() => { TestBed.configureTestingModule({ declarations: [ServiceInstancesComponent], imports: [ @@ -22,7 +22,7 @@ describe('ServiceInstancesComponent', () => { ] }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ServiceInstancesComponent); diff --git a/src/frontend/packages/core/src/features/services/services.routing.ts b/src/frontend/packages/core/src/features/services/services.routing.ts index d50433bd6d..ae768cb4df 100644 --- a/src/frontend/packages/core/src/features/services/services.routing.ts +++ b/src/frontend/packages/core/src/features/services/services.routing.ts @@ -1,11 +1,14 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { + AddServiceInstanceBaseStepComponent, +} from '../../shared/components/add-service-instance/add-service-instance-base-step/add-service-instance-base-step.component'; import { AddServiceInstanceComponent, } from '../../shared/components/add-service-instance/add-service-instance/add-service-instance.component'; -import { ServicesWallComponent } from './services-wall/services-wall.component'; import { DetachServiceInstanceComponent } from './detach-service-instance/detach-service-instance.component'; +import { ServicesWallComponent } from './services-wall/services-wall.component'; const services: Routes = [ { @@ -14,14 +17,18 @@ const services: Routes = [ }, { path: 'new', + component: AddServiceInstanceBaseStepComponent + }, + { + path: 'new/:type', component: AddServiceInstanceComponent }, { - path: ':endpointId/:serviceInstanceId/edit', + path: ':type/:endpointId/:serviceInstanceId/edit', component: AddServiceInstanceComponent }, { - path: ':endpointId/:serviceInstanceId/detach', + path: ':type/:endpointId/:serviceInstanceId/detach', component: DetachServiceInstanceComponent } ]; diff --git a/src/frontend/packages/core/src/features/setup/upgrade-page/upgrade-page.component.spec.ts b/src/frontend/packages/core/src/features/setup/upgrade-page/upgrade-page.component.spec.ts index ca1f7ce1a6..25764503cb 100644 --- a/src/frontend/packages/core/src/features/setup/upgrade-page/upgrade-page.component.spec.ts +++ b/src/frontend/packages/core/src/features/setup/upgrade-page/upgrade-page.component.spec.ts @@ -1,8 +1,6 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { UpgradePageComponent } from './upgrade-page.component'; -import { SharedModule } from '../../../shared/shared.module'; -import { CoreModule } from '../../../core/core.module'; import { IntroScreenComponent } from '../../../shared/components/intro-screen/intro-screen.component'; import { StratosTitleComponent } from '../../../shared/components/stratos-title/stratos-title.component'; import { MDAppModule } from '../../../core/md.module'; diff --git a/src/frontend/packages/core/src/features/applications/app-name-unique.directive/app-name-unique.directive.spec.ts b/src/frontend/packages/core/src/shared/app-name-unique.directive/app-name-unique.directive.spec.ts similarity index 57% rename from src/frontend/packages/core/src/features/applications/app-name-unique.directive/app-name-unique.directive.spec.ts rename to src/frontend/packages/core/src/shared/app-name-unique.directive/app-name-unique.directive.spec.ts index fa80292c71..3ae4b79923 100644 --- a/src/frontend/packages/core/src/features/applications/app-name-unique.directive/app-name-unique.directive.spec.ts +++ b/src/frontend/packages/core/src/shared/app-name-unique.directive/app-name-unique.directive.spec.ts @@ -1,19 +1,20 @@ import { CommonModule } from '@angular/common'; -import { CoreModule } from '../../../core/core.module'; -import { SharedModule } from '../../../shared/shared.module'; +import { HttpClient } from '@angular/common/http'; import { inject, TestBed } from '@angular/core/testing'; -import { Store, StoreModule } from '@ngrx/store'; +import { ConnectionBackend, HttpModule } from '@angular/http'; +import { MockBackend } from '@angular/http/testing'; +import { MatDialogModule } from '@angular/material'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Store } from '@ngrx/store'; import { AppNameUniqueDirective } from './app-name-unique.directive'; -import { RouterTestingModule } from '@angular/router/testing'; -import { MatDialogModule } from '@angular/material'; -import { MockBackend } from '@angular/http/testing'; -import { HttpModule, Http, ConnectionBackend } from '@angular/http'; -import { GITHUB_API_URL, getGitHubAPIURL } from '../../../core/github.helpers'; -import { AppStoreModule } from '../../../../../store/src/store.module'; -import { AppState } from '../../../../../store/src/app-state'; -import { createBasicStoreModule } from '../../../../test-framework/store-test-helper'; -import { ExtensionService } from '../../../core/extension/extension-service'; +import { AppStoreModule } from '../../../../store/src/store.module'; +import { CoreModule } from '../../core/core.module'; +import { SharedModule } from '../shared.module'; +import { createBasicStoreModule } from '../../../test-framework/store-test-helper'; +import { ExtensionService } from '../../core/extension/extension-service'; +import { GITHUB_API_URL, getGitHubAPIURL } from '../../core/github.helpers'; +import { AppState } from '../../../../store/src/app-state'; describe('AppNameUniqueDirective', () => { @@ -35,12 +36,12 @@ describe('AppNameUniqueDirective', () => { provide: ConnectionBackend, useClass: MockBackend }, - Http, + HttpClient, { provide: GITHUB_API_URL, useFactory: getGitHubAPIURL } ] }); }); - it('should create an instance', inject([Store, Http], (store: Store, http: Http) => { + it('should create an instance', inject([Store, HttpClient], (store: Store, http: HttpClient) => { const directive = new AppNameUniqueDirective(store, http); expect(directive).toBeTruthy(); })); diff --git a/src/frontend/packages/core/src/shared/app-name-unique.directive/app-name-unique.directive.ts b/src/frontend/packages/core/src/shared/app-name-unique.directive/app-name-unique.directive.ts new file mode 100644 index 0000000000..3db46c0984 --- /dev/null +++ b/src/frontend/packages/core/src/shared/app-name-unique.directive/app-name-unique.directive.ts @@ -0,0 +1,137 @@ +import { HttpClient, HttpHeaders, HttpParams, HttpRequest, HttpResponse } from '@angular/common/http'; +import { Directive, forwardRef, Input, OnInit } from '@angular/core'; +import { AbstractControl, AsyncValidator, NG_ASYNC_VALIDATORS } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { Observable, of as observableOf, throwError as observableThrowError, timer as observableTimer } from 'rxjs'; +import { catchError, filter, map, switchMap, take } from 'rxjs/operators'; + +import { AppState } from '../../../../store/src/app-state'; +import { selectNewAppState } from '../../../../store/src/effects/create-app-effects'; +import { environment } from '../../environments/environment.prod'; + + +/* tslint:disable:no-use-before-declare */ +const APP_UNIQUE_NAME_PROVIDER = { + provide: NG_ASYNC_VALIDATORS, useExisting: forwardRef(() => AppNameUniqueDirective), multi: true +}; +/* tslint:enable */ + +// See: https://medium.com/@kahlil/asynchronous-validation-with-angular-reactive-forms-1a392971c062 + +const { proxyAPIVersion, cfAPIVersion } = environment; +export type NameTaken = (response: HttpResponse) => boolean; +export type UniqueValidatorRequestBuilder = (name: string) => HttpRequest; +export class AppNameUniqueChecking { + busy: boolean; + taken: boolean; + status: string; + + set(busy: boolean, taken?: boolean) { + this.busy = busy; + this.taken = taken; + + if (this.busy) { + this.status = 'busy'; + } else if (this.taken === undefined) { + this.status = ''; + } else { + this.status = this.taken ? 'error' : 'done'; + } + } +} + +@Directive({ + selector: '[appApplicationNameUnique][formControlName],[appApplicationNameUnique][formControl],[appApplicationNameUnique][ngModel]', + providers: [APP_UNIQUE_NAME_PROVIDER] +}) +export class AppNameUniqueDirective implements AsyncValidator, OnInit { + + @Input() appApplicationNameUnique: AppNameUniqueChecking; + @Input() appApplicationNameUniqueRequest: UniqueValidatorRequestBuilder; + @Input() appApplicationNameUniqueValidator: NameTaken = (res: HttpResponse) => res.body.total_results > 0; + + constructor( + private store: Store, + private http: HttpClient, + ) { + if (!this.appApplicationNameUnique) { + this.appApplicationNameUnique = new AppNameUniqueChecking(); + } + } + + ngOnInit() { + if (!this.appApplicationNameUnique) { + this.appApplicationNameUnique = new AppNameUniqueChecking(); + } + this.appApplicationNameUnique.set(false); + } + + public validate(control: AbstractControl): Observable<{ appNameTaken: boolean } | null> { + if (!control.dirty) { + return observableOf(null); + } + this.appApplicationNameUnique.set(true); + return observableTimer(500).pipe( + switchMap(() => this.getCheck(control.value)), + map(appNameTaken => { + this.appApplicationNameUnique.set(false, appNameTaken); + return appNameTaken ? { appNameTaken } : null; + }), + catchError(err => { + this.appApplicationNameUnique.set(false); + return observableThrowError(err); + })); + } + + private getCheck(name: string): Observable { + if (this.appApplicationNameUniqueRequest) { + return this.nameTaken( + this.appApplicationNameUniqueRequest(name), + this.appApplicationNameUniqueValidator + ); + } + return this.getDefaultRequestData(name); + } + + private getDefaultRequest(cfGuid: string, spaceGuid: string, name: string) { + const params = new HttpParams() + .set('q', 'name:' + name) + .append('q', 'space_guid:' + spaceGuid); + const headers = new HttpHeaders({ + 'x-cap-cnsi-list': cfGuid, + 'x-cap-passthrough': 'true' + }); + return new HttpRequest( + 'GET', + `/pp/${proxyAPIVersion}/proxy/${cfAPIVersion}/apps`, + { + headers, + params + }, + ); + } + + private getDefaultRequestData(name: string) { + return this.store.select(selectNewAppState).pipe( + take(1), + switchMap( + newAppState => { + const cfGuid = newAppState.cloudFoundryDetails.cloudFoundry; + const spaceGuid = newAppState.cloudFoundryDetails.space; + const request = this.getDefaultRequest(cfGuid, spaceGuid, name); + return this.nameTaken( + request, + this.appApplicationNameUniqueValidator + ); + } + ) + ); + } + + private nameTaken(requestData: HttpRequest, taken: NameTaken) { + return this.http.request(requestData).pipe( + filter((event) => event instanceof HttpResponse), + map(taken) + ); + } +} diff --git a/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance-base-step/add-service-instance-base-step.component.html b/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance-base-step/add-service-instance-base-step.component.html new file mode 100644 index 0000000000..bdb623280b --- /dev/null +++ b/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance-base-step/add-service-instance-base-step.component.html @@ -0,0 +1,17 @@ + + Choose Service Type + + + +
+

Choose service type to {{ bindApp ? 'bind' : 'create' }}

+

+ Want to know more? Follow the links for information about marketplace services and user provided + services. +

+ +
+
+
\ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance-base-step/add-service-instance-base-step.component.scss b/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance-base-step/add-service-instance-base-step.component.scss new file mode 100644 index 0000000000..bc1f60623e --- /dev/null +++ b/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance-base-step/add-service-instance-base-step.component.scss @@ -0,0 +1,6 @@ +.select-service-step { + width: 100%; + &__help { + padding-bottom: 10px; + } +} diff --git a/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance-base-step/add-service-instance-base-step.component.spec.ts b/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance-base-step/add-service-instance-base-step.component.spec.ts new file mode 100644 index 0000000000..156d8c4bfc --- /dev/null +++ b/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance-base-step/add-service-instance-base-step.component.spec.ts @@ -0,0 +1,26 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AddServiceInstanceBaseStepComponent } from './add-service-instance-base-step.component'; +import { BaseTestModules } from '../../../../../test-framework/cloud-foundry-endpoint-service.helper'; + +describe('AddServiceInstanceBaseStepComponent', () => { + let component: AddServiceInstanceBaseStepComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: BaseTestModules + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AddServiceInstanceBaseStepComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance-base-step/add-service-instance-base-step.component.ts b/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance-base-step/add-service-instance-base-step.component.ts new file mode 100644 index 0000000000..864f3dcd34 --- /dev/null +++ b/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance-base-step/add-service-instance-base-step.component.ts @@ -0,0 +1,59 @@ +import { query } from '@angular/animations'; +import { Component } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Store } from '@ngrx/store'; + +import { RouterNav } from '../../../../../../store/src/actions/router.actions'; +import { AppState } from '../../../../../../store/src/app-state'; +import { TileConfigManager } from '../../tile/tile-selector.helpers'; +import { ITileConfig, ITileData } from './../../tile/tile-selector.types'; +import { BASE_REDIRECT_QUERY, SERVICE_INSTANCE_TYPES } from './add-service-instance.types'; + +interface ICreateServiceTilesData extends ITileData { + type: string; +} + +@Component({ + selector: 'app-add-service-instance-base-step', + templateUrl: './add-service-instance-base-step.component.html', + styleUrls: ['./add-service-instance-base-step.component.scss'] +}) +export class AddServiceInstanceBaseStepComponent { + private tileManager = new TileConfigManager(); + public serviceType: string; + + public tileSelectorConfig = [ + this.tileManager.getNextTileConfig( + 'Marketplace Service', + { matIcon: 'store' }, + { type: SERVICE_INSTANCE_TYPES.SERVICE } + ), + this.tileManager.getNextTileConfig( + 'User Provided Service', + { matIcon: 'person' }, + { type: SERVICE_INSTANCE_TYPES.USER_SERVICE } + ) + ]; + + private pSelectedTile: ITileConfig; + public bindApp: boolean; + get selectedTile() { + return this.pSelectedTile; + } + set selectedTile(tile: ITileConfig) { + this.serviceType = tile ? tile.data.type : null; + this.pSelectedTile = tile; + if (tile) { + const baseUrl = this.bindApp ? this.router.routerState.snapshot.url : '/services/new'; + this.store.dispatch(new RouterNav({ + path: `${baseUrl}/${this.serviceType}`, + query: { + [BASE_REDIRECT_QUERY]: baseUrl + } + })); + } + } + constructor(private route: ActivatedRoute, private router: Router, public store: Store) { + this.bindApp = !!this.route.snapshot.data.bind; + } +} diff --git a/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance-base-step/add-service-instance.types.ts b/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance-base-step/add-service-instance.types.ts new file mode 100644 index 0000000000..fdda58d283 --- /dev/null +++ b/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance-base-step/add-service-instance.types.ts @@ -0,0 +1,6 @@ +export enum SERVICE_INSTANCE_TYPES { + SERVICE = 'service', + USER_SERVICE = 'user-service' +} + +export const BASE_REDIRECT_QUERY = 'base-previous-redirect'; diff --git a/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance/add-service-instance.component.html b/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance/add-service-instance.component.html index dbefdec04a..143abf99d9 100644 --- a/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance/add-service-instance.component.html +++ b/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance/add-service-instance.component.html @@ -1,22 +1,56 @@ {{ title$ | async }} -
- - - - - - - - - - - - - - - - +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -
+ +
\ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance/add-service-instance.component.spec.ts b/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance/add-service-instance.component.spec.ts index 8cc7a1a54b..e2909c8fa6 100644 --- a/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance/add-service-instance.component.spec.ts +++ b/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance/add-service-instance.component.spec.ts @@ -39,6 +39,9 @@ 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 { SpecifyUserProvidedDetailsComponent } from '../specify-user-provided-details/specify-user-provided-details.component'; +import { AppNameUniqueDirective } from '../../../app-name-unique.directive/app-name-unique.directive'; +import { StatefulIconComponent } from '../../stateful-icon/stateful-icon.component'; describe('AddServiceInstanceComponent', () => { let component: AddServiceInstanceComponent; @@ -47,6 +50,7 @@ describe('AddServiceInstanceComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ + AppNameUniqueDirective, AddServiceInstanceComponent, SelectPlanStepComponent, SpecifyDetailsStepComponent, @@ -71,7 +75,9 @@ describe('AddServiceInstanceComponent', () => { MultilineTitleComponent, ServicePlanPublicComponent, ServicePlanPriceComponent, - FocusDirective + StatefulIconComponent, + FocusDirective, + SpecifyUserProvidedDetailsComponent ], imports: [ PageHeaderModule, diff --git a/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance/add-service-instance.component.ts b/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance/add-service-instance.component.ts index 9c1005063c..9110f285ef 100644 --- a/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance/add-service-instance.component.ts +++ b/src/frontend/packages/core/src/shared/components/add-service-instance/add-service-instance/add-service-instance.component.ts @@ -2,47 +2,59 @@ import { TitleCasePipe } from '@angular/common'; import { AfterContentInit, Component, OnDestroy } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Store } from '@ngrx/store'; -import { Observable, of as observableOf } from 'rxjs'; -import { filter, first, map, switchMap, take, tap } from 'rxjs/operators'; +import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; +import { + delay, + distinctUntilChanged, + filter, + first, + map, + publishReplay, + refCount, + switchMap, + take, + tap, +} from 'rxjs/operators'; +import { GetApplication } from '../../../../../../store/src/actions/application.actions'; +import { + ResetCreateServiceInstanceOrgAndSpaceState, + ResetCreateServiceInstanceState, + SetCreateServiceInstance, + SetCreateServiceInstanceCFDetails, + SetCreateServiceInstanceServiceGuid, + SetCreateServiceInstanceServicePlan, + SetServiceInstanceGuid, +} from '../../../../../../store/src/actions/create-service-instance.actions'; +import { IRouterNavPayload } from '../../../../../../store/src/actions/router.actions'; +import { GetServiceInstance } from '../../../../../../store/src/actions/service-instances.actions'; +import { GetAllAppsInSpace, GetSpace } from '../../../../../../store/src/actions/space.actions'; +import { AppState } from '../../../../../../store/src/app-state'; +import { + applicationSchemaKey, + entityFactory, + serviceInstancesSchemaKey, + spaceSchemaKey, +} from '../../../../../../store/src/helpers/entity-factory'; +import { + createEntityRelationKey, + createEntityRelationPaginationKey, +} from '../../../../../../store/src/helpers/entity-relations/entity-relations.types'; +import { getPaginationObservables } from '../../../../../../store/src/reducers/pagination-reducer/pagination-reducer.helper'; +import { selectCreateServiceInstance } from '../../../../../../store/src/selectors/create-service-instance.selectors'; +import { APIResource } from '../../../../../../store/src/types/api.types'; import { IServiceInstance } from '../../../../core/cf-api-svc.types'; import { IApp, ISpace } from '../../../../core/cf-api.types'; import { EntityServiceFactory } from '../../../../core/entity-service-factory.service'; import { getIdFromRoute } from '../../../../features/cloud-foundry/cf.helpers'; import { servicesServiceFactoryProvider } from '../../../../features/service-catalog/service-catalog.helpers'; - import { CfOrgSpaceDataService } from '../../../data-services/cf-org-space-service.service'; import { PaginationMonitorFactory } from '../../../monitors/pagination-monitor.factory'; +import { BASE_REDIRECT_QUERY, SERVICE_INSTANCE_TYPES } from '../add-service-instance-base-step/add-service-instance.types'; import { CreateServiceInstanceHelperServiceFactory } from '../create-service-instance-helper-service-factory.service'; import { CreateServiceInstanceHelper } from '../create-service-instance-helper.service'; import { CsiGuidsService } from '../csi-guids.service'; import { CsiModeService } from '../csi-mode.service'; -import { AppState } from '../../../../../../store/src/app-state'; -import { selectCreateServiceInstance } from '../../../../../../store/src/selectors/create-service-instance.selectors'; -import { - createEntityRelationPaginationKey, - createEntityRelationKey -} from '../../../../../../store/src/helpers/entity-relations/entity-relations.types'; -import { - spaceSchemaKey, - entityFactory, - applicationSchemaKey, - serviceInstancesSchemaKey -} from '../../../../../../store/src/helpers/entity-factory'; -import { getPaginationObservables } from '../../../../../../store/src/reducers/pagination-reducer/pagination-reducer.helper'; -import { APIResource } from '../../../../../../store/src/types/api.types'; -import { GetAllAppsInSpace, GetSpace } from '../../../../../../store/src/actions/space.actions'; -import { - SetCreateServiceInstanceCFDetails, - ResetCreateServiceInstanceOrgAndSpaceState, - ResetCreateServiceInstanceState, - SetCreateServiceInstanceServiceGuid, - SetServiceInstanceGuid, - SetCreateServiceInstance, - SetCreateServiceInstanceServicePlan -} from '../../../../../../store/src/actions/create-service-instance.actions'; -import { GetApplication } from '../../../../../../store/src/actions/application.actions'; -import { GetServiceInstance } from '../../../../../../store/src/actions/service-instances.actions'; @Component({ selector: 'app-add-service-instance', @@ -59,6 +71,7 @@ import { GetServiceInstance } from '../../../../../../store/src/actions/service- }) export class AddServiceInstanceComponent implements OnDestroy, AfterContentInit { initialisedService$: Observable; + apps$: Observable[]>; skipApps$: Observable; marketPlaceMode: boolean; cSIHelperService: CreateServiceInstanceHelper; @@ -69,7 +82,16 @@ export class AddServiceInstanceComponent implements OnDestroy, AfterContentInit stepperText = 'Select a Cloud Foundry instance, organization and space for the service instance.'; bindAppStepperText = 'Bind App (Optional)'; appId: string; + serviceInstanceId: string; public inMarketplaceMode: boolean; + public serviceType: SERVICE_INSTANCE_TYPES; + public serviceTypes = SERVICE_INSTANCE_TYPES; + public basePreviousRedirect: IRouterNavPayload; + private cfDetails$ = this.store.select(selectCreateServiceInstance); + public cfGuid$: Observable; + public spaceGuid$ = this.cfDetails$.pipe( + map(details => details.spaceGuid) + ); constructor( private cSIHelperServiceFactory: CreateServiceInstanceHelperServiceFactory, @@ -79,10 +101,21 @@ export class AddServiceInstanceComponent implements OnDestroy, AfterContentInit private csiGuidsService: CsiGuidsService, private entityServiceFactory: EntityServiceFactory, public modeService: CsiModeService, - private paginationMonitorFactory: PaginationMonitorFactory + private paginationMonitorFactory: PaginationMonitorFactory, + route: ActivatedRoute ) { + const cfGuid = getIdFromRoute(this.activatedRoute, 'endpointId'); + this.cfGuid$ = cfGuid ? observableOf(cfGuid) : this.cfDetails$.pipe( + map(details => details.cfGuid) + ); this.inMarketplaceMode = this.modeService.isMarketplaceMode(); + this.serviceType = route.snapshot.params.type || SERVICE_INSTANCE_TYPES.SERVICE; + this.basePreviousRedirect = route.snapshot.queryParams[BASE_REDIRECT_QUERY] ? { + path: route.snapshot.queryParams[BASE_REDIRECT_QUERY] + } : null; } + + appsEmitted = new BehaviorSubject(null); ngAfterContentInit(): void { // Check if wizard has been initiated from the Services Marketplace if (this.inMarketplaceMode) { @@ -101,17 +134,32 @@ export class AddServiceInstanceComponent implements OnDestroy, AfterContentInit this.title$ = observableOf(`Create Service Instance`); } - this.skipApps$ = this.store.select(selectCreateServiceInstance).pipe( - filter(p => !!p && !!p.spaceGuid && !!p.cfGuid), - switchMap(createServiceInstance => { - const paginationKey = createEntityRelationPaginationKey(spaceSchemaKey, createServiceInstance.spaceGuid); + if (!this.initialisedService$) { + this.initialisedService$ = observableOf(true); + } + + this.apps$ = this.store.select(selectCreateServiceInstance).pipe( + filter(csi => !!csi && !!csi.spaceGuid && !!csi.cfGuid), + distinctUntilChanged((x, y) => x.cfGuid + x.spaceGuid === y.cfGuid + y.spaceGuid), + switchMap(csi => { + const paginationKey = createEntityRelationPaginationKey(spaceSchemaKey, csi.spaceGuid); return getPaginationObservables>({ store: this.store, - action: new GetAllAppsInSpace(createServiceInstance.cfGuid, createServiceInstance.spaceGuid, paginationKey), - paginationMonitor: this.paginationMonitorFactory.create(paginationKey, entityFactory(applicationSchemaKey)) + action: new GetAllAppsInSpace(csi.cfGuid, csi.spaceGuid, paginationKey), + paginationMonitor: this.paginationMonitorFactory.create( + paginationKey, + entityFactory(applicationSchemaKey) + ) }, true).entities$; }), - map(apps => apps.length === 0) + publishReplay(1), + refCount(), + ); + this.skipApps$ = this.apps$.pipe( + map(apps => apps.length === 0), + tap(() => this.appsEmitted.next(true)), + publishReplay(1), + refCount(), ); } @@ -121,7 +169,13 @@ export class AddServiceInstanceComponent implements OnDestroy, AfterContentInit this.cfOrgSpaceService.org.select.getValue(), this.cfOrgSpaceService.space.select.getValue() )); - return observableOf({ success: true }); + return this.appsEmitted.asObservable().pipe( + filter(emitted => emitted), + delay(1), + map(() => ({ success: true })), + tap(() => this.appsEmitted.next(false)) + + ); } resetStoreData = () => { @@ -133,7 +187,6 @@ export class AddServiceInstanceComponent implements OnDestroy, AfterContentInit } private setupForAppServiceMode() { - const appId = getIdFromRoute(this.activatedRoute, 'id'); const cfId = getIdFromRoute(this.activatedRoute, 'endpointId'); this.appId = appId; @@ -143,7 +196,8 @@ export class AddServiceInstanceComponent implements OnDestroy, AfterContentInit entityFactory(applicationSchemaKey), appId, new GetApplication(appId, cfId, [createEntityRelationKey(applicationSchemaKey, spaceSchemaKey)]), - true); + true + ); return entityService.waitForEntity$.pipe( filter(p => !!p), tap(app => { @@ -154,47 +208,52 @@ export class AddServiceInstanceComponent implements OnDestroy, AfterContentInit this.title$ = observableOf(`Create and/or Bind Service Instance to '${app.entity.entity.name}'`); }), take(1), - map(o => false) + map(o => true) ); } private configureForEditServiceInstanceMode() { const { endpointId, serviceInstanceId } = this.activatedRoute.snapshot.params; - const entityService = this.getServiceInstanceEntityService(serviceInstanceId, endpointId); - return entityService.waitForEntity$.pipe( - filter(p => !!p), - tap(serviceInstance => { - const serviceInstanceEntity = serviceInstance.entity.entity; - this.csiGuidsService.cfGuid = endpointId; - this.title$ = observableOf(`Edit Service Instance: ${serviceInstanceEntity.name}`); - const serviceGuid = serviceInstance.entity.entity.service_guid; - this.csiGuidsService.serviceGuid = serviceGuid; - this.cSIHelperService = this.cSIHelperServiceFactory.create(endpointId, serviceGuid); - this.store.dispatch(new SetCreateServiceInstanceServiceGuid(serviceGuid)); - this.store.dispatch(new SetServiceInstanceGuid(serviceInstance.entity.metadata.guid)); - this.store.dispatch(new SetCreateServiceInstance( - serviceInstanceEntity.name, - serviceInstanceEntity.space_guid, - serviceInstanceEntity.tags, - '' - )); - this.store.dispatch(new SetCreateServiceInstanceServicePlan(serviceInstanceEntity.service_plan_guid)); - const spaceEntityService = this.getSpaceEntityService(serviceInstanceEntity.space_guid, endpointId); - spaceEntityService.waitForEntity$.pipe( - filter(p => !!p), - tap(spaceEntity => { - this.store.dispatch(new SetCreateServiceInstanceCFDetails( - endpointId, - spaceEntity.entity.entity.organization_guid, - spaceEntity.entity.metadata.guid) - ); - }), - take(1) - ).subscribe(); - }), - take(1), - map(o => false), - ); + if (this.serviceType === this.serviceTypes.USER_SERVICE) { + this.serviceInstanceId = serviceInstanceId; + this.title$ = observableOf('Edit User Provided Service Instance'); + } else { + const entityService = this.getServiceInstanceEntityService(serviceInstanceId, endpointId); + return entityService.waitForEntity$.pipe( + filter(p => !!p), + tap(serviceInstance => { + const serviceInstanceEntity = serviceInstance.entity.entity; + this.csiGuidsService.cfGuid = endpointId; + this.title$ = observableOf(`Edit Service Instance: ${serviceInstanceEntity.name}`); + const serviceGuid = serviceInstance.entity.entity.service_guid; + this.csiGuidsService.serviceGuid = serviceGuid; + this.cSIHelperService = this.cSIHelperServiceFactory.create(endpointId, serviceGuid); + this.store.dispatch(new SetCreateServiceInstanceServiceGuid(serviceGuid)); + this.store.dispatch(new SetServiceInstanceGuid(serviceInstance.entity.metadata.guid)); + this.store.dispatch(new SetCreateServiceInstance( + serviceInstanceEntity.name, + serviceInstanceEntity.space_guid, + serviceInstanceEntity.tags, + '' + )); + this.store.dispatch(new SetCreateServiceInstanceServicePlan(serviceInstanceEntity.service_plan_guid)); + const spaceEntityService = this.getSpaceEntityService(serviceInstanceEntity.space_guid, endpointId); + spaceEntityService.waitForEntity$.pipe( + filter(p => !!p), + tap(spaceEntity => { + this.store.dispatch(new SetCreateServiceInstanceCFDetails( + endpointId, + spaceEntity.entity.entity.organization_guid, + spaceEntity.entity.metadata.guid) + ); + }), + take(1) + ).subscribe(); + }), + take(1), + map(o => true), + ); + } } private getServiceInstanceEntityService(serviceInstanceId: string, cfId: string) { @@ -240,7 +299,7 @@ export class AddServiceInstanceComponent implements OnDestroy, AfterContentInit filter(p => !!p), first(), tap(e => this.cfOrgSpaceService.cf.select.next(endpointId)), - map(o => false), + map(o => true), ); } } diff --git a/src/frontend/packages/core/src/shared/components/add-service-instance/bind-apps-step/bind-apps-step.component.ts b/src/frontend/packages/core/src/shared/components/add-service-instance/bind-apps-step/bind-apps-step.component.ts index 346ac91dc1..5962ca2b0c 100644 --- a/src/frontend/packages/core/src/shared/components/add-service-instance/bind-apps-step/bind-apps-step.component.ts +++ b/src/frontend/packages/core/src/shared/components/add-service-instance/bind-apps-step/bind-apps-step.component.ts @@ -2,17 +2,9 @@ import { AfterContentInit, Component, Input, OnDestroy } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs'; -import { filter, switchMap } from 'rxjs/operators'; import { SetCreateServiceInstanceApp } from '../../../../../../store/src/actions/create-service-instance.actions'; -import { GetAllAppsInSpace } from '../../../../../../store/src/actions/space.actions'; import { AppState } from '../../../../../../store/src/app-state'; -import { applicationSchemaKey, entityFactory, spaceSchemaKey } from '../../../../../../store/src/helpers/entity-factory'; -import { - createEntityRelationPaginationKey, -} from '../../../../../../store/src/helpers/entity-relations/entity-relations.types'; -import { getPaginationObservables } from '../../../../../../store/src/reducers/pagination-reducer/pagination-reducer.helper'; -import { selectCreateServiceInstance } from '../../../../../../store/src/selectors/create-service-instance.selectors'; import { APIResource } from '../../../../../../store/src/types/api.types'; import { IServicePlan } from '../../../../core/cf-api-svc.types'; import { IApp } from '../../../../core/cf-api.types'; @@ -31,11 +23,13 @@ export class BindAppsStepComponent implements OnDestroy, AfterContentInit { @Input() boundAppId: string; + @Input() + apps$: Observable[]>; + validateSubscription: Subscription; validate = new BehaviorSubject(true); serviceInstanceGuid: string; stepperForm: FormGroup; - apps$: Observable[]>; guideText = 'Specify the application to bind (Optional)'; selectedServicePlan: APIResource; bindingParams: object = {}; @@ -59,19 +53,6 @@ export class BindAppsStepComponent implements OnDestroy, AfterContentInit { } ngAfterContentInit() { - this.apps$ = this.store.select(selectCreateServiceInstance).pipe( - filter(p => !!p && !!p.spaceGuid && !!p.cfGuid), - switchMap(createServiceInstance => { - const paginationKey = createEntityRelationPaginationKey(spaceSchemaKey, createServiceInstance.spaceGuid); - return getPaginationObservables>({ - store: this.store, - action: new GetAllAppsInSpace(createServiceInstance.cfGuid, createServiceInstance.spaceGuid, paginationKey), - paginationMonitor: this.paginationMonitorFactory.create( - paginationKey, - entityFactory(applicationSchemaKey) - ) - }, true).entities$; - })); this.setBoundApp(); } diff --git a/src/frontend/packages/core/src/shared/components/add-service-instance/csi-mode.service.ts b/src/frontend/packages/core/src/shared/components/add-service-instance/csi-mode.service.ts index c863325be3..e94fb7926d 100644 --- a/src/frontend/packages/core/src/shared/components/add-service-instance/csi-mode.service.ts +++ b/src/frontend/packages/core/src/shared/components/add-service-instance/csi-mode.service.ts @@ -1,6 +1,12 @@ import { Injectable } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { filter, map } from 'rxjs/operators'; +import { CreateServiceBinding } from '../../../../../store/src/actions/service-bindings.actions'; +import { AppState } from '../../../../../store/src/app-state'; +import { serviceBindingSchemaKey } from '../../../../../store/src/helpers/entity-factory'; +import { selectRequestInfo } from '../../../../../store/src/selectors/api.selectors'; import { getIdFromRoute } from '../../../features/cloud-foundry/cf.helpers'; import { SpaceScopedService } from '../../../features/service-catalog/services.service'; @@ -11,6 +17,14 @@ export enum CreateServiceInstanceMode { EDIT_SERVICE_INSTANCE_MODE = 'editServiceInstanceMode' } +export const enum CreateServiceFormMode { + CreateServiceInstance = 'create-service-instance', + BindServiceInstance = 'bind-service-instance', +} + +export const CANCEL_SPACE_ID_PARAM = 'space-guid'; +export const CANCEL_ORG_ID_PARAM = 'org-guid'; + interface ViewDetail { showSelectCf: boolean; showSelectService: boolean; @@ -37,10 +51,14 @@ export class CsiModeService { spaceScopedDetails: SpaceScopedService = { isSpaceScoped: false }; constructor( - private activatedRoute: ActivatedRoute + private activatedRoute: ActivatedRoute, + private store: Store ) { const serviceId = getIdFromRoute(activatedRoute, 'serviceId'); const serviceInstanceId = getIdFromRoute(activatedRoute, 'serviceInstanceId'); + this.cancelUrl = `/services`; + const spaceGuid = activatedRoute.snapshot.queryParams[CANCEL_SPACE_ID_PARAM]; + const orgGuid = activatedRoute.snapshot.queryParams[CANCEL_ORG_ID_PARAM]; const cfId = getIdFromRoute(activatedRoute, 'endpointId'); const id = getIdFromRoute(activatedRoute, 'id'); @@ -66,12 +84,11 @@ export class CsiModeService { showSelectService: false, showBindApp: false }; - let returnUrl = `/services`; const appId = this.activatedRoute.snapshot.queryParams.appId; if (appId) { - returnUrl = `/applications/${cfId}/${appId}/services`; + this.cancelUrl = `/applications/${cfId}/${appId}/services`; } - this.cancelUrl = returnUrl; + } if (!!id && !!cfId) { @@ -81,15 +98,16 @@ export class CsiModeService { showSelectCf: false, }; this.cancelUrl = `/applications/${cfId}/${id}/services`; - } if (!cfId) { this.mode = CreateServiceInstanceMode.SERVICES_WALL_MODE; this.viewDetail = defaultViewDetail; - this.cancelUrl = `/services`; } + if (spaceGuid && orgGuid) { + this.cancelUrl = `/cloud-foundry/${cfId}/organizations/${orgGuid}/spaces/${spaceGuid}/service-instances`; + } } @@ -100,4 +118,30 @@ export class CsiModeService { isServicesWallMode = () => this.mode === CreateServiceInstanceMode.SERVICES_WALL_MODE; isEditServiceInstanceMode = () => this.mode === CreateServiceInstanceMode.EDIT_SERVICE_INSTANCE_MODE; + + public createApplicationServiceBinding(serviceInstanceGuid: string, cfGuid: string, appGuid: string, params: object) { + + const guid = `${cfGuid}-${appGuid}-${serviceInstanceGuid}`; + + this.store.dispatch(new CreateServiceBinding( + cfGuid, + guid, + appGuid, + serviceInstanceGuid, + params + )); + + return this.store.select(selectRequestInfo(serviceBindingSchemaKey, guid)).pipe( + filter(s => { + return s && !s.creating; + }), + map(req => { + if (req.error) { + return { success: false, message: `Failed to create service instance binding: ${req.message}` }; + } + return { success: true }; + }) + ); + } + } diff --git a/src/frontend/packages/core/src/shared/components/add-service-instance/specify-details-step/specify-details-step.component.html b/src/frontend/packages/core/src/shared/components/add-service-instance/specify-details-step/specify-details-step.component.html index d68770c653..3484a178ec 100644 --- a/src/frontend/packages/core/src/shared/components/add-service-instance/specify-details-step/specify-details-step.component.html +++ b/src/frontend/packages/core/src/shared/components/add-service-instance/specify-details-step/specify-details-step.component.html @@ -1,14 +1,17 @@
- - + + {{ mode.label }}
-
+ @@ -20,24 +23,30 @@ - + {{tag.label}} cancel - +
Service Parameters
- +
-
+ - {{ sI.entity.name }} + {{ sI.entity.name }}
-
+ \ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/add-service-instance/specify-details-step/specify-details-step.component.ts b/src/frontend/packages/core/src/shared/components/add-service-instance/specify-details-step/specify-details-step.component.ts index e7ad644758..ff8ff64d71 100644 --- a/src/frontend/packages/core/src/shared/components/add-service-instance/specify-details-step/specify-details-step.component.ts +++ b/src/frontend/packages/core/src/shared/components/add-service-instance/specify-details-step/specify-details-step.component.ts @@ -54,13 +54,9 @@ import { StepOnNextResult } from '../../stepper/step/step.component'; import { CreateServiceInstanceHelperServiceFactory } from '../create-service-instance-helper-service-factory.service'; import { CreateServiceInstanceHelper } from '../create-service-instance-helper.service'; import { CsiGuidsService } from '../csi-guids.service'; -import { CsiModeService } from '../csi-mode.service'; +import { CreateServiceFormMode, CsiModeService } from '../csi-mode.service'; -const enum FormMode { - CreateServiceInstance = 'create-service-instance', - BindServiceInstance = 'bind-service-instance', -} @Component({ selector: 'app-specify-details-step', templateUrl: './specify-details-step.component.html', @@ -76,11 +72,11 @@ export class SpecifyDetailsStepComponent implements OnDestroy, AfterContentInit formModes = [ { label: 'Create and Bind to a new Service Instance', - key: FormMode.CreateServiceInstance + key: CreateServiceFormMode.CreateServiceInstance }, { label: 'Bind to an Existing Service Instance', - key: FormMode.BindServiceInstance + key: CreateServiceFormMode.BindServiceInstance } ]; @Input() @@ -88,7 +84,7 @@ export class SpecifyDetailsStepComponent implements OnDestroy, AfterContentInit @Input() appId: string; - formMode: FormMode; + formMode: CreateServiceFormMode; selectExistingInstanceForm: FormGroup; createNewInstanceForm: FormGroup; @@ -200,7 +196,7 @@ export class SpecifyDetailsStepComponent implements OnDestroy, AfterContentInit }; } - this.formMode = FormMode.CreateServiceInstance; + this.formMode = CreateServiceFormMode.CreateServiceInstance; this.allServiceInstances$ = this.cSIHelperService.getServiceInstancesForService(null, null, this.csiGuidsService.cfGuid); if (this.modeService.isEditServiceInstanceMode()) { this.store.select(selectCreateServiceInstance).pipe( @@ -230,14 +226,14 @@ export class SpecifyDetailsStepComponent implements OnDestroy, AfterContentInit this.serviceParamsValid.next(valid); } - resetForms = (mode: FormMode) => { + resetForms = (mode: CreateServiceFormMode) => { this.validate.next(false); this.createNewInstanceForm.reset(); this.selectExistingInstanceForm.reset(); - if (mode === FormMode.CreateServiceInstance) { + if (mode === CreateServiceFormMode.CreateServiceInstance) { this.tags = []; this.bindExistingInstance = false; - } else if (mode === FormMode.BindServiceInstance) { + } else if (mode === CreateServiceFormMode.BindServiceInstance) { this.bindExistingInstance = true; } } @@ -259,7 +255,8 @@ export class SpecifyDetailsStepComponent implements OnDestroy, AfterContentInit )), tap(o => { this.allServiceInstanceNames = o.map(s => s.entity.name); })); - })).subscribe(); + }) + ).subscribe(); } private setupForms() { @@ -312,24 +309,25 @@ export class SpecifyDetailsStepComponent implements OnDestroy, AfterContentInit const serviceInstanceGuid = this.setServiceInstanceGuid(request); this.store.dispatch(new SetServiceInstanceGuid(serviceInstanceGuid)); if (!!state.bindAppGuid) { - return this.createBinding(serviceInstanceGuid, state.cfGuid, state.bindAppGuid, state.bindAppParams) - .pipe( - filter(s => { - return s && !s.creating; - }), - map(req => { - if (req.error) { - return { success: false, message: `Failed to create service instance binding: ${req.message}` }; - } else { - // Refetch env vars for app, since they have been changed by CF - this.store.dispatch( - new GetAppEnvVarsAction(state.bindAppGuid, state.cfGuid) - ); - - return this.routeToServices(state.cfGuid, state.bindAppGuid); - } - }) - ); + return this.modeService.createApplicationServiceBinding( + serviceInstanceGuid, + state.cfGuid, + state.bindAppGuid, + state.bindAppParams + ).pipe( + map(req => { + if (!req.success) { + return req; + } else { + // Refetch env vars for app, since they have been changed by CF + this.store.dispatch( + new GetAppEnvVarsAction(state.bindAppGuid, state.cfGuid) + ); + + return this.routeToServices(state.cfGuid, state.bindAppGuid); + } + }) + ); } else { return observableOf(this.routeToServices()); } diff --git a/src/frontend/packages/core/src/shared/components/add-service-instance/specify-user-provided-details/specify-user-provided-details.component.html b/src/frontend/packages/core/src/shared/components/add-service-instance/specify-user-provided-details/specify-user-provided-details.component.html new file mode 100644 index 0000000000..dad3260d98 --- /dev/null +++ b/src/frontend/packages/core/src/shared/components/add-service-instance/specify-user-provided-details/specify-user-provided-details.component.html @@ -0,0 +1,62 @@ +
+ + + {{ mode.label }} + + +
+
+ + + + A service instance with this name already exists. Please enter a different one. + + + A service instance name cannot exceed 50 characters. + +
+ +
+
+ + + Invalid URL + + + + Invalid URL + + + + + {{tag.label}} + cancel + + + + + + + + Not valid JSON. + + +
+ + +
+ + + {{ sI.entity.name }} + + +
+
\ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/add-service-instance/specify-user-provided-details/specify-user-provided-details.component.scss b/src/frontend/packages/core/src/shared/components/add-service-instance/specify-user-provided-details/specify-user-provided-details.component.scss new file mode 100644 index 0000000000..337bb09768 --- /dev/null +++ b/src/frontend/packages/core/src/shared/components/add-service-instance/specify-user-provided-details/specify-user-provided-details.component.scss @@ -0,0 +1,13 @@ +:host { + flex: 1; +} + +.specify-details { + &__radio-group { + display: flex; + flex-direction: column; + &__radio:first-of-type { + padding-bottom: 10px; + } + } +} diff --git a/src/frontend/packages/core/src/shared/components/add-service-instance/specify-user-provided-details/specify-user-provided-details.component.spec.ts b/src/frontend/packages/core/src/shared/components/add-service-instance/specify-user-provided-details/specify-user-provided-details.component.spec.ts new file mode 100644 index 0000000000..e74e5dc8f8 --- /dev/null +++ b/src/frontend/packages/core/src/shared/components/add-service-instance/specify-user-provided-details/specify-user-provided-details.component.spec.ts @@ -0,0 +1,36 @@ +import { HttpClientModule } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CsiModeService } from '../csi-mode.service'; +import { BaseTestModules } from './../../../../../test-framework/cloud-foundry-endpoint-service.helper'; +import { SpecifyUserProvidedDetailsComponent } from './specify-user-provided-details.component'; + +describe('SpecifyUserProvidedDetailsComponent', () => { + let component: SpecifyUserProvidedDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ...BaseTestModules, + HttpClientModule, + HttpClientTestingModule + ], + providers: [ + CsiModeService + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SpecifyUserProvidedDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/core/src/shared/components/add-service-instance/specify-user-provided-details/specify-user-provided-details.component.ts b/src/frontend/packages/core/src/shared/components/add-service-instance/specify-user-provided-details/specify-user-provided-details.component.ts new file mode 100644 index 0000000000..695fe77457 --- /dev/null +++ b/src/frontend/packages/core/src/shared/components/add-service-instance/specify-user-provided-details/specify-user-provided-details.component.ts @@ -0,0 +1,309 @@ +import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes'; +import { HttpHeaders, HttpParams, HttpRequest } from '@angular/common/http'; +import { Component, Input, OnDestroy } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatChipInputEvent } from '@angular/material'; +import { ActivatedRoute } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { BehaviorSubject, combineLatest as obsCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; +import { combineLatest, filter, first, map, publishReplay, refCount, startWith, switchMap } from 'rxjs/operators'; + +import { GetAppEnvVarsAction } from '../../../../../../store/src/actions/app-metadata.actions'; +import { + IUserProvidedServiceInstanceData, + UpdateUserProvidedServiceInstance, +} from '../../../../../../store/src/actions/user-provided-service.actions'; +import { + serviceBindingSchemaKey, + userProvidedServiceInstanceSchemaKey, +} from '../../../../../../store/src/helpers/entity-factory'; +import { createEntityRelationKey } from '../../../../../../store/src/helpers/entity-relations/entity-relations.types'; +import { selectCreateServiceInstance } from '../../../../../../store/src/selectors/create-service-instance.selectors'; +import { APIResource } from '../../../../../../store/src/types/api.types'; +import { IUserProvidedServiceInstance } from '../../../../core/cf-api-svc.types'; +import { safeUnsubscribe, urlValidationExpression } from '../../../../core/utils.service'; +import { environment } from '../../../../environments/environment'; +import { AppNameUniqueChecking } from '../../../app-name-unique.directive/app-name-unique.directive'; +import { isValidJsonValidator } from '../../../form-validators'; +import { CloudFoundryUserProvidedServicesService } from '../../../services/cloud-foundry-user-provided-services.service'; +import { StepOnNextResult } from '../../stepper/step/step.component'; +import { AppState } from './../../../../../../store/src/app-state'; +import { CreateServiceFormMode, CsiModeService } from './../csi-mode.service'; + + +const { proxyAPIVersion, cfAPIVersion } = environment; +@Component({ + selector: 'app-specify-user-provided-details', + templateUrl: './specify-user-provided-details.component.html', + styleUrls: ['./specify-user-provided-details.component.scss'] +}) +export class SpecifyUserProvidedDetailsComponent implements OnDestroy { + public createEditServiceInstance: FormGroup; + public bindExistingInstance: FormGroup; + public separatorKeysCodes = [ENTER, COMMA, SPACE]; + public allServiceInstanceNames: string[]; + public subs: Subscription[] = []; + public isUpdate: boolean; + public tags: { label: string }[] = []; + public valid = new BehaviorSubject(false); + private subscriptions: Subscription[] = []; + @Input() + public cfGuid: string; + @Input() + public spaceGuid: string; + @Input() + public appId: string; + @Input() + public serviceInstanceId: string; + + @Input() + public showModeSelection = false; + + public appNameChecking = new AppNameUniqueChecking(); + + public serviceBindingForApplication$ = this.serviceInstancesForApplication(); + formModes = [ + { + label: 'Create and Bind to a new User Provided Service Instance', + key: CreateServiceFormMode.CreateServiceInstance + }, + { + label: 'Bind to an Existing User Provided Service Instance', + key: CreateServiceFormMode.BindServiceInstance + } + ]; + formMode = CreateServiceFormMode.CreateServiceInstance; + + constructor( + route: ActivatedRoute, + private upsService: CloudFoundryUserProvidedServicesService, + public modeService: CsiModeService, + private store: Store, + ) { + const { endpointId, serviceInstanceId } = + route && route.snapshot ? route.snapshot.params : { endpointId: null, serviceInstanceId: null }; + this.isUpdate = endpointId && serviceInstanceId; + + this.createEditServiceInstance = new FormGroup({ + name: new FormControl('', [Validators.required, Validators.maxLength(50)]), + syslog_drain_url: new FormControl('', [Validators.pattern(urlValidationExpression)]), + credentials: new FormControl('', isValidJsonValidator()), + route_service_url: new FormControl('', [Validators.pattern(urlValidationExpression)]), + tags: new FormControl([]), + }); + this.bindExistingInstance = new FormGroup({ + serviceInstances: new FormControl('', [Validators.required]), + }); + this.initUpdate(serviceInstanceId, endpointId); + this.setupValidate(); + } + + ngOnDestroy(): void { + safeUnsubscribe(...this.subscriptions); + } + + private setupValidate() { + this.subscriptions.push( + obsCombineLatest([ + this.createEditServiceInstance.statusChanges.pipe(startWith('INVALID')), + this.bindExistingInstance.statusChanges.pipe(startWith('INVALID')), + ]).pipe( + map(([createValid, bindValid]) => this.formMode === CreateServiceFormMode.CreateServiceInstance ? + this.formStatusToBool(createValid) : + this.formStatusToBool(bindValid)) + ) + .subscribe(valid => this.valid.next(valid)) + ); + } + + private formStatusToBool(status: string): boolean { + return status === 'VALID'; + } + + resetForms = (mode: CreateServiceFormMode) => { + this.valid.next(false); + this.createEditServiceInstance.reset(); + this.bindExistingInstance.reset(); + if (mode === CreateServiceFormMode.CreateServiceInstance) { + this.tags = []; + } + } + + private serviceInstancesForApplication() { + return this.store.select(selectCreateServiceInstance).pipe( + filter(p => !!p && !!p.spaceGuid && !!p.cfGuid), + first(), + switchMap(p => this.upsService.getUserProvidedServices( + p.cfGuid, + p.spaceGuid, + [createEntityRelationKey(userProvidedServiceInstanceSchemaKey, serviceBindingSchemaKey)] + )), + map(upsis => upsis.map(upsi => { + const alreadyBound = !!upsi.entity.service_bindings.find(binding => binding.entity.app_guid === this.appId); + if (alreadyBound) { + const updatedSvc: APIResource = { + entity: { ...upsi.entity }, + metadata: { ...upsi.metadata } + }; + updatedSvc.entity.name += ' (Already bound)'; + updatedSvc.metadata.guid = null; + return updatedSvc; + } + return upsi; + })), + startWith(null), + publishReplay(1), + refCount() + ); + + } + private initUpdate(serviceInstanceId: string, endpointId: string) { + if (this.isUpdate) { + this.createEditServiceInstance.disable(); + this.upsService.getUserProvidedService(endpointId, serviceInstanceId).pipe( + first(), + map(entityInfo => entityInfo.entity) + ).subscribe(entity => { + this.createEditServiceInstance.enable(); + const serviceEntity = entity; + this.createEditServiceInstance.setValue({ + name: serviceEntity.name, + syslog_drain_url: serviceEntity.syslog_drain_url, + credentials: JSON.stringify(serviceEntity.credentials), + route_service_url: serviceEntity.route_service_url, + tags: [] + }); + this.tags = this.tagsArrayToChips(serviceEntity.tags); + }); + } + } + + public getUniqueRequest = (name: string) => { + const params = new HttpParams() + .set('q', 'name:' + name) + .append('q', 'space_guid:' + this.spaceGuid); + const headers = new HttpHeaders({ + 'x-cap-cnsi-list': this.cfGuid, + 'x-cap-passthrough': 'true' + }); + return new HttpRequest( + 'GET', + `/pp/${proxyAPIVersion}/proxy/${cfAPIVersion}/user_provided_service_instances`, + { + headers, + params + }, + ); + } + + public onNext = (): Observable => { + return this.isUpdate ? + this.onNextUpdate() : + this.formMode === CreateServiceFormMode.CreateServiceInstance ? this.onNextCreate() : this.onNextBind(); + } + + private onNextCreate(): Observable { + const data = this.getServiceData(); + const guid = `user-services-instance-${this.cfGuid}-${this.spaceGuid}-${data.name}`; + return this.upsService.createUserProvidedService( + this.cfGuid, + guid, + data as IUserProvidedServiceInstanceData, + ).pipe( + combineLatest(this.store.select(selectCreateServiceInstance)), + switchMap(([request, state]) => { + const newGuid = request.response.result[0]; + const success = !request.error; + const redirect = !request.error; + if (!!state.bindAppGuid && success) { + return this.createApplicationServiceBinding(newGuid, state); + } + return observableOf({ + success, + redirect, + message: success ? '' : 'Failed to create User Provided Service Instance. Reason: "' + request.message + '"' + }); + }) + ); + } + + private onNextBind(): Observable { + return this.store.select(selectCreateServiceInstance).pipe( + switchMap(data => this.createApplicationServiceBinding(this.bindExistingInstance.controls.serviceInstances.value, data)) + ); + } + + private createApplicationServiceBinding(serviceGuid: string, data: any): Observable { + return this.modeService.createApplicationServiceBinding(serviceGuid, data.cfGuid, data.bindAppGuid, data.bindAppParams) + .pipe( + map(req => { + if (!req.success) { + return { success: false, message: `Failed to create service instance binding: ${req.message}` }; + } else { + // Refetch env vars for app, since they have been changed by CF + this.store.dispatch( + new GetAppEnvVarsAction(data.bindAppGuid, data.cfGuid) + ); + return { success: true, redirect: true }; + } + }) + ); + } + + private onNextUpdate() { + const updateData = this.getServiceData(); + return this.upsService.updateUserProvidedService( + this.cfGuid, + this.serviceInstanceId, + updateData + ).pipe( + map(er => ({ + success: !er.updating[UpdateUserProvidedServiceInstance.updateServiceInstance].error, + redirect: !er.updating[UpdateUserProvidedServiceInstance.updateServiceInstance].error + })) + ); + } + + private getServiceData() { + const data = { + ...this.createEditServiceInstance.value, + spaceGuid: this.spaceGuid + }; + data.credentials = data.credentials ? JSON.parse(data.credentials) : {}; + + data.tags = this.getTagsArray(); + return data; + } + + + private getTagsArray() { + return this.tags && Array.isArray(this.tags) ? this.tags.map(tag => tag.label) : []; + } + + private tagsArrayToChips(tagsArray: string[]) { + return tagsArray && Array.isArray(tagsArray) ? tagsArray.map(label => ({ label })) : []; + } + + + public addTag(event: MatChipInputEvent): void { + const input = event.input; + + const label = (event.value || '').trim(); + if (label) { + this.tags.push({ label }); + } + + if (input) { + input.value = ''; + } + } + + public removeTag(tag: any): void { + const index = this.tags.indexOf(tag); + + if (index >= 0) { + this.tags.splice(index, 1); + } + } + +} diff --git a/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-data-source-config.ts b/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-data-source-config.ts index 1668aef4e4..0b47f12bd7 100644 --- a/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-data-source-config.ts +++ b/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-data-source-config.ts @@ -1,12 +1,51 @@ import { Store } from '@ngrx/store'; import { schema } from 'normalizr'; -import { OperatorFunction, Observable } from 'rxjs'; +import { Observable, OperatorFunction } from 'rxjs'; -import { DataFunction, DataFunctionDefinition } from './list-data-source'; -import { getRowUniqueId, RowsState, RowState } from './list-data-source-types'; -import { IListConfig } from '../list.component.types'; import { AppState } from '../../../../../../store/src/app-state'; +import { EntitySchema } from '../../../../../../store/src/helpers/entity-factory'; import { PaginatedAction } from '../../../../../../store/src/types/pagination.types'; +import { IListConfig } from '../list.component.types'; +import { DataFunction, DataFunctionDefinition } from './list-data-source'; +import { getRowUniqueId, RowsState, RowState } from './list-data-source-types'; + + +/** + * Allows a list to manage separate actions and/or separate entity types. + * Also used to configure the entity type dropdown. + * @export + */ +export class MultiActionConfig { + /** + * Creates an instance of MultiActionConfig. + * @param schemaConfigs configs to drive a multi action list + * @param [selectPlaceholder='Select entity type'] The message that will be show in the select. + * If this is null then the dropdown will be hidden + * @param [deselectText=null] What string should be shown for the "deselect" select item. + * A null value will show an empty item + */ + constructor( + public schemaConfigs: ActionSchemaConfig[], + public selectPlaceholder: string = 'Select entity type', + public deselectText: string = 'All' + ) { } +} + +/** + * Gives information for an action and entity type used in multi action list. * + * @export + */ +export class ActionSchemaConfig { + /** + * Creates an instance of ActionSchemaConfig. + * @param [prettyName] The value that will be shown in the entity dropdown. + */ + constructor( + public paginationAction: PaginatedAction, + public schemaKey: string, + public prettyName?: string + ) { } +} export interface IListDataSourceConfig { store: Store; @@ -18,7 +57,7 @@ export interface IListDataSourceConfig { /** * The entity which will be fetched via the action */ - schema: schema.Entity; + schema: EntitySchema | MultiActionConfig; /** * A function which will return a unique id for the given row/entity */ diff --git a/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-data-source-types.ts b/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-data-source-types.ts index acea04c74f..715588167d 100644 --- a/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-data-source-types.ts +++ b/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-data-source-types.ts @@ -7,6 +7,29 @@ import { MetricsAction } from '../../../../../../store/src/actions/metrics.actio import { IRequestEntityTypeState } from '../../../../../../store/src/app-state'; import { PaginatedAction, PaginationEntityState, PaginationParam } from '../../../../../../store/src/types/pagination.types'; +export interface IEntitySelectItem { + page: number; + label: string; + schemaKey: string; +} + +/** + * Drives the entity list entity select + */ +export class EntitySelectConfig { + /** + * Creates an instance of EntitySelectConfig. + * @param selectPlaceholder Placeholder text to show. + * @param selectEmptyText The text shown when no value is selected + * @param entitySelectItems Dictates which pagination page + * is storing which entity ids. Used in the pagination monitor. + */ + constructor( + public selectPlaceholder: string, + public selectEmptyText: string, + public entitySelectItems: IEntitySelectItem[] + ) { } +} export interface AppEvent { actee_name: string; actee_type: string; @@ -35,7 +58,8 @@ export class ListActionConfig { interface ICoreListDataSource extends DataSource { rowsState?: Observable; - getRowState?(row: T): Observable; + + getRowState?(row: T, schemaKey?: string): Observable; trackBy(index: number, item: T); } @@ -50,12 +74,15 @@ export interface IListDataSource extends ICoreListDataSource { entities: T[], paginationState: PaginationEntityState ) => T[])[]; - action: PaginatedAction; + action: PaginatedAction | PaginatedAction[]; entityKey: string; paginationKey: string; + page$: Observable; + isMultiAction$?: Observable; + addItem: T; isAdding$: BehaviorSubject; isSelecting$: BehaviorSubject; @@ -71,6 +98,7 @@ export interface IListDataSource extends ICoreListDataSource { selectedRows: Map; // Select items - remove once ng-content can exist in md-table selectedRows$: ReplaySubject>; // Select items - remove once ng-content can exist in md-table getRowUniqueId: getRowUniqueId; + entitySelectConfig?: EntitySelectConfig; // For multi action lists, this is used to configure the entity select. selectAllFilteredRows(); // Select items - remove once ng-content can exist in md-table selectedRowToggle(row: T, multiMode?: boolean); // Select items - remove once ng-content can exist in md-table selectClear(); diff --git a/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-data-source.ts b/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-data-source.ts index 15b06f9108..ff192a68dc 100644 --- a/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-data-source.ts +++ b/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-data-source.ts @@ -1,7 +1,6 @@ import { DataSource } from '@angular/cdk/table'; import { SortDirection } from '@angular/material'; import { Store } from '@ngrx/store'; -import { schema } from 'normalizr'; import { BehaviorSubject, combineLatest, @@ -12,12 +11,37 @@ import { Subscription, } from 'rxjs'; import { tag } from 'rxjs-spy/operators'; -import { distinctUntilChanged, filter, first, map, publishReplay, refCount, tap } from 'rxjs/operators'; +import { + distinctUntilChanged, + filter, + first, + map, + publishReplay, + refCount, + startWith, + switchMap, + tap, + withLatestFrom, +} from 'rxjs/operators'; +import { ListFilter, ListSort } from '../../../../../../store/src/actions/list.actions'; +import { MetricsAction } from '../../../../../../store/src/actions/metrics.actions'; +import { SetResultCount } from '../../../../../../store/src/actions/pagination.actions'; +import { AppState } from '../../../../../../store/src/app-state'; +import { entityFactory, EntitySchema } from '../../../../../../store/src/helpers/entity-factory'; +import { getPaginationObservables } from '../../../../../../store/src/reducers/pagination-reducer/pagination-reducer.helper'; +import { + PaginatedAction, + PaginationEntityState, + PaginationParam, + QParam, +} from '../../../../../../store/src/types/pagination.types'; import { PaginationMonitor } from '../../../monitors/pagination-monitor'; -import { IListDataSourceConfig } from './list-data-source-config'; +import { IListDataSourceConfig, MultiActionConfig } from './list-data-source-config'; import { + EntitySelectConfig, getRowUniqueId, + IEntitySelectItem, IListDataSource, ListPaginationMultiFilterChange, RowsState, @@ -25,13 +49,7 @@ import { } from './list-data-source-types'; import { getDataFunctionList } from './local-filtering-sorting'; import { LocalListController } from './local-list-controller'; -import { PaginationEntityState, PaginatedAction, PaginationParam, QParam } from '../../../../../../store/src/types/pagination.types'; -import { AppState } from '../../../../../../store/src/app-state'; -import { MetricsAction } from '../../../../../../store/src/actions/metrics.actions'; -import { getPaginationObservables } from '../../../../../../store/src/reducers/pagination-reducer/pagination-reducer.helper'; -import { SetResultCount } from '../../../../../../store/src/actions/pagination.actions'; -import { ListFilter, ListSort } from '../../../../../../store/src/actions/list.actions'; - +import { LocalPaginationHelpers } from './local-list.helpers'; export class DataFunctionDefinition { type: 'sort' | 'filter'; @@ -79,7 +97,6 @@ export abstract class ListDataSource extends DataSource implements public editRow: T; // Cached collections - public filteredRows: Array; public transformedEntities: Array; // Misc @@ -94,8 +111,9 @@ export abstract class ListDataSource extends DataSource implements private externalDestroy: () => void; protected store: Store; - public action: PaginatedAction; - protected sourceScheme: schema.Entity; + public action: PaginatedAction | PaginatedAction[]; + public masterAction: PaginatedAction; + protected sourceScheme: EntitySchema; public getRowUniqueId: getRowUniqueId; private getEmptyType: () => T; public paginationKey: string; @@ -107,9 +125,11 @@ export abstract class ListDataSource extends DataSource implements private transformedEntitiesSubscription: Subscription; private seedSyncSub: Subscription; private metricsAction: MetricsAction; + public entitySelectConfig: EntitySelectConfig; public refresh: () => void; + public isMultiAction$: Observable; public getRowState: (row: T) => Observable = () => observableOf({}); constructor( @@ -120,7 +140,8 @@ export abstract class ListDataSource extends DataSource implements const paginationMonitor = new PaginationMonitor( this.store, this.paginationKey, - this.sourceScheme + this.sourceScheme, + this.isLocal ); const { pagination$, entities$ } = getPaginationObservables({ store: this.store, @@ -129,7 +150,7 @@ export abstract class ListDataSource extends DataSource implements }, this.isLocal ); - + this.isMultiAction$ = paginationMonitor.isMultiAction$; const transformEntities = this.transformEntities || []; // Add any additional functions via an optional listConfig, such as sorting from the column definition const listColumns = this.config.listConfig ? this.config.listConfig.getColumns() : []; @@ -146,15 +167,7 @@ export abstract class ListDataSource extends DataSource implements const dataFunctions: DataFunction[] = getDataFunctionList(transformEntities); const transformedEntities$ = this.attachTransformEntity(entities$, this.transformEntity); - this.transformedEntitiesSubscription = transformedEntities$.pipe( - tap(items => this.transformedEntities = items) - ).subscribe(); - - const setResultCount = (paginationEntity: PaginationEntityState, entities: T[]) => { - // if (paginationEntity.currentlyMaxed) { - // // If we're currently maxed the entities are junk, don't try to update total counts - // return; - // } + const setResultCount = (paginationEntity: PaginationEntityState, entities: any[]) => { const newLength = entities.length; if ( paginationEntity.ids[paginationEntity.currentPage] && @@ -162,21 +175,35 @@ export abstract class ListDataSource extends DataSource implements this.store.dispatch(new SetResultCount(this.entityKey, this.paginationKey, newLength)); } }; - this.page$ = this.isLocal ? - new LocalListController(transformedEntities$, pagination$, setResultCount, dataFunctions).page$ - : transformedEntities$.pipe(publishReplay(1), refCount()); - this.pageSubscription = this.page$.pipe(tap(items => this.filteredRows = items)).subscribe(); - this.pagination$ = pagination$; + // NJ - We should avoid these kind on side-effect subscriptions + this.transformedEntitiesSubscription = transformedEntities$.pipe( + tap(items => this.transformedEntities = items) + ).subscribe(); + this.isLoadingPage$ = paginationMonitor.fetchingCurrentPage$; + const page$ = this.isLocal ? + new LocalListController(transformedEntities$, pagination$, setResultCount, dataFunctions).page$ + : transformedEntities$; + + this.page$ = page$.pipe( + withLatestFrom(this.isLoadingPage$.pipe(startWith(false))), + filter(([page, isLoading]) => !isLoading), + map(([page]) => page), + publishReplay(1), + refCount() + ); + + this.pagination$ = pagination$; + this.sort$ = this.createSortObservable(); this.filter$ = this.createFilterObservable(); - this.maxedResults$ = !!this.action.flattenPaginationMax ? + this.maxedResults$ = !!this.masterAction.flattenPaginationMax ? this.pagination$.pipe( - map(pagination => pagination.currentlyMaxed), + map(LocalPaginationHelpers.isPaginationMaxed), distinctUntilChanged(), ) : observableOf(false); } @@ -185,7 +212,8 @@ export abstract class ListDataSource extends DataSource implements this.store = config.store; this.action = config.action; this.refresh = this.getRefreshFunction(config); - this.sourceScheme = config.schema; + this.sourceScheme = config.schema instanceof MultiActionConfig ? + entityFactory(config.schema.schemaConfigs[0].schemaKey) : config.schema; this.getRowUniqueId = config.getRowUniqueId; this.getEmptyType = config.getEmptyType ? config.getEmptyType : () => ({} as T); this.paginationKey = config.paginationKey; @@ -197,25 +225,73 @@ export abstract class ListDataSource extends DataSource implements this.externalDestroy = config.destroy || (() => { }); this.addItem = this.getEmptyType(); this.entityKey = this.sourceScheme.key; + this.masterAction = this.action as PaginatedAction; + this.setupAction(config); if (!this.isLocal && this.config.listConfig) { // This is a non-local data source so the results-per-page should match the initial page size. This will avoid making two calls // (one for the page size in the action and another when the initial page size is set) - this.action.initialParams = this.action.initialParams || {}; - this.action.initialParams['results-per-page'] = this.config.listConfig.pageSizeOptions[0]; + this.masterAction.initialParams = this.masterAction.initialParams || {}; + this.masterAction.initialParams['results-per-page'] = this.config.listConfig.pageSizeOptions[0]; + } + } + private setupAction(config: IListDataSourceConfig) { + if (config.schema instanceof MultiActionConfig) { + if (!config.isLocal) { + // We cannot do multi action lists for none local lists + this.action = config.schema[0].paginationAction; + this.masterAction = this.action as PaginatedAction; + } else { + this.action = config.schema.schemaConfigs.map((multiActionConfig, i) => ({ + ...multiActionConfig.paginationAction, + paginationKey: this.masterAction.paginationKey, + entityKey: this.masterAction.entityKey, + entity: this.masterAction.entity, + flattenPaginationMax: this.masterAction.flattenPaginationMax, + flattenPagination: this.masterAction.flattenPagination, + __forcedPageNumber__: i + 1, + __forcedPageSchemaKey__: multiActionConfig.schemaKey + }) as PaginatedAction); + } + this.entitySelectConfig = this.getEntitySelectConfig(config.schema); } } + private getEntitySelectConfig(multiActionConfig: MultiActionConfig) { + if (!multiActionConfig.selectPlaceholder) { + return null; + } + const pageToIdMap = multiActionConfig.schemaConfigs.reduce((actionMap, schemaConfig, i) => ([ + ...actionMap, + { + page: i + 1, + label: schemaConfig.prettyName, + schemaKey: schemaConfig.schemaKey + } + ]), [] as IEntitySelectItem[]); + if (Object.keys(pageToIdMap).length < 2) { + return null; + } + return new EntitySelectConfig( + multiActionConfig.selectPlaceholder, + multiActionConfig.deselectText, + pageToIdMap + ); + } + private getRefreshFunction(config: IListDataSourceConfig) { if (config.listConfig && config.listConfig.hideRefresh) { return null; } return config.refresh ? config.refresh : () => { - this.store.dispatch(this.metricsAction || this.action); + if (Array.isArray(this.action)) { + this.action.forEach(action => this.store.dispatch(action)); + } else { + this.store.dispatch(this.metricsAction || this.masterAction); + } }; } disconnect() { - this.pageSubscription.unsubscribe(); this.transformedEntitiesSubscription.unsubscribe(); if (this.seedSyncSub) { this.seedSyncSub.unsubscribe(); } this.externalDestroy(); @@ -238,8 +314,9 @@ export abstract class ListDataSource extends DataSource implements selectedRowToggle(row: T, multiMode: boolean = true) { this.getRowState(row).pipe( - first() - ).subscribe(rowState => { + first(), + withLatestFrom(this.page$) + ).subscribe(([rowState, filteredRows]) => { if (rowState.disabled) { return; } @@ -252,7 +329,7 @@ export abstract class ListDataSource extends DataSource implements this.selectedRows.clear(); } this.selectedRows.set(this.getRowUniqueId(row), row); - this.selectAllChecked = multiMode && this.selectedRows.size === this.filteredRows.length; + this.selectAllChecked = multiMode && this.selectedRows.size === filteredRows.length; } this.selectedRows$.next(this.selectedRows); this.isSelecting$.next(multiMode && this.selectedRows.size > 0); @@ -262,24 +339,26 @@ export abstract class ListDataSource extends DataSource implements selectAllFilteredRows() { this.selectAllChecked = !this.selectAllChecked; - const updatedAllRows = this.filteredRows.reduce((obs, row) => { - obs.push(this.getRowState(row).pipe( - first(), - tap(rowState => { - if (rowState.disabled) { - return; - } - if (this.selectAllChecked) { - this.selectedRows.set(this.getRowUniqueId(row), row); - } else { - this.selectedRows.delete(this.getRowUniqueId(row)); - } - }) - )); - return obs; - }, []); - - combineLatest(...updatedAllRows).pipe( + const updatedAllRows$ = this.page$.pipe(switchMap((filterEntities) => { + return combineLatest(filterEntities.reduce((obs, row) => { + obs.push(this.getRowState(row).pipe( + first(), + tap(rowState => { + if (rowState.disabled) { + return; + } + if (this.selectAllChecked) { + this.selectedRows.set(this.getRowUniqueId(row), row); + } else { + this.selectedRows.delete(this.getRowUniqueId(row)); + } + }) + )); + return obs; + }, [])); + })); + + updatedAllRows$.pipe( first() ).subscribe(() => { this.selectedRows$.next(this.selectedRows); @@ -308,7 +387,7 @@ export abstract class ListDataSource extends DataSource implements trackBy = (index: number, item: T) => this.getRowUniqueId(item) || item; - attachTransformEntity(entities$, entityLettable): Observable { + attachTransformEntity(entities$, entityLettable): Observable { if (entityLettable) { return entities$.pipe( this.transformEntity diff --git a/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-pagination-controller.ts b/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-pagination-controller.ts index 4d4ccb99eb..a9f3ada7ed 100644 --- a/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-pagination-controller.ts +++ b/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-pagination-controller.ts @@ -15,7 +15,7 @@ import { import { AppState } from '../../../../../../store/src/app-state'; import { defaultClientPaginationPageSize, -} from '../../../../../../store/src/reducers/pagination-reducer/pagination-reducer.helper'; +} from '../../../../../../store/src/reducers/pagination-reducer/pagination-reducer-reset-pagination'; import { PaginationClientFilter, PaginationEntityState } from '../../../../../../store/src/types/pagination.types'; import { enterZone, leaveZone } from '../../../../leaveEnterAngularZone'; import { IListMultiFilterConfig } from '../list.component.types'; diff --git a/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/local-filtering-sorting.ts b/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/local-filtering-sorting.ts index 15cfd2d8f9..993ebb65c1 100644 --- a/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/local-filtering-sorting.ts +++ b/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/local-filtering-sorting.ts @@ -1,4 +1,5 @@ import { DataFunction, DataFunctionDefinition } from './list-data-source'; +import { MultiActionListEntity } from '../../../monitors/pagination-monitor'; export function getDataFunctionList(entityFunctions: (DataFunction | DataFunctionDefinition)[]): DataFunction[] { return entityFunctions.map(functionOrDef => { @@ -23,7 +24,11 @@ function getFilterFunction(def: DataFunctionDefinition): DataFunction { const fieldArray = getFieldArray(def); return (entities, paginationState) => { const upperCaseFilter = paginationState.clientPagination.filter.string.toUpperCase(); + if (upperCaseFilter && upperCaseFilter.length === 0) { + return entities; + } return entities.filter(e => { + e = extractActualListEntity(e); const value = getValue(e, fieldArray, 0, true); if (!value) { return false; @@ -44,6 +49,8 @@ function getSortFunction(def: DataFunctionDefinition): DataFunction { } return entities.sort((a, b) => { + a = extractActualListEntity(a); + b = extractActualListEntity(b); const valueA = checkAndUpperCase(getValue(a, fieldArray)); const valueB = checkAndUpperCase(getValue(b, fieldArray)); if (valueA > valueB) { @@ -68,6 +75,8 @@ export function getIntegerFieldSortFunction(field: string): DataFunction { return (entities, paginationState) => { const orderDirection = paginationState.params['order-direction'] || 'asc'; return entities.sort((a, b) => { + a = extractActualListEntity(a); + b = extractActualListEntity(b); const valueA = parseInt(getValue(a, fieldArray), 10); const valueB = parseInt(getValue(b, fieldArray), 10); if (valueA > valueB) { @@ -87,6 +96,7 @@ function checkAndUpperCase(value: any) { } return value; } + function getValue(obj, fieldArray: string[], index = 0, castToString = false): string { const field = fieldArray[index]; if (!field) { @@ -100,3 +110,10 @@ function getValue(obj, fieldArray: string[], index = 0, castToString = false): s } return getValue(obj[field], fieldArray, ++index); } + +export function extractActualListEntity(entity: any | MultiActionListEntity) { + if (entity instanceof MultiActionListEntity) { + return entity.entity; + } + return entity; +} diff --git a/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/local-list-controller.ts b/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/local-list-controller.ts index f68b0dee59..21cb9b6085 100644 --- a/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/local-list-controller.ts +++ b/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/local-list-controller.ts @@ -1,10 +1,11 @@ -import { Observable, combineLatest } from 'rxjs'; -import { distinctUntilChanged, filter, map, publishReplay, refCount, tap } from 'rxjs/operators'; - -import { splitCurrentPage } from './local-list-controller.helpers'; +import { combineLatest, Observable, of as observableOf } from 'rxjs'; import { tag } from 'rxjs-spy/operators/tag'; +import { distinctUntilChanged, map, publishReplay, refCount, switchMap, tap } from 'rxjs/operators'; + import { PaginationEntityState } from '../../../../../../store/src/types/pagination.types'; import { DataFunction } from './list-data-source'; +import { splitCurrentPage } from './local-list-controller.helpers'; +import { LocalPaginationHelpers } from './local-list.helpers'; export class LocalListController { public page$: Observable; @@ -44,15 +45,15 @@ export class LocalListController { cleanPage$: Observable, cleanPagination$: Observable, dataFunctions?: DataFunction[]) { - return combineLatest( + const fullPageObs$ = combineLatest( cleanPagination$, cleanPage$ ).pipe( - // If currentlyMaxed is set the entities list contains junk, so don't continue - filter(([paginationEntity, entities]) => !paginationEntity.currentlyMaxed), map(([paginationEntity, entities]) => { this.pageSplitCache = null; - if (!entities || !entities.length) { + // `entities` can become out of sync with `paginationEntity.ids`. If either are empty just return empty, + // otherwise this leads to churn and result count flip flopping + if (!entities || !entities.length || Object.keys(paginationEntity.ids).length === 0) { return { paginationEntity, entities: [] }; } if (dataFunctions && dataFunctions.length) { @@ -65,6 +66,14 @@ export class LocalListController { tap(({ paginationEntity, entities }) => this.setResultCount(paginationEntity, entities)), map(({ entities }) => entities) ); + return cleanPagination$.pipe( + map(pagination => LocalPaginationHelpers.isPaginationMaxed(pagination)), + distinctUntilChanged(), + switchMap(maxed => { + return maxed ? observableOf([]) : fullPageObs$; + }) + ); + } /* @@ -89,7 +98,7 @@ export class LocalListController { /* * Emit a page, which has been created by splitting up a local list, when either - * 1) the core pages 'entities' (covers entire list of all entities and their order) + * 1) the core pages 'entities' (covers entire list of all entities and their order) changes * 2) the client side page number changes * 3) the client size page size changes */ @@ -103,7 +112,7 @@ export class LocalListController { currentPageSizeObservable$.pipe(tap(() => { this.pageSplitCache = null; })), - currentPageNumber$.pipe(), + currentPageNumber$, ).pipe( map(([entities, pageSize, currentPage]) => { const pages = this.pageSplitCache ? this.pageSplitCache : entities; @@ -127,6 +136,7 @@ export class LocalListController { + (paginationEntity.params['order-direction-field'] || '') + ',' + (paginationEntity.params['order-direction'] || '') + ',' + paginationEntity.clientPagination.filter.string + ',' + + paginationEntity.forcedLocalPage + Object.values(paginationEntity.clientPagination.filter.items); // Some outlier cases actually fetch independently from this list (looking at you app variables) } diff --git a/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/local-list.helpers.ts b/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/local-list.helpers.ts new file mode 100644 index 0000000000..33746d6a94 --- /dev/null +++ b/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/local-list.helpers.ts @@ -0,0 +1,29 @@ +import { PaginationEntityState } from '../../../../../../store/src/types/pagination.types'; + +export class LocalPaginationHelpers { + + /** + * Looks in all the places necessary to see if the current pagination section is maxed. + */ + static isPaginationMaxed(pagination: PaginationEntityState) { + if (pagination.forcedLocalPage) { + return !!pagination.pageRequests[pagination.forcedLocalPage].maxed; + } + return !!Object.values(pagination.pageRequests).find(request => request.maxed); + } + + /** + * Gets a local page request section relating to a particular schema key. + */ + static getEntityPageRequest(pagination: PaginationEntityState, entityKey: string) { + const { pageRequests } = pagination; + const pageNumber = Object.keys(pagination.pageRequests).find(key => pageRequests[key].entityKey === entityKey) || null; + if (pageNumber) { + return { + pageNumber, + pageRequest: pageRequests[pageNumber] + }; + } + return null; + } +} diff --git a/src/frontend/packages/core/src/shared/components/list/list-cards/card.component.types.ts b/src/frontend/packages/core/src/shared/components/list/list-cards/card.component.types.ts new file mode 100644 index 0000000000..d9ca7b1960 --- /dev/null +++ b/src/frontend/packages/core/src/shared/components/list/list-cards/card.component.types.ts @@ -0,0 +1,21 @@ +import { Type } from '@angular/core'; + +import { CardCell } from '../list.types'; + + +export interface ICardMultiActionComponentList { + [schemaKey: string]: Type>; +} + +export class CardMultiActionComponents { + constructor(private cardList: ICardMultiActionComponentList, public columns = CardCell.columns) { } + public getComponent(schemaKey: string) { + return this.cardList[schemaKey]; + } +} + +export type CardDynamicComponentFn = (row: T) => Type>; + +export class CardDynamicComponent { + constructor(public getComponent: CardDynamicComponentFn) { } +} diff --git a/src/frontend/packages/core/src/shared/components/list/list-cards/card/card.component.html b/src/frontend/packages/core/src/shared/components/list/list-cards/card/card.component.html index df3f8a5c0b..6e81826a58 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-cards/card/card.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-cards/card/card.component.html @@ -1 +1 @@ -
+
\ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/list/list-cards/card/card.component.ts b/src/frontend/packages/core/src/shared/components/list/list-cards/card/card.component.ts index de0f98d1ab..64dd9a92e0 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-cards/card/card.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-cards/card/card.component.ts @@ -1,16 +1,6 @@ -import { - Component, - ComponentFactoryResolver, - Input, - OnChanges, - OnInit, - SimpleChange, - SimpleChanges, - Type, - ViewChild, - ViewContainerRef, -} from '@angular/core'; +import { Component, ComponentFactoryResolver, ComponentRef, Input, Type, ViewChild, ViewContainerRef } from '@angular/core'; +import { MultiActionListEntity } from '../../../../monitors/pagination-monitor'; import { IListDataSource } from '../../data-sources-controllers/list-data-source-types'; import { AppServiceBindingCardComponent, @@ -28,7 +18,11 @@ import { EndpointCardComponent } from '../../list-types/endpoint/endpoint-card/e import { ServiceInstanceCardComponent, } from '../../list-types/services-wall/service-instance-card/service-instance-card.component'; +import { + UserProvidedServiceInstanceCardComponent, +} from '../../list-types/services-wall/user-provided-service-instance-card/user-provided-service-instance-card.component'; import { CardCell } from '../../list.types'; +import { CardDynamicComponent, CardMultiActionComponents } from '../card.component.types'; export const listCards = [ CardAppComponent, @@ -40,8 +34,16 @@ export const listCards = [ CfServiceCardComponent, AppServiceBindingCardComponent, ServiceInstanceCardComponent, + UserProvidedServiceInstanceCardComponent, EndpointCardComponent ]; +export type CardTypes = Type> | CardMultiActionComponents | CardDynamicComponent; + +interface ISetupData { + dataSource: IListDataSource; + componentType: CardTypes; + item: T | MultiActionListEntity; +} @Component({ selector: 'app-card', templateUrl: './card.component.html', @@ -50,39 +52,108 @@ export const listCards = [ ...listCards ] }) -export class CardComponent implements OnInit, OnChanges { +export class CardComponent { + private componentRef: ComponentRef; + private pComponent: CardTypes; + private pDataSource: IListDataSource; + + @Input() set dataSource(dataSource: IListDataSource) { + if (!this.pDataSource) { + this.componentCreator({ dataSource }); + this.pDataSource = dataSource; + } + } - @Input() component: Type<{}>; - @Input() item: T; - @Input() dataSource = null as IListDataSource; + @Input() set component(componentType: CardTypes) { + if (!this.pComponent) { + this.componentCreator({ componentType }); + this.pComponent = componentType; + } + } - @ViewChild('target', { read: ViewContainerRef }) target; + @Input() set item(item: T | MultiActionListEntity) { + this.componentCreator({ item }); + } + + @ViewChild('target', { read: ViewContainerRef }) target: ViewContainerRef; cardComponent: CardCell; constructor(private componentFactoryResolver: ComponentFactoryResolver) { } - ngOnInit() { - if (!this.component) { + private componentCreator = (() => { + let completeSetupData: Partial> = {}; + return (setupData: Partial>, ) => { + completeSetupData = { + ...completeSetupData, + ...setupData + }; + if (completeSetupData.componentType && completeSetupData.dataSource && completeSetupData.item) { + this.setupComponent(completeSetupData.componentType, completeSetupData.item, completeSetupData.dataSource); + } + }; + })(); + + private setupComponent(componentType: CardTypes, item: T | MultiActionListEntity, dataSource: IListDataSource) { + if (!componentType || !item) { return; } - const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.component); - // Add to target to ensure ngcontent is correct in new component - const componentRef = this.target.createComponent(componentFactory); - this.cardComponent = componentRef.instance as CardCell; - this.cardComponent.row = this.item; - this.cardComponent.dataSource = this.dataSource; + const { component, entityKey, entity } = this.getComponent(componentType, item); + if (component) { + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(component); + if (componentFactory) { + this.clear(); + this.componentRef = this.target.createComponent(componentFactory); + this.cardComponent = this.componentRef.instance as CardCell; + this.cardComponent.row = entity; + this.cardComponent.dataSource = dataSource; + this.cardComponent.entityKey = entityKey; + } + } } - ngOnChanges(changes: SimpleChanges) { - const row: SimpleChange = changes.item; - if ( - row && - this.cardComponent && - row.previousValue !== row.currentValue - ) { - this.cardComponent.row = row.currentValue; + private clear() { + if (this.target) { + this.target.clear(); + } + if (this.componentRef) { + this.componentRef.destroy(); } } + private getComponent(component: CardTypes, item: T | MultiActionListEntity): { + component: Type>, + entity: T, + entityKey?: string; + } { + const { entityKey, entity } = this.getEntity(item); + if (component instanceof CardMultiActionComponents) { + return { + component: entityKey ? component.getComponent(entityKey) : null, + entityKey, + entity + }; + } else if (component instanceof CardDynamicComponent) { + return { + component: component.getComponent(entity), + entity + }; + } + return { + component: (component as Type>), + entity + }; + } + + private getEntity(item: T | MultiActionListEntity) { + if (item instanceof MultiActionListEntity) { + return { + entityKey: item.entityKey, + entity: item.entity + }; + } + return { + entity: item + }; + } } diff --git a/src/frontend/packages/core/src/shared/components/list/list-cards/cards.component.html b/src/frontend/packages/core/src/shared/components/list/list-cards/cards.component.html index 8052bb1761..a288c4b4d3 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-cards/cards.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-cards/cards.component.html @@ -1,4 +1,7 @@ -
- +
+ + + + -
+
\ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/list/list-cards/cards.component.spec.ts b/src/frontend/packages/core/src/shared/components/list/list-cards/cards.component.spec.ts index 2e333b8c4c..aecdde19a9 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-cards/cards.component.spec.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-cards/cards.component.spec.ts @@ -1,11 +1,12 @@ +import { Type } from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { EntityInfo } from '../../../../../../store/src/types/api.types'; import { CoreModule } from '../../../../core/core.module'; import { SharedModule } from '../../../shared.module'; import { IListDataSource } from '../data-sources-controllers/list-data-source-types'; -import { CardsComponent } from './cards.component'; import { CardCell } from '../list.types'; -import { EntityInfo } from '../../../../../../store/src/types/api.types'; +import { CardsComponent } from './cards.component'; describe('CardsComponent', () => { let component: CardsComponent; @@ -24,7 +25,7 @@ describe('CardsComponent', () => { beforeEach(() => { fixture = TestBed.createComponent>(CardsComponent); component = fixture.componentInstance; - component.component = {} as CardCell; + component.component = {} as Type>; component.dataSource = {} as IListDataSource; fixture.detectChanges(); }); diff --git a/src/frontend/packages/core/src/shared/components/list/list-cards/cards.component.ts b/src/frontend/packages/core/src/shared/components/list/list-cards/cards.component.ts index 1ea537a0f2..ec7c9954a7 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-cards/cards.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-cards/cards.component.ts @@ -1,7 +1,9 @@ import { Component, Input } from '@angular/core'; +import { MultiActionListEntity } from '../../../monitors/pagination-monitor'; import { IListDataSource } from '../data-sources-controllers/list-data-source-types'; import { CardCell } from '../list.types'; +import { CardTypes } from './card/card.component'; @Component({ selector: 'app-cards', @@ -11,12 +13,24 @@ import { CardCell } from '../list.types'; export class CardsComponent { public columns = CardCell.columns; @Input() dataSource: IListDataSource; - private pComponent: CardCell; + private pComponent: CardTypes; @Input() get component() { return this.pComponent; } - set component(cardCell) { + set component(cardCell: CardTypes) { this.pComponent = cardCell; - /* tslint:disable-next-line:no-string-literal */ - this.columns = cardCell['columns']; + } + + public multiActionTrackBy(index: number, item: any | MultiActionListEntity) { + if (!this.dataSource) { + return null; + } + if (this.isMultiActionItem(item)) { + return this.dataSource.trackBy(index, item.entity); + } + return this.dataSource.trackBy(index, item); + } + + public isMultiActionItem(component: any | MultiActionListEntity) { + return component instanceof MultiActionListEntity; } } diff --git a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-item/meta-card-item.component.ts b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-item/meta-card-item.component.ts index ec19984bbe..a87c530066 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-item/meta-card-item.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-item/meta-card-item.component.ts @@ -11,6 +11,7 @@ import { MetaCardValueComponent } from '../meta-card-value/meta-card-value.compo }) export class MetaCardItemComponent implements OnInit { + defaultStyle = 'row'; styles = { row: 'meta-card-item-row', 'row-top': 'meta-card-item-row-top', @@ -27,10 +28,10 @@ export class MetaCardItemComponent implements OnInit { @ViewChild('content') content: TemplateRef; - @Input() customStyle = 'row'; + @Input() customStyle = this.defaultStyle; ngOnInit() { - this.itemStyle = this.styles[this.customStyle]; + this.itemStyle = this.styles[this.customStyle || this.defaultStyle]; } } diff --git a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-value/meta-card-value.component.ts b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-value/meta-card-value.component.ts index 0fa975bb74..6b2351cbd0 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-value/meta-card-value.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-cards/meta-card/meta-card-value/meta-card-value.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { ChangeDetectionStrategy, Component, TemplateRef, ViewChild } from '@angular/core'; @Component({ selector: 'app-meta-card-value', @@ -6,14 +6,7 @@ import { ChangeDetectionStrategy, Component, OnInit, TemplateRef, ViewChild } fr styleUrls: ['./meta-card-value.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class MetaCardValueComponent implements OnInit { - +export class MetaCardValueComponent { @ViewChild(TemplateRef) content: TemplateRef; - - constructor() { } - - ngOnInit() { - } - } diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/app-table-cell-default/app-table-cell-default.component.html b/src/frontend/packages/core/src/shared/components/list/list-table/app-table-cell-default/app-table-cell-default.component.html index 3933fe2f38..9bc259ff74 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/app-table-cell-default/app-table-cell-default.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-table/app-table-cell-default/app-table-cell-default.component.html @@ -26,4 +26,4 @@ {{ value }} - - + \ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/app-table-cell-default/app-table-cell-default.component.ts b/src/frontend/packages/core/src/shared/components/list/list-table/app-table-cell-default/app-table-cell-default.component.ts index a0912122fc..334fd69f44 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/app-table-cell-default/app-table-cell-default.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-table/app-table-cell-default/app-table-cell-default.component.ts @@ -22,7 +22,17 @@ export class TableCellDefaultComponent extends TableCellCustom implements set row(row: T) { this.pRow = row; if (row) { - this.setValue(row); + this.setValue(row, this.schemaKey); + } + } + + private pSchemaKey: string; + @Input('schemaKey') + get schemaKey() { return this.pSchemaKey; } + set schemaKey(schemaKey: string) { + this.pSchemaKey = schemaKey; + if (this.row) { + this.setValue(this.row, schemaKey); } } @@ -33,7 +43,7 @@ export class TableCellDefaultComponent extends TableCellCustom implements public isExternalLink = false; public linkValue: string; public linkTarget = '_self'; - public valueGenerator: (row: T) => string; + public valueGenerator: (row: T, schemaKey?: string) => string; public showShortLink = false; public init() { @@ -82,11 +92,11 @@ export class TableCellDefaultComponent extends TableCellCustom implements }); } - private setValue(row: T) { + private setValue(row: T, schemaKey?: string) { if (this.cellDefinition && this.cellDefinition.asyncValue) { this.setupAsync(row); } else if (this.valueGenerator) { - this.valueContext.value = this.valueGenerator(row); + this.valueContext.value = this.valueGenerator(row, schemaKey); } } diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table-cell/table-cell.component.ts b/src/frontend/packages/core/src/shared/components/list/list-table/table-cell/table-cell.component.ts index 2e0a6232bb..fa6e19cfde 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table-cell/table-cell.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table-cell/table-cell.component.ts @@ -3,10 +3,7 @@ import { Component, ComponentFactoryResolver, Input, - OnChanges, OnInit, - SimpleChange, - SimpleChanges, Type, ViewChild, ViewContainerRef, @@ -118,6 +115,7 @@ import { import { TableCellSelectComponent } from '../table-cell-select/table-cell-select.component'; import { TableHeaderSelectComponent } from '../table-header-select/table-header-select.component'; import { ICellDefinition } from '../table.types'; +import { MultiActionListEntity } from './../../../../monitors/pagination-monitor'; /* tslint:enable:max-line-length */ export const listTableCells = [ @@ -177,16 +175,31 @@ export const listTableCells = [ // NgComponentOutlet (create in html with custom external module factory). Alternatively try marking as entry component where they live? entryComponents: [...listTableCells] }) -export class TableCellComponent implements OnInit, OnChanges { +export class TableCellComponent implements OnInit { @ViewChild('target', { read: ViewContainerRef }) target: ViewContainerRef; + private rcRow: T | MultiActionListEntity; @Input() dataSource = null as IListDataSource; @Input() component: Type<{}>; @Input() cellDefinition: ICellDefinition; @Input() func: () => string; - @Input() row: T; + @Input() set row(row: T | MultiActionListEntity) { + if (this.cellComponent) { + const { rowValue, entityKey } = this.getRowData(row); + this.cellComponent.row = rowValue; + this.cellComponent.entityKey = entityKey; + if (this.dataSource.getRowState) { + this.cellComponent.rowState = this.dataSource.getRowState(rowValue, entityKey); + } + } + this.rcRow = row; + } + get row() { + return this.rcRow; + } + @Input() config: any; private cellComponent: TableCellCustom; @@ -211,18 +224,28 @@ export class TableCellComponent implements OnInit, OnChanges { return !!component ? this.target.createComponent(component) : null; } + private getRowData(rowData: T | MultiActionListEntity) { + const rowValue = MultiActionListEntity.getEntity(rowData); + const entityKey = MultiActionListEntity.getEntityKey(rowData); + return { + rowValue, + entityKey + }; + } + ngOnInit() { const component = this.createComponent(); if (component) { // Add to target to ensure ngcontent is correct in new component this.cellComponent = component.instance as TableCellCustom; - - this.cellComponent.row = this.row; + const { rowValue, entityKey } = this.getRowData(this.row); + this.cellComponent.row = rowValue; + this.cellComponent.entityKey = entityKey; this.cellComponent.dataSource = this.dataSource; this.cellComponent.config = this.config; if (this.dataSource.getRowState) { - this.cellComponent.rowState = this.dataSource.getRowState(this.row); + this.cellComponent.rowState = this.dataSource.getRowState(rowValue, entityKey); } if (this.cellDefinition) { const defaultTableCell = this.cellComponent as TableCellDefaultComponent; @@ -232,10 +255,4 @@ export class TableCellComponent implements OnInit, OnChanges { } } - ngOnChanges(changes: SimpleChanges) { - const row: SimpleChange = changes.row; - if (row && this.cellComponent && row.previousValue !== row.currentValue) { - this.cellComponent.row = { ...row.currentValue }; - } - } } diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table.component.html b/src/frontend/packages/core/src/shared/components/list/list-table/table.component.html index 79b0f22ce0..462db288ed 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table.component.html @@ -1,28 +1,38 @@
- + +
- +
- - + +
- - + +
- - + +
- - + + +
-
+ \ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table.types.ts b/src/frontend/packages/core/src/shared/components/list/list-table/table.types.ts index 9b67e6d6e6..8124ebcc18 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table.types.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table.types.ts @@ -18,13 +18,13 @@ export interface ICellDefinition { // Dot separated path to get the value from the row valuePath?: string; // Takes president over valuePath - getValue?: (row: T) => string; + getValue?: (row: T, schemaKey?: string) => string; // Should the value of getLink be used in a href or routerLink externalLink?: boolean; // Automatically turns the cell into a link - getLink?: (row: T) => string; + getLink?: (row: T, schemaKey?: string) => string; // Used in conjunction with asyncValue - getAsyncLink?: (value) => string; + getAsyncLink?: (value, schemaKey?: string) => string; newTab?: boolean; asyncValue?: ICellAsyncValue; showShortLink?: boolean; diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/app-sevice-bindings/app-service-binding-card/app-service-binding-card.component.html b/src/frontend/packages/core/src/shared/components/list/list-types/app-sevice-bindings/app-service-binding-card/app-service-binding-card.component.html index 7d7893e4ed..842bf02253 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/app-sevice-bindings/app-service-binding-card/app-service-binding-card.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-types/app-sevice-bindings/app-service-binding-card/app-service-binding-card.component.html @@ -5,13 +5,7 @@ - - - - {{ (service$ | async)?.entity.entity.description}} - - - + {{ data.label }} {{ data.data$ | async }} @@ -27,4 +21,4 @@ - + \ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/app-sevice-bindings/app-service-binding-card/app-service-binding-card.component.spec.ts b/src/frontend/packages/core/src/shared/components/list/list-types/app-sevice-bindings/app-service-binding-card/app-service-binding-card.component.spec.ts index aab6e5ee8c..10b74603a3 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/app-sevice-bindings/app-service-binding-card/app-service-binding-card.component.spec.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/app-sevice-bindings/app-service-binding-card/app-service-binding-card.component.spec.ts @@ -1,12 +1,14 @@ import { DatePipe } from '@angular/common'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { - ApplicationEnvVarsHelper, -} from '../../../../../../features/applications/application/application-tabs-base/tabs/build-tab/application-env-vars.service'; +import { APIResource } from '../../../../../../../../store/src/types/api.types'; import { generateTestApplicationServiceProvider } from '../../../../../../../test-framework/application-service-helper'; import { BaseTestModules } from '../../../../../../../test-framework/cloud-foundry-endpoint-service.helper'; import { createBasicStoreModule } from '../../../../../../../test-framework/store-test-helper'; +import { IServiceInstance } from '../../../../../../core/cf-api-svc.types'; +import { + ApplicationEnvVarsHelper, +} from '../../../../../../features/applications/application/application-tabs-base/tabs/build-tab/application-env-vars.service'; import { ServiceActionHelperService } from '../../../../../data-services/service-action-helper.service'; import { EntityMonitorFactory } from '../../../../../monitors/entity-monitor.factory.service'; import { PaginationMonitorFactory } from '../../../../../monitors/pagination-monitor.factory'; @@ -51,6 +53,9 @@ describe('AppServiceBindingCardComponent', () => { volume_mounts: [], app_url: '', service_instance_url: '', + service_instance: { + entity: {} + } as APIResource }, metadata: { guid: '', diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/app-sevice-bindings/app-service-binding-card/app-service-binding-card.component.ts b/src/frontend/packages/core/src/shared/components/list/list-types/app-sevice-bindings/app-service-binding-card/app-service-binding-card.component.ts index 4650e998fb..bac562699d 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/app-sevice-bindings/app-service-binding-card/app-service-binding-card.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/app-sevice-bindings/app-service-binding-card/app-service-binding-card.component.ts @@ -1,31 +1,37 @@ import { DatePipe } from '@angular/common'; import { Component, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material'; -import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; +import { combineLatest as observableCombineLatest, Observable, of as observableOf, of } from 'rxjs'; import { filter, first, map, switchMap } from 'rxjs/operators'; import { GetServiceInstance } from '../../../../../../../../store/src/actions/service-instances.actions'; +import { GetUserProvidedService } from '../../../../../../../../store/src/actions/user-provided-service.actions'; import { entityFactory, serviceBindingSchemaKey, serviceInstancesSchemaKey, + userProvidedServiceInstanceSchemaKey, } from '../../../../../../../../store/src/helpers/entity-factory'; import { APIResource, EntityInfo } from '../../../../../../../../store/src/types/api.types'; import { AppEnvVarsState } from '../../../../../../../../store/src/types/app-metadata.types'; -import { IService, IServiceBinding, IServiceInstance } from '../../../../../../core/cf-api-svc.types'; +import { + IService, + IServiceBinding, + IServiceInstance, + IUserProvidedServiceInstance, +} from '../../../../../../core/cf-api-svc.types'; import { CurrentUserPermissions } from '../../../../../../core/current-user-permissions.config'; import { CurrentUserPermissionsService } from '../../../../../../core/current-user-permissions.service'; import { EntityServiceFactory } from '../../../../../../core/entity-service-factory.service'; import { ApplicationService } from '../../../../../../features/applications/application.service'; +import { isUserProvidedServiceInstance } from '../../../../../../features/cloud-foundry/cf.helpers'; import { getCfService } from '../../../../../../features/service-catalog/services-helper'; import { ServiceActionHelperService } from '../../../../../data-services/service-action-helper.service'; import { ComponentEntityMonitorConfig } from '../../../../../shared.types'; import { AppChip } from '../../../../chips/chips.component'; import { EnvVarViewComponent } from '../../../../env-var-view/env-var-view.component'; import { MetaCardMenuItem } from '../../../list-cards/meta-card/meta-card-base/meta-card.component'; -import { CardCell, IListRowCell, IListRowCellData } from '../../../list.types'; - - +import { CardCell, IListRowCell } from '../../../list.types'; interface EnvVarData { key: string; @@ -39,13 +45,18 @@ interface EnvVarData { export class AppServiceBindingCardComponent extends CardCell> implements OnInit, IListRowCell { envVarsAvailable$: Observable; - listData: IListRowCellData[]; - envVarUrl: string; + listData: { + label: string; + data$: Observable; + customStyle?: string; + }[]; cardMenu: MetaCardMenuItem[]; - service$: Observable>>; - serviceInstance$: Observable>>; - tags$: Observable[]>; + service$: Observable> | null>; + serviceInstance$: Observable>>; + tags$: Observable[]>; entityConfig: ComponentEntityMonitorConfig; + private envVarServicesSection$: Observable; + private isUserProvidedServiceInstance: boolean; constructor( private dialog: MatDialog, @@ -80,58 +91,106 @@ export class AppServiceBindingCardComponent extends CardCell>( + + this.isUserProvidedServiceInstance = !!isUserProvidedServiceInstance(this.row.entity.service_instance.entity); + if (this.isUserProvidedServiceInstance) { + this.setupAsUserProvidedServiceInstance(); + } else { + this.setupAsServiceInstance(); + } + + this.listData.push({ + label: 'Date Created On', + data$: observableOf(this.datePipe.transform(this.row.metadata.created_at, 'medium')) + }); + + this.tags$ = this.serviceInstance$.pipe( + filter(o => !!o.entity.entity.tags), + map(o => o.entity.entity.tags.map(t => ({ value: t }))) + ); + + this.setupEnvVars(); + } + + private setupAsServiceInstance() { + const serviceInstance$ = this.entityServiceFactory.create>( serviceInstancesSchemaKey, entityFactory(serviceInstancesSchemaKey), this.row.entity.service_instance_guid, new GetServiceInstance(this.row.entity.service_instance_guid, this.appService.cfGuid), true ).waitForEntity$; - - this.service$ = this.serviceInstance$.pipe( + this.serviceInstance$ = serviceInstance$; + this.service$ = serviceInstance$.pipe( switchMap(o => getCfService(o.entity.entity.service_guid, this.appService.cfGuid, this.entityServiceFactory).waitForEntity$), filter(service => !!service) ); - - this.listData = [ - { - label: 'Service Name', - data$: this.service$.pipe( - map(service => service.entity.entity.label) - ) - }, - { - label: 'Service Plan', - data$: this.serviceInstance$.pipe( - map(service => service.entity.entity.service_plan.entity.name) - ) - }, - { - label: 'Date Created On', - data$: observableOf(this.datePipe.transform(this.row.metadata.created_at, 'medium')) - } + this.listData = [{ + label: null, + data$: this.service$.pipe( + map(service => service.entity.entity.description) + ), + customStyle: 'long-text' + }, + { + label: 'Service Name', + data$: this.service$.pipe( + map(service => service.entity.entity.label) + ) + }, + { + label: 'Service Plan', + data$: serviceInstance$.pipe( + map(service => service.entity.entity.service_plan.entity.name) + ) + } ]; + this.envVarServicesSection$ = this.service$.pipe(map(s => s.entity.entity.label)); + } - this.tags$ = this.serviceInstance$.pipe( - map(o => o.entity.entity.tags.map(t => ({ value: t }))) - ); - this.envVarUrl = `/applications/${this.appService.cfGuid}/${this.appService.appGuid}/service-bindings/${this.row.metadata.guid}/vars`; + private setupAsUserProvidedServiceInstance() { + const userProvidedServiceInstance$ = this.entityServiceFactory.create>( + userProvidedServiceInstanceSchemaKey, + entityFactory(userProvidedServiceInstanceSchemaKey), + this.row.entity.service_instance_guid, + new GetUserProvidedService(this.row.entity.service_instance_guid, this.appService.cfGuid), + true + ).waitForEntity$; + this.serviceInstance$ = userProvidedServiceInstance$; + this.service$ = of(null); + this.listData = [{ + label: null, + data$: of('User Provided Service Instance'), + customStyle: 'long-text' + }, { + label: 'Route Service URL', + data$: userProvidedServiceInstance$.pipe( + map(service => service.entity.entity.route_service_url) + ) + }, { + label: 'Syslog Drain URL', + data$: userProvidedServiceInstance$.pipe( + map(service => service.entity.entity.syslog_drain_url) + ) + }]; + this.envVarServicesSection$ = of('user-provided'); + } - this.envVarsAvailable$ = observableCombineLatest(this.service$, this.serviceInstance$, this.appService.appEnvVars.entities$) + private setupEnvVars() { + this.envVarsAvailable$ = observableCombineLatest( + this.envVarServicesSection$, + this.serviceInstance$, + this.appService.appEnvVars.entities$) .pipe( first(), - map(([service, serviceInstance, allEnvVars]) => { + map(([serviceLabel, serviceInstance, allEnvVars]) => { const systemEnvJson = (allEnvVars as APIResource[])[0].entity.system_env_json; const serviceInstanceName = serviceInstance.entity.entity.name; - const serviceLabel = (service as EntityInfo>).entity.entity.label; - - if (systemEnvJson.VCAP_SERVICES[serviceLabel]) { - return { - key: serviceInstanceName, - value: systemEnvJson.VCAP_SERVICES[serviceLabel].find(s => s.name === serviceInstanceName) - }; - } - return null; + + return systemEnvJson.VCAP_SERVICES[serviceLabel] ? { + key: serviceInstanceName, + value: systemEnvJson.VCAP_SERVICES[serviceLabel].find(s => s.name === serviceInstanceName) + } : null; }), filter(p => !!p), ); @@ -148,14 +207,17 @@ export class AppServiceBindingCardComponent extends CardCell this.serviceActionHelperService.editServiceBinding( this.row.entity.service_instance_guid, this.appService.cfGuid, - { appId: this.appService.appGuid } + { appId: this.appService.appGuid }, + this.isUserProvidedServiceInstance ) } diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/app-sevice-bindings/app-service-binding-data-source.ts b/src/frontend/packages/core/src/shared/components/list/list-types/app-sevice-bindings/app-service-binding-data-source.ts index 14a44f817e..560bec4aec 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/app-sevice-bindings/app-service-binding-data-source.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/app-sevice-bindings/app-service-binding-data-source.ts @@ -1,27 +1,27 @@ import { Store } from '@ngrx/store'; -import { ApplicationService } from '../../../../../features/applications/application.service'; -import { getRowMetadata } from '../../../../../features/cloud-foundry/cf.helpers'; - -import { ListDataSource } from '../../data-sources-controllers/list-data-source'; -import { IListConfig } from '../../list.component.types'; -import { APIResource } from '../../../../../../../store/src/types/api.types'; -import { - createEntityRelationPaginationKey, - createEntityRelationKey -} from '../../../../../../../store/src/helpers/entity-relations/entity-relations.types'; +import { GetAppServiceBindings } from '../../../../../../../store/src/actions/application-service-routes.actions'; +import { AppState } from '../../../../../../../store/src/app-state'; import { applicationSchemaKey, entityFactory, serviceBindingSchemaKey, serviceInstancesSchemaKey, - serviceSchemaKey, servicePlanSchemaKey, + serviceSchemaKey, } from '../../../../../../../store/src/helpers/entity-factory'; -import { GetAppServiceBindings } from '../../../../../../../store/src/actions/application-service-routes.actions'; -import { AppState } from '../../../../../../../store/src/app-state'; +import { + createEntityRelationKey, + createEntityRelationPaginationKey, +} from '../../../../../../../store/src/helpers/entity-relations/entity-relations.types'; +import { APIResource } from '../../../../../../../store/src/types/api.types'; +import { IServiceBinding } from '../../../../../core/cf-api-svc.types'; +import { ApplicationService } from '../../../../../features/applications/application.service'; +import { getRowMetadata } from '../../../../../features/cloud-foundry/cf.helpers'; +import { ListDataSource } from '../../data-sources-controllers/list-data-source'; +import { IListConfig } from '../../list.component.types'; -export class AppServiceBindingDataSource extends ListDataSource { +export class AppServiceBindingDataSource extends ListDataSource> { static createGetAllServiceBindings(appGuid: string, cfGuid: string) { const paginationKey = createEntityRelationPaginationKey(serviceBindingSchemaKey, appGuid); return new GetAppServiceBindings( @@ -33,7 +33,7 @@ export class AppServiceBindingDataSource extends ListDataSource { ]); } - constructor(store: Store, appService: ApplicationService, listConfig?: IListConfig) { + constructor(store: Store, appService: ApplicationService, listConfig?: IListConfig>) { const action = AppServiceBindingDataSource.createGetAllServiceBindings(appService.appGuid, appService.cfGuid); super({ store, diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/app-sevice-bindings/app-service-binding-list-config.service.ts b/src/frontend/packages/core/src/shared/components/list/list-types/app-sevice-bindings/app-service-binding-list-config.service.ts index 18ed78d0e6..035c421fee 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/app-sevice-bindings/app-service-binding-list-config.service.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/app-sevice-bindings/app-service-binding-list-config.service.ts @@ -3,9 +3,15 @@ import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { switchMap } from 'rxjs/operators'; +import { ListView } from '../../../../../../../store/src/actions/list.actions'; +import { RouterNav } from '../../../../../../../store/src/actions/router.actions'; +import { AppState } from '../../../../../../../store/src/app-state'; +import { APIResource } from '../../../../../../../store/src/types/api.types'; +import { IServiceBinding } from '../../../../../core/cf-api-svc.types'; import { CurrentUserPermissions } from '../../../../../core/current-user-permissions.config'; import { CurrentUserPermissionsService } from '../../../../../core/current-user-permissions.service'; import { ApplicationService } from '../../../../../features/applications/application.service'; +import { isServiceInstance } from '../../../../../features/cloud-foundry/cf.helpers'; import { DataFunctionDefinition } from '../../data-sources-controllers/list-data-source'; import { IGlobalListAction, ListViewTypes } from '../../list.component.types'; import { BaseCfListConfig } from '../base-cf/base-cf-list-config'; @@ -14,19 +20,15 @@ import { } from '../cf-spaces-service-instances/table-cell-service-instance-tags/table-cell-service-instance-tags.component'; import { AppServiceBindingCardComponent } from './app-service-binding-card/app-service-binding-card.component'; import { AppServiceBindingDataSource } from './app-service-binding-data-source'; -import { APIResource } from '../../../../../../../store/src/types/api.types'; -import { ListView } from '../../../../../../../store/src/actions/list.actions'; -import { RouterNav } from '../../../../../../../store/src/actions/router.actions'; -import { AppState } from '../../../../../../../store/src/app-state'; @Injectable() -export class AppServiceBindingListConfigService extends BaseCfListConfig { +export class AppServiceBindingListConfigService extends BaseCfListConfig> { dataSource: AppServiceBindingDataSource; cardComponent = AppServiceBindingCardComponent; viewType = ListViewTypes.BOTH; defaultView = 'cards' as ListView; - private listActionAdd: IGlobalListAction = { + private listActionAdd: IGlobalListAction> = { action: () => { this.store.dispatch(new RouterNav({ path: ['applications', this.appService.cfGuid, this.appService.appGuid, 'bind'] })); }, @@ -56,7 +58,10 @@ export class AppServiceBindingListConfigService extends BaseCfListConfig 'Service', cellDefinition: { - getValue: (row) => row.entity.service_instance.entity.service.entity.label + getValue: (row: APIResource) => { + const si = isServiceInstance(row.entity.service_instance.entity); + return si ? si.service.entity.label : 'User Service'; + } }, cellFlex: '1' }, @@ -64,7 +69,10 @@ export class AppServiceBindingListConfigService extends BaseCfListConfig 'Plan', cellDefinition: { - getValue: (row) => row.entity.service_instance.entity.service_plan.entity.name + getValue: (row: APIResource) => { + const si = isServiceInstance(row.entity.service_instance.entity); + return si ? si.service_plan.entity.name : null; + } }, cellFlex: '1' }, diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/app/cf-apps-data-source.ts b/src/frontend/packages/core/src/shared/components/list/list-types/app/cf-apps-data-source.ts index a375ca4156..28b29c4407 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/app/cf-apps-data-source.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/app/cf-apps-data-source.ts @@ -3,27 +3,27 @@ import { Subscription } from 'rxjs'; import { tag } from 'rxjs-spy/operators/tag'; import { debounceTime, delay, distinctUntilChanged, map, withLatestFrom } from 'rxjs/operators'; -import { DispatchSequencer, DispatchSequencerAction } from '../../../../../core/dispatch-sequencer'; -import { getRowMetadata, cfOrgSpaceFilter } from '../../../../../features/cloud-foundry/cf.helpers'; - -import { distinctPageUntilChanged, ListDataSource } from '../../data-sources-controllers/list-data-source'; -import { IListConfig } from '../../list.component.types'; +import { GetAppStatsAction } from '../../../../../../../store/src/actions/app-metadata.actions'; import { GetAllApplications } from '../../../../../../../store/src/actions/application.actions'; -import { createEntityRelationKey } from '../../../../../../../store/src/helpers/entity-relations/entity-relations.types'; +import { CreatePagination } from '../../../../../../../store/src/actions/pagination.actions'; +import { AppState } from '../../../../../../../store/src/app-state'; import { applicationSchemaKey, - spaceSchemaKey, + entityFactory, organizationSchemaKey, routeSchemaKey, - entityFactory + spaceSchemaKey, } from '../../../../../../../store/src/helpers/entity-factory'; +import { createEntityRelationKey } from '../../../../../../../store/src/helpers/entity-relations/entity-relations.types'; import { APIResource } from '../../../../../../../store/src/types/api.types'; import { PaginationParam } from '../../../../../../../store/src/types/pagination.types'; -import { AppState } from '../../../../../../../store/src/app-state'; -import { CreatePagination } from '../../../../../../../store/src/actions/pagination.actions'; -import { GetAppStatsAction } from '../../../../../../../store/src/actions/app-metadata.actions'; -import { ListPaginationMultiFilterChange } from '../../data-sources-controllers/list-data-source-types'; +import { DispatchSequencer, DispatchSequencerAction } from '../../../../../core/dispatch-sequencer'; +import { cfOrgSpaceFilter, getRowMetadata } from '../../../../../features/cloud-foundry/cf.helpers'; import { createCfOrSpaceMultipleFilterFn } from '../../../../data-services/cf-org-space-service.service'; +import { MultiActionListEntity } from '../../../../monitors/pagination-monitor'; +import { distinctPageUntilChanged, ListDataSource } from '../../data-sources-controllers/list-data-source'; +import { ListPaginationMultiFilterChange } from '../../data-sources-controllers/list-data-source-types'; +import { IListConfig } from '../../list.component.types'; export function createGetAllAppAction(paginationKey): GetAllApplications { return new GetAllApplications(paginationKey, null, [ @@ -95,6 +95,9 @@ export class CfAppsDataSource extends ListDataSource { } const actions = new Array(); page.forEach(app => { + if (app instanceof MultiActionListEntity) { + app = app.entity; + } const appState = app.entity.state; const appGuid = app.metadata.guid; const cfGuid = app.entity.cfGuid; diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/base-cf/base-cf-list-config.ts b/src/frontend/packages/core/src/shared/components/list/list-types/base-cf/base-cf-list-config.ts index 804e8079c4..747de07e5a 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/base-cf/base-cf-list-config.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/base-cf/base-cf-list-config.ts @@ -1,6 +1,7 @@ +import { ListView } from '../../../../../../../store/src/actions/list.actions'; import { IListDataSource } from '../../data-sources-controllers/list-data-source-types'; +import { CardTypes } from '../../list-cards/card/card.component'; import { IListConfig, ListViewTypes } from '../../list.component.types'; -import { ListView } from '../../../../../../../store/src/actions/list.actions'; export class BaseCfListConfig implements IListConfig { @@ -8,7 +9,7 @@ export class BaseCfListConfig implements IListConfig { isLocal = true; viewType = ListViewTypes.CARD_ONLY; defaultView = 'cards' as ListView; - cardComponent; + cardComponent: CardTypes; enableTextFilter = false; showMetricsRange = false; getColumns = () => []; diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/cf-routes/cf-routes-list-config-base.ts b/src/frontend/packages/core/src/shared/components/list/list-types/cf-routes/cf-routes-list-config-base.ts index 1688cb5618..cfd6e7cc93 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/cf-routes/cf-routes-list-config-base.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/cf-routes/cf-routes-list-config-base.ts @@ -138,7 +138,7 @@ export abstract class CfRoutesListConfigBase implements IListConfig routeGuid, appGuid, this.cfGuid, - this.removeEntityOnUnmap ? this.getDataSource().action.paginationKey : null + this.removeEntityOnUnmap ? this.getDataSource().masterAction.paginationKey : null )); }); } diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/cf-routes/cf-routes-list-config.service.ts b/src/frontend/packages/core/src/shared/components/list/list-types/cf-routes/cf-routes-list-config.service.ts index 176358bc60..0a2cce34e2 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/cf-routes/cf-routes-list-config.service.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/cf-routes/cf-routes-list-config.service.ts @@ -4,6 +4,8 @@ import { Store } from '@ngrx/store'; import { Observable, of as observableOf } from 'rxjs'; import { publishReplay, refCount, switchMap } from 'rxjs/operators'; +import { AppState } from '../../../../../../../store/src/app-state'; +import { APIResource } from '../../../../../../../store/src/types/api.types'; import { CurrentUserPermissions } from '../../../../../core/current-user-permissions.config'; import { CurrentUserPermissionsService } from '../../../../../core/current-user-permissions.service'; import { CloudFoundryEndpointService } from '../../../../../features/cloud-foundry/services/cloud-foundry-endpoint.service'; @@ -17,8 +19,6 @@ import { IListConfig, IListMultiFilterConfig } from '../../list.component.types' import { CfRoutesDataSource } from './cf-routes-data-source'; import { ListCfRoute } from './cf-routes-data-source-base'; import { CfRoutesListConfigBase } from './cf-routes-list-config-base'; -import { APIResource } from '../../../../../../../store/src/types/api.types'; -import { AppState } from '../../../../../../../store/src/app-state'; @Injectable() @@ -70,8 +70,8 @@ export class CfRoutesListConfigService extends CfRoutesListConfigBase implements ]; this.getMultiFiltersConfigs = () => multiFilterConfigs; initCfOrgSpaceService(store, cfOrgSpaceService, - this.dataSource.action.entityKey, - this.dataSource.action.paginationKey).subscribe(); + this.dataSource.masterAction.entityKey, + this.dataSource.masterAction.paginationKey).subscribe(); cfOrgSpaceService.cf.select.next(cfService.cfGuid); } } diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/cf-services/cf-service-instances-list-config.base.ts b/src/frontend/packages/core/src/shared/components/list/list-types/cf-services/cf-service-instances-list-config.base.ts index 35e9275f54..1ee1440b6f 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/cf-services/cf-service-instances-list-config.base.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/cf-services/cf-service-instances-list-config.base.ts @@ -4,11 +4,15 @@ import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; +import { ListView } from '../../../../../../../store/src/actions/list.actions'; +import { AppState } from '../../../../../../../store/src/app-state'; +import { APIResource } from '../../../../../../../store/src/types/api.types'; import { IServiceInstance } from '../../../../../core/cf-api-svc.types'; import { CurrentUserPermissions } from '../../../../../core/current-user-permissions.config'; import { CurrentUserPermissionsService } from '../../../../../core/current-user-permissions.service'; import { ListDataSource } from '../../../../../shared/components/list/data-sources-controllers/list-data-source'; import { ServiceActionHelperService } from '../../../../data-services/service-action-helper.service'; +import { CANCEL_ORG_ID_PARAM, CANCEL_SPACE_ID_PARAM } from '../../../add-service-instance/csi-mode.service'; import { ITableColumn } from '../../list-table/table.types'; import { defaultPaginationPageSizeOptionsTable, IListAction, IListConfig, ListViewTypes } from '../../list.component.types'; import { @@ -26,9 +30,6 @@ import { import { TableCellSpaceNameComponent, } from '../cf-spaces-service-instances/table-cell-space-name/table-cell-space-name.component'; -import { APIResource } from '../../../../../../../store/src/types/api.types'; -import { ListView } from '../../../../../../../store/src/actions/list.actions'; -import { AppState } from '../../../../../../../store/src/app-state'; interface CanCache { [spaceGuid: string]: Observable; @@ -140,7 +141,10 @@ export class CfServiceInstancesListConfigBase implements IListConfig = { action: (item: APIResource) => - this.serviceActionHelperService.editServiceBinding(item.metadata.guid, item.entity.cfGuid), + this.serviceActionHelperService.editServiceBinding(item.metadata.guid, item.entity.cfGuid, { + [CANCEL_SPACE_ID_PARAM]: item.entity.space_guid, + [CANCEL_ORG_ID_PARAM]: item.entity.space.entity.organization_guid + }), label: 'Edit', description: 'Edit Service Instance', createVisible: (row$: Observable>) => diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/cf-services/cf-services-data-source.ts b/src/frontend/packages/core/src/shared/components/list/list-types/cf-services/cf-services-data-source.ts index 02a1a4b574..8684570d9a 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/cf-services/cf-services-data-source.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/cf-services/cf-services-data-source.ts @@ -15,10 +15,9 @@ import { IListConfig } from '../../list.component.types'; export class CfServicesDataSource extends ListDataSource { constructor(store: Store, endpointGuid: string, listConfig?: IListConfig) { const paginationKey = createEntityRelationPaginationKey(endpointSchemaKey); - const action = new GetAllServices(paginationKey); super({ store, - action, + action: new GetAllServices(paginationKey), schema: entityFactory(serviceSchemaKey), getRowUniqueId: getRowMetadata, paginationKey, diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/cf-spaces-service-instances/table-cell-service-instance-tags/table-cell-service-instance-tags.component.spec.ts b/src/frontend/packages/core/src/shared/components/list/list-types/cf-spaces-service-instances/table-cell-service-instance-tags/table-cell-service-instance-tags.component.spec.ts index e7992d9caf..95d3b68f39 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/cf-spaces-service-instances/table-cell-service-instance-tags/table-cell-service-instance-tags.component.spec.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/cf-spaces-service-instances/table-cell-service-instance-tags/table-cell-service-instance-tags.component.spec.ts @@ -6,8 +6,8 @@ import { AppChipsComponent } from '../../../../chips/chips.component'; import { TableCellServiceInstanceTagsComponent } from './table-cell-service-instance-tags.component'; describe('TableCellServiceInstanceTagsComponent', () => { - let component: TableCellServiceInstanceTagsComponent; - let fixture: ComponentFixture>; + let component: TableCellServiceInstanceTagsComponent; + let fixture: ComponentFixture; beforeEach(async(() => { TestBed.configureTestingModule({ diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/cf-spaces-service-instances/table-cell-service-instance-tags/table-cell-service-instance-tags.component.ts b/src/frontend/packages/core/src/shared/components/list/list-types/cf-spaces-service-instances/table-cell-service-instance-tags/table-cell-service-instance-tags.component.ts index 9362c99058..7aa516717e 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/cf-spaces-service-instances/table-cell-service-instance-tags/table-cell-service-instance-tags.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/cf-spaces-service-instances/table-cell-service-instance-tags/table-cell-service-instance-tags.component.ts @@ -1,10 +1,10 @@ import { Component, Input } from '@angular/core'; import { of as observableOf } from 'rxjs'; -import { IServiceInstance } from '../../../../../../core/cf-api-svc.types'; +import { APIResource } from '../../../../../../../../store/src/types/api.types'; +import { IServiceInstance, IUserProvidedServiceInstance } from '../../../../../../core/cf-api-svc.types'; import { AppChip } from '../../../../chips/chips.component'; import { TableCellCustom } from '../../../list.types'; -import { APIResource } from '../../../../../../../../store/src/types/api.types'; interface Tag { value: string; @@ -15,14 +15,15 @@ interface Tag { templateUrl: './table-cell-service-instance-tags.component.html', styleUrls: ['./table-cell-service-instance-tags.component.scss'] }) -export class TableCellServiceInstanceTagsComponent extends TableCellCustom { +export class TableCellServiceInstanceTagsComponent + extends TableCellCustom | APIResource> { tags: AppChip[] = []; @Input('row') set row(row) { if (row) { this.tags.length = 0; - if (row.entity && row.entity.service_instance) { + if (row.entity && row.entity.service_instance && row.entity.service_instance.entity.tags) { row.entity.service_instance.entity.tags.forEach(t => { this.tags.push({ value: t, diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/cf-spaces-service-instances/table-cell-service-name/table-cell-service-name.component.ts b/src/frontend/packages/core/src/shared/components/list/list-types/cf-spaces-service-instances/table-cell-service-name/table-cell-service-name.component.ts index 2800b91b99..af2421fad9 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/cf-spaces-service-instances/table-cell-service-name/table-cell-service-name.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/cf-spaces-service-instances/table-cell-service-name/table-cell-service-name.component.ts @@ -1,6 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import { IServiceExtra } from '../../../../../../core/cf-api-svc.types'; @@ -8,6 +8,7 @@ import { EntityServiceFactory } from '../../../../../../core/entity-service-fact import { TableCellCustom } from '../../../list.types'; import { AppState } from '../../../../../../../../store/src/app-state'; import { getCfService } from '../../../../../../features/service-catalog/services-helper'; +import { userProvidedServiceInstanceSchemaKey } from '../../../../../../../../store/src/helpers/entity-factory'; @Component({ selector: 'app-table-cell-service-name', @@ -18,22 +19,26 @@ export class TableCellServiceNameComponent extends TableCellCustom impleme serviceName$: Observable; @Input() row; + @Input() entityKey: string; constructor(private store: Store, private entityServiceFactory: EntityServiceFactory) { super(); } ngOnInit() { - this.serviceName$ = getCfService(this.row.entity.service_guid, this.row.entity.cfGuid, this.entityServiceFactory).waitForEntity$.pipe( - filter(s => !!s), - map(s => { - let serviceLabel = s.entity.entity.label; - try { - const extraInfo: IServiceExtra = s.entity.entity.extra ? JSON.parse(s.entity.entity.extra) : null; - serviceLabel = extraInfo && extraInfo.displayName ? extraInfo.displayName : serviceLabel; - } catch (e) { } - return serviceLabel; - }) - ); + if (this.entityKey === userProvidedServiceInstanceSchemaKey) { + this.serviceName$ = of('User Provided'); + } else { + this.serviceName$ = getCfService(this.row.entity.service_guid, this.row.entity.cfGuid, this.entityServiceFactory).waitForEntity$.pipe( + filter(s => !!s), + map(s => { + let serviceLabel = s.entity.entity.label || 'User Provided'; + try { + const extraInfo: IServiceExtra = s.entity.entity.extra ? JSON.parse(s.entity.entity.extra) : null; + serviceLabel = extraInfo && extraInfo.displayName ? extraInfo.displayName : serviceLabel; + } catch (e) { } + return serviceLabel; + }) + ); + } } - } diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/cf-spaces-service-instances/table-cell-service-plan/table-cell-service-plan.component.ts b/src/frontend/packages/core/src/shared/components/list/list-types/cf-spaces-service-instances/table-cell-service-plan/table-cell-service-plan.component.ts index 797732cc8a..748e615be3 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/cf-spaces-service-instances/table-cell-service-plan/table-cell-service-plan.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/cf-spaces-service-instances/table-cell-service-plan/table-cell-service-plan.component.ts @@ -1,6 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import { IServicePlan } from '../../../../../../core/cf-api-svc.types'; @@ -8,6 +8,7 @@ import { TableCellCustom } from '../../../list.types'; import { AppState } from '../../../../../../../../store/src/app-state'; import { selectEntity } from '../../../../../../../../store/src/selectors/api.selectors'; import { APIResource } from '../../../../../../../../store/src/types/api.types'; +import { userProvidedServiceInstanceSchemaKey } from '../../../../../../../../store/src/helpers/entity-factory'; @Component({ selector: 'app-table-cell-service-plan', @@ -17,14 +18,19 @@ import { APIResource } from '../../../../../../../../store/src/types/api.types'; export class TableCellServicePlanComponent extends TableCellCustom implements OnInit { @Input() row; + @Input() entityKey: string; servicePlanName$: Observable; constructor(private store: Store) { super(); } ngOnInit() { - this.servicePlanName$ = this.store.select(selectEntity>('servicePlan', this.row.entity.service_plan_guid)) - .pipe( - filter(s => !!s), - map(s => s.entity.name) - ); + if (this.entityKey === userProvidedServiceInstanceSchemaKey) { + this.servicePlanName$ = of('-'); + } else { + this.servicePlanName$ = this.store.select(selectEntity>('servicePlan', this.row.entity.service_plan_guid)) + .pipe( + filter(s => !!s), + map(s => s.entity.name) + ); + } } } diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/cf-spaces/cf-spaces-list-config.service.ts b/src/frontend/packages/core/src/shared/components/list/list-types/cf-spaces/cf-spaces-list-config.service.ts index 36027a9472..27ff550d79 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/cf-spaces/cf-spaces-list-config.service.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/cf-spaces/cf-spaces-list-config.service.ts @@ -14,7 +14,7 @@ import { CfSpaceCardComponent } from './cf-space-card/cf-space-card.component'; import { CfSpacesDataSourceService } from './cf-spaces-data-source.service'; @Injectable() -export class CfSpacesListConfigService implements IListConfig { +export class CfSpacesListConfigService implements IListConfig> { viewType = ListViewTypes.CARD_ONLY; dataSource: CfSpacesDataSourceService; cardComponent = CfSpaceCardComponent; diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-details/table-cell-endpoint-details.component.spec.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-details/table-cell-endpoint-details.component.spec.ts index 338a1ce656..fc615d8678 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-details/table-cell-endpoint-details.component.spec.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-details/table-cell-endpoint-details.component.spec.ts @@ -4,7 +4,7 @@ import { BaseTestModules } from '../../../../../../../test-framework/cloud-found import { EndpointListHelper } from '../endpoint-list.helpers'; import { TableCellEndpointDetailsComponent } from './table-cell-endpoint-details.component'; -fdescribe('TableCellEndpointDetailsComponent', () => { +describe('TableCellEndpointDetailsComponent', () => { let component: TableCellEndpointDetailsComponent; let fixture: ComponentFixture; diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/services-wall/service-instance-card/service-instance-card.component.html b/src/frontend/packages/core/src/shared/components/list/list-types/services-wall/service-instance-card/service-instance-card.component.html index 1f51a14aa9..25181b66cf 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/services-wall/service-instance-card/service-instance-card.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-types/services-wall/service-instance-card/service-instance-card.component.html @@ -1,6 +1,8 @@ - + - {{ serviceInstanceEntity.entity.name }} + {{ serviceInstanceEntity.entity.name }} + Space @@ -18,11 +20,12 @@ Applications Attached - {{ serviceInstanceEntity.entity.service_bindings.length }} + {{ serviceInstanceEntity.entity.service_bindings?.length }} Last Updated - {{ serviceInstanceEntity.entity.last_operation?.updated_at | date:'medium' }} + {{ serviceInstanceEntity.entity.last_operation?.updated_at | date:'medium' }} + Dashboard @@ -30,7 +33,8 @@