cloudfoundry/stratos

View on GitHub
src/frontend/packages/cloud-foundry/src/shared/components/add-service-instance/specify-user-provided-details/specify-user-provided-details.component.ts

Summary

Maintainability
B
4 hrs
Test Coverage
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/chips';
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 { IUserProvidedServiceInstanceData } from '../../../../../../cloud-foundry/src/actions/user-provided-service.actions';
import { CFAppState } from '../../../../../../cloud-foundry/src/cf-app-state';
import {
  serviceBindingEntityType,
  userProvidedServiceInstanceEntityType,
} from '../../../../../../cloud-foundry/src/cf-entity-types';
import { createEntityRelationKey } from '../../../../../../cloud-foundry/src/entity-relations/entity-relations.types';
import {
  selectCreateServiceInstance,
} from '../../../../../../cloud-foundry/src/store/selectors/create-service-instance.selectors';
import { safeUnsubscribe, urlValidationExpression } from '../../../../../../core/src/core/utils.service';
import { environment } from '../../../../../../core/src/environments/environment';
import { StepOnNextResult } from '../../../../../../core/src/shared/components/stepper/step/step.component';
import { isValidJsonValidator } from '../../../../../../core/src/shared/form-validators';
import { APIResource } from '../../../../../../store/src/types/api.types';
import { IUserProvidedServiceInstance } from '../../../../cf-api-svc.types';
import { cfEntityCatalog } from '../../../../cf-entity-catalog';
import { AppNameUniqueChecking } from '../../../directives/app-name-unique.directive/app-name-unique.directive';
import { CloudFoundryUserProvidedServicesService } from '../../../services/cloud-foundry-user-provided-services.service';
import { AppServiceBindingDataSource } from '../../list/list-types/app-sevice-bindings/app-service-binding-data-source';
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 {

  constructor(
    private route: ActivatedRoute,
    private upsService: CloudFoundryUserProvidedServicesService,
    public modeService: CsiModeService,
    private store: Store<CFAppState>,
  ) {
    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();
  }
  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[] = [];
  private tagsChanged = new BehaviorSubject(true);

  @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;

  private originalFormValue;

  ngOnDestroy(): void {
    safeUnsubscribe(...this.subscriptions);
  }

  private setupValidate() {
    const obs = obsCombineLatest([
      this.createEditServiceInstance.statusChanges.pipe(startWith('INVALID')),
      this.bindExistingInstance.statusChanges.pipe(startWith('INVALID')),
      this.tagsChanged
    ]).pipe(
      map(([createValid, bindValid]) =>
        this.formStatusToBool(this.formMode === CreateServiceFormMode.CreateServiceInstance ? createValid : bindValid)
      ),
      map(valid => this.validAndChanged(valid)),
    );
    this.subscriptions.push(this.tagsChanged.subscribe(() => {
      if (this.formMode === CreateServiceFormMode.CreateServiceInstance) {
        this.createEditServiceInstance.markAsTouched();
        this.createEditServiceInstance.markAsDirty();
        this.createEditServiceInstance.updateValueAndValidity();
      } else {
        this.bindExistingInstance.markAsTouched();
        this.bindExistingInstance.markAsDirty();
        this.bindExistingInstance.updateValueAndValidity();
      }
    }));
    this.subscriptions.push(obs.subscribe(valid => this.valid.next(valid)));
  }

  private validAndChanged(isValid = false): boolean {
    // Determine if the step is valid given
    // 1) the form element's validation state
    // 2) if process is update... also consider whether the form values have changed

    // Not valid, return immediately
    if (!isValid) {
      return false;
    }

    // Valid, but not update. Skip second part
    if (!this.isUpdate) {
      return true;
    }

    // Haven't yet initialised correctly, skip
    if (!this.originalFormValue) {
      return false;
    }

    // Compare original and new form values
    const newFormValue = this.getServiceData();
    if (JSON.stringify(this.originalFormValue) === JSON.stringify(newFormValue)) {
      // No change, return false
      return false;
    }
    return true;
  }

  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(userProvidedServiceInstanceEntityType, serviceBindingEntityType)]
      )),
      map(upsis => upsis.map(upsi => {
        const alreadyBound = !!upsi.entity.service_bindings.find(binding => binding.entity.app_guid === this.appId);
        if (alreadyBound) {
          const updatedSvc: APIResource<IUserProvidedServiceInstance> = {
            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);
        this.originalFormValue = this.getServiceData();
      });
    }
  }

  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<StepOnNextResult> => {
    return this.isUpdate ?
      this.onNextUpdate() :
      this.formMode === CreateServiceFormMode.CreateServiceInstance ? this.onNextCreate() : this.onNextBind();
  };

  private onNextCreate(): Observable<StepOnNextResult> {
    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 success = !request.error;
        const redirect = !request.error;
        if (!!state.bindAppGuid && success) {
          const newGuid = request.response.result[0];
          return this.createApplicationServiceBinding(newGuid, state);
        }
        return observableOf({
          success,
          redirect,
          message: success ? '' : 'Failed to create User Provided Service Instance. Reason: "' + request.message + '"'
        });
      })
    );
  }

  private onNextBind(): Observable<StepOnNextResult> {
    return this.store.select(selectCreateServiceInstance).pipe(
      switchMap(data => this.createApplicationServiceBinding(this.bindExistingInstance.controls.serviceInstances.value, data))
    );
  }

  private createApplicationServiceBinding(serviceGuid: string, data: any): Observable<StepOnNextResult> {
    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
            cfEntityCatalog.appEnvVar.api.getMultiple(data.bindAppGuid, data.cfGuid);
            return { success: true, redirect: true };
          }
        })
      );
  }

  private onNextUpdate(): Observable<StepOnNextResult> {
    const updateData = this.getServiceData();
    return this.upsService.updateUserProvidedService(
      this.cfGuid,
      this.serviceInstanceId,
      updateData
    ).pipe(
      map(er => {
        if (!er.error) {
          // Update the application binding list
          const appId = this.appId || this.route.snapshot.queryParamMap.get('appId');
          if (appId) {
            this.store.dispatch(AppServiceBindingDataSource.createGetAllServiceBindings(appId, this.cfGuid));
          }
          return {
            success: true,
            redirect: true,
          };
        }
        return {
          success: false,
          redirect: false,
          message: `Failed to update service instance: ${er.message}`
        };
      })
    );
  }

  private getServiceData() {
    const data = {
      ...this.createEditServiceInstance.value,
      spaceGuid: this.spaceGuid || null
    };
    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 });
      this.tagsChanged.next(true);
    }

    if (input) {
      input.value = '';
    }
  }

  public removeTag(tag: any): void {
    const index = this.tags.indexOf(tag);

    if (index >= 0) {
      this.tags.splice(index, 1);
      this.tagsChanged.next(true);
    }
  }

}