cloudfoundry/stratos

View on GitHub
src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application-step2/deploy-application-step2.component.ts

Summary

Maintainability
B
5 hrs
Test Coverage
import { HttpClient } from '@angular/common/http';
import { AfterContentInit, Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { Store } from '@ngrx/store';
import { GitBranch, GitCommit, gitEntityCatalog, GitRepo, GitSCM, GitSCMService, GitSCMType } from '@stratosui/git';
import {
  combineLatest,
  combineLatest as observableCombineLatest,
  Observable,
  of as observableOf,
  of,
  Subscription,
  timer as observableTimer,
} from 'rxjs';
import {
  catchError,
  distinctUntilChanged,
  filter,
  first,
  map,
  pairwise,
  publishReplay,
  refCount,
  startWith,
  switchMap,
  take,
  tap,
  withLatestFrom,
} from 'rxjs/operators';

import {
  ProjectDoesntExist,
  SaveAppDetails,
  SetAppSourceDetails,
  SetBranch,
  SetDeployBranch,
} from '../../../../../../cloud-foundry/src/actions/deploy-applications.actions';
import { CFAppState } from '../../../../../../cloud-foundry/src/cf-app-state';
import {
  selectDeployAppState,
  selectDeployBranchName,
  selectNewProjectCommit,
  selectPEProjectName,
  selectProjectExists,
  selectSourceType,
} from '../../../../../../cloud-foundry/src/store/selectors/deploy-application.selector';
import { StepOnNextFunction } from '../../../../../../core/src/shared/components/stepper/step/step.component';
import { getCommitGuid } from '../../../../../../git/src/store/git-entity-factory';
import { DeployApplicationState, SourceType } from '../../../../store/types/deploy-application.types';
import { ApplicationDeploySourceTypes, DEPLOY_TYPES_IDS } from '../deploy-application-steps.types';
import { GitSuggestedRepo } from './../../../../../../git/src/store/git.public-types';



@Component({
  selector: 'app-deploy-application-step2',
  templateUrl: './deploy-application-step2.component.html',
  styleUrls: ['./deploy-application-step2.component.scss']
})
export class DeployApplicationStep2Component
  implements OnInit, OnDestroy, AfterContentInit {

  @Input() isRedeploy = false;

  commitInfo: GitCommit;
  public DEPLOY_TYPES_IDS = DEPLOY_TYPES_IDS;
  sourceType$: Observable<SourceType>;
  INITIAL_SOURCE_TYPE = 0; // Fall back to GitHub, for cases where there's no type in store (refresh) or url (removed & nav)
  validate: Observable<boolean>;

  stepperText$: Observable<string>;

  // Observables for source types
  sourceTypeGithub$: Observable<boolean>;
  sourceTypeNeedsUpload$: Observable<boolean>;
  // tslint:disable-next-line:ban-types
  canDeployType$: Observable<Boolean>;
  isLoading$: Observable<boolean>;

  // Local FS data when file or folder upload
  // @Input('fsSourceData') fsSourceData;

  // ---- GIT ----------
  repositoryBranches$: Observable<GitBranch[]>;

  projectInfo$: Observable<GitRepo>;
  commitSubscription: Subscription;

  sourceType: SourceType;
  repositoryBranch: GitBranch = null;
  repository: string;

  scm: GitSCM;

  cachedSuggestions = {};

  // We don't have any repositories to suggest initially - need user to start typing
  suggestedRepos$: Observable<GitSuggestedRepo[]>;

  // Git URL
  gitUrl: string;
  gitUrlBranchName: string;
  // --------------

  // ---- Docker ----------
  dockerAppName: string;
  dockerImg: string;
  dockerUsername: string;
  // --------------

  @ViewChild('sourceSelectionForm', { static: true }) sourceSelectionForm: NgForm;
  subscriptions: Array<Subscription> = [];

  @ViewChild('fsChooser') fsChooser;

  ngOnDestroy() {
    this.subscriptions.forEach(p => p.unsubscribe());
    if (this.commitSubscription) {
      this.commitSubscription.unsubscribe();
    }
  }

  constructor(
    private store: Store<CFAppState>,
    private route: ActivatedRoute,
    private scmService: GitSCMService,
    private httpClient: HttpClient,
    private appDeploySourceTypes: ApplicationDeploySourceTypes
  ) {
  }

  onNext: StepOnNextFunction = () => {
    // Set the details based on which source type is selected
    if (this.sourceType.group === 'gitscm') {
      gitEntityCatalog.repo.store.getRepoInfo.getEntityService({
        projectName: this.repository,
        scm: this.scm,
      }).waitForEntity$.pipe(first()).subscribe(repo => {
        this.store.dispatch(new SaveAppDetails({
          projectName: this.repository,
          branch: this.repositoryBranch,
          url: repo.entity.clone_url,
          commit: this.isRedeploy ? this.commitInfo.sha : undefined,
          endpointGuid: this.sourceType.endpointGuid,
        }, null));
      });
    } else if (this.sourceType.id === DEPLOY_TYPES_IDS.GIT_URL) {
      this.store.dispatch(new SaveAppDetails({
        projectName: this.gitUrl,
        branch: {
          name: this.gitUrlBranchName,
          guid: null,
          projectName: null,
          scmType: null,
          endpointGuid: null,
        },
        endpointGuid: null
      }, null));
    } else if (this.sourceType.id === DEPLOY_TYPES_IDS.DOCKER_IMG) {
      this.store.dispatch(new SaveAppDetails(null, {
        applicationName: this.dockerAppName,
        dockerImage: this.dockerImg,
        dockerUsername: this.dockerUsername,
      }));
    }
    return observableOf({ success: true, data: this.sourceSelectionForm.form.value.fsLocalSource });
  };

  ngOnInit() {
    this.sourceType$ = combineLatest(
      this.appDeploySourceTypes.getAutoSelectedType(this.route),
      this.store.select(selectSourceType),
      this.appDeploySourceTypes.types$.pipe(first(), map(st => st[this.INITIAL_SOURCE_TYPE]))
    ).pipe(
      map(([sourceFromParam, sourceFromStore, sourceDefault]) => sourceFromParam || sourceFromStore || sourceDefault),
      filter(sourceType => !!sourceType),
    );

    this.sourceTypeGithub$ = this.sourceType$.pipe(
      filter(type => type && !!type.id),
      map(type => type.group === 'gitscm')
    );

    this.sourceTypeNeedsUpload$ = this.sourceType$.pipe(
      filter(type => type && !!type.id),
      map(type => type.id === DEPLOY_TYPES_IDS.FOLDER || type.id === DEPLOY_TYPES_IDS.FILE)
    );


    const setInitialSourceType$ = this.sourceType$.pipe(
      first(),
      tap(sourceType => {
        this.setSourceType(sourceType);
        this.sourceType = sourceType;
      })
    );

    const cfGuid$ = this.store.select(selectDeployAppState).pipe(
      filter((appDetail: DeployApplicationState) => !!appDetail.cloudFoundryDetails),
      map((appDetail: DeployApplicationState) => appDetail.cloudFoundryDetails.cloudFoundry)
    );

    this.canDeployType$ = combineLatest([
      cfGuid$,
      this.sourceType$
    ]).pipe(
      filter(([cfGuid, sourceType]) => !!cfGuid && !!sourceType),
      switchMap(([cfGuid, sourceType]) => this.appDeploySourceTypes.canDeployType(cfGuid, sourceType.id)),
      publishReplay(1),
      refCount()
    );

    this.stepperText$ = this.canDeployType$.pipe(
      switchMap(canDeployType => canDeployType ?
        this.isRedeploy ? of('Review source details') : this.sourceType$.pipe(map(st => st.helpText)) :
        of(null)
      )
    );

    this.subscriptions.push(setInitialSourceType$.subscribe());
  }

  setSourceType = (sourceType: SourceType) => {
    if (sourceType.group === 'gitscm' || sourceType.id === DEPLOY_TYPES_IDS.GIT_URL) {
      this.setupForGit();
    }

    this.store.dispatch(new SetAppSourceDetails(sourceType));
  };

  ngAfterContentInit() {
    this.validate = this.sourceSelectionForm.statusChanges.pipe(map(() => {
      return this.sourceSelectionForm.valid || this.isRedeploy;
    }));
  }

  /* Git ------------------*/
  private setupForGit() {
    this.projectInfo$ = this.store.select(selectProjectExists).pipe(
      filter(p => !!p),
      map(p => (!!p.exists && !!p.data) ? p.data : null),
      tap(p => {
        if (!!p && !this.isRedeploy) {
          this.store.dispatch(new SetDeployBranch(p.default_branch));
        }
      })
    );

    const deployBranchName$ = this.store.select(selectDeployBranchName);
    const deployCommit$ = this.store.select(selectNewProjectCommit);

    this.repositoryBranches$ = this.store
      .select(selectProjectExists)
      .pipe(
        // Wait for a new project name change
        filter(state => state && !state.checking && !state.error && state.exists),
        distinctUntilChanged((x, y) => x.name.toLowerCase() === y.name.toLowerCase()),
        // Convert project name into branches pagination observable
        switchMap(state =>
          gitEntityCatalog.branch.store.getPaginationService(null, null, {
            scm: this.scm,
            projectName: state.name
          }).entities$
        ),
        // Find the specific branch we're interested in
        withLatestFrom(deployBranchName$),
        filter(([, branchName]) => !!branchName),
        tap(([branches, branchName]) => {
          this.repositoryBranch = branches.find(
            branch => branch.name === branchName
          );
        }),
        map(([branches, branchName]) => branches),
        publishReplay(1),
        refCount()
      );

    const updateBranchAndCommit = observableCombineLatest(
      this.repositoryBranches$,
      deployBranchName$,
      this.projectInfo$,
      deployCommit$,
    ).pipe(
      tap(([branches, name, projectInfo, commit]) => {
        const branch = branches.find(b => b.name === name);
        if (branch && !!projectInfo && branch.projectName === projectInfo.full_name) {
          this.store.dispatch(new SetBranch(branch));

          if (this.isRedeploy) {
            const commitSha = commit || branch.commit.sha;
            const commitGuid = getCommitGuid(this.scm.getType(), projectInfo.full_name, commitSha);
            const commitEntityService = gitEntityCatalog.commit.store.getEntityService(commitGuid, null, {
              projectName: projectInfo.full_name,
              scm: this.scm, commitSha
            });

            if (this.commitSubscription) {
              this.commitSubscription.unsubscribe();
            }
            this.commitSubscription = commitEntityService.waitForEntity$.pipe(
              first(),
              map(p => p.entity),
              tap(p => this.commitInfo = p),
            ).subscribe();
          }
        }
      })
    );

    this.subscriptions.push(updateBranchAndCommit.subscribe());

    const setSourceTypeModel$ = this.store.select(selectSourceType).pipe(
      filter(p => !!p),
      withLatestFrom(this.appDeploySourceTypes.types$),
      tap(([p, sourceTypes]) => {
        this.sourceType = sourceTypes.find(s => s.id === p.id && (p.endpointGuid ? s.endpointGuid === p.endpointGuid : true));

        const newScm = this.scmService.getSCM(this.sourceType.id as GitSCMType, this.sourceType.endpointGuid);
        if (!!newScm) {
          // User selected one of the SCM options
          if (this.scm && newScm.getType() !== this.scm.getType()) {
            // User changed the SCM type, so reset the project and branch
            this.repository = null;
            this.commitInfo = null;
            this.repositoryBranch = null;
            this.store.dispatch(new SetBranch(null));
            this.store.dispatch(new ProjectDoesntExist(''));
            this.store.dispatch(new SaveAppDetails({ projectName: '', branch: null, endpointGuid: this.sourceType.endpointGuid }, null));
          }
          this.scm = newScm;
        }
      })
    );

    const setProjectName = this.store.select(selectPEProjectName).pipe(
      filter(p => !!p),
      take(1),
      tap(p => {
        this.repository = p;
      })
    );

    this.subscriptions.push(setSourceTypeModel$.subscribe());
    this.subscriptions.push(setProjectName.subscribe());

    this.suggestedRepos$ = this.sourceSelectionForm.valueChanges.pipe(
      map(form => form.projectName),
      startWith(''),
      pairwise(),
      filter(([oldName, newName]) => oldName !== newName),
      switchMap(([, newName]) => this.updateSuggestedRepositories(newName))
    );
  }

  updateSuggestedRepositories(name: string): Observable<GitSuggestedRepo[]> {
    if (!name || name.length < 3) {
      return observableOf([] as GitSuggestedRepo[]);
    }

    const cacheName = this.scm.getType() + ':' + name;
    if (this.cachedSuggestions[cacheName]) {
      return observableOf(this.cachedSuggestions[cacheName]);
    }

    return observableTimer(500).pipe(
      take(1),
      switchMap(() => this.scm.getMatchingRepositories(this.httpClient, name)),
      catchError(e => observableOf(null)),
      tap(suggestions => this.cachedSuggestions[cacheName] = suggestions),
    );
  }

  updateBranchName(branch: GitBranch) {
    this.store.dispatch(new SetDeployBranch(branch.name));
  }


}