src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/tabs/build-tab/build-tab.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { GitCommit, gitEntityCatalog, GitRepo, GitSCMService, GitSCMType, SCMIcon } from '@stratosui/git';
import { combineLatest as observableCombineLatest, Observable, of as observableOf, of } from 'rxjs';
import { combineLatest, delay, distinct, filter, first, map, mergeMap, startWith, switchMap, tap } from 'rxjs/operators';
import { AppMetadataTypes } from '../../../../../../../../cloud-foundry/src/actions/app-metadata.actions';
import { UpdateExistingApplication } from '../../../../../../../../cloud-foundry/src/actions/application.actions';
import { CFAppState } from '../../../../../../../../cloud-foundry/src/cf-app-state';
import {
CurrentUserPermissionsService,
} from '../../../../../../../../core/src/core/permissions/current-user-permissions.service';
import { ConfirmationDialogConfig } from '../../../../../../../../core/src/shared/components/confirmation-dialog.config';
import { ConfirmationDialogService } from '../../../../../../../../core/src/shared/components/confirmation-dialog.service';
import { ResetPagination } from '../../../../../../../../store/src/actions/pagination.actions';
import { getFullEndpointApiUrl } from '../../../../../../../../store/src/endpoint-utils';
import { ActionState } from '../../../../../../../../store/src/reducers/api-request-reducer/types';
import { EntityInfo } from '../../../../../../../../store/src/types/api.types';
import { IAppSummary } from '../../../../../../cf-api.types';
import { cfEntityCatalog } from '../../../../../../cf-entity-catalog';
import { CfCurrentUserPermissions } from '../../../../../../user-permissions/cf-user-permissions-checkers';
import { ApplicationMonitorService } from '../../../../application-monitor.service';
import { ApplicationData, ApplicationService } from '../../../../application.service';
import { DEPLOY_TYPES_IDS } from '../../../../deploy-application/deploy-application-steps.types';
import { EnvVarStratosProjectSource } from './application-env-vars.service';
const isDockerHubRegEx = /^([a-zA-Z0-9_-]+)\/([a-zA-Z0-9_-]+):([a-zA-Z0-9_.-]+)/g;
// Confirmation dialogs
const appStopConfirmation = new ConfirmationDialogConfig(
'Stop Application',
'Are you sure you want to stop this Application?',
'Stop'
);
const appStartConfirmation = new ConfirmationDialogConfig(
'Start Application',
'Are you sure you want to start this Application?',
'Start'
);
const appRestartConfirmation = new ConfirmationDialogConfig(
'Restart Application',
'Are you sure you want to restart this Application?',
'Restart'
);
const appRestageConfirmation = new ConfirmationDialogConfig(
'Restage Application',
'Are you sure you want to restage this Application?',
'Restage'
);
interface CustomEnvVarStratosProjectSource extends EnvVarStratosProjectSource {
label?: string;
icon?: SCMIcon;
commitURL?: string;
}
@Component({
selector: 'app-build-tab',
templateUrl: './build-tab.component.html',
styleUrls: ['./build-tab.component.scss'],
providers: [
ApplicationMonitorService,
]
})
export class BuildTabComponent implements OnInit {
public isBusyUpdating$: Observable<{ updating: boolean, }>;
public manageAppPermission = CfCurrentUserPermissions.APPLICATION_MANAGE;
constructor(
public applicationService: ApplicationService,
private scmService: GitSCMService,
private store: Store<CFAppState>,
private route: ActivatedRoute,
private router: Router,
private confirmDialog: ConfirmationDialogService,
private cups: CurrentUserPermissionsService
) { }
cardTwoFetching$: Observable<boolean>;
public async: any;
getFullApiUrl = getFullEndpointApiUrl;
sshStatus$: Observable<string>;
deploySource$: Observable<CustomEnvVarStratosProjectSource>;
public gitRepo$: Observable<GitRepo>;
ngOnInit() {
this.cardTwoFetching$ = this.applicationService.application$.pipe(
combineLatest(
this.applicationService.appSummary$
),
map(([app, appSummary]: [ApplicationData, EntityInfo<IAppSummary>]) => {
return app.fetching || appSummary.entityRequestInfo.fetching;
}), distinct());
this.isBusyUpdating$ = this.applicationService.entityService.updatingSection$.pipe(
map(updatingSection => {
const updating = this.updatingSectionBusy(updatingSection.restaging) ||
this.updatingSectionBusy(updatingSection[UpdateExistingApplication.updateKey]);
return { updating };
}),
startWith({ updating: true })
);
this.sshStatus$ = this.applicationService.application$.pipe(
combineLatest(this.applicationService.appSpace$),
map(([app, space]) => {
if (!space.entity.allow_ssh) {
return 'Disabled by the space';
} else {
return app.app.entity.enable_ssh ? 'Yes' : 'No';
}
})
);
const canSeeEnvVars$ = this.applicationService.appSpace$.pipe(
switchMap(space => this.cups.can(
CfCurrentUserPermissions.APPLICATION_VIEW_ENV_VARS,
this.applicationService.cfGuid,
space.metadata.guid)
)
);
this.gitRepo$ = this.applicationService.applicationStratProject$.pipe(
map(project => {
const scmType = project.deploySource.scm || project.deploySource.type;
const scm = this.scmService.getSCM(scmType as GitSCMType, project.deploySource.endpointGuid);
return gitEntityCatalog.repo.store.getRepoInfo.getEntityService({ projectName: project.deploySource.project, scm });
}),
switchMap(repoService => repoService.waitForEntity$),
map(p => p.entity)
);
const deploySource$ = observableCombineLatest(
this.applicationService.applicationStratProject$,
this.applicationService.application$
).pipe(
map(([project, app]) => {
if (!!project) {
const deploySource: CustomEnvVarStratosProjectSource = { ...project.deploySource };
// Legacy
if (deploySource.type === 'github') {
deploySource.type = 'gitscm';
deploySource.scm = 'github';
}
if (deploySource.type === DEPLOY_TYPES_IDS.DOCKER_IMG) {
return {
type: 'docker',
dockerImage: app.app.entity.docker_image,
dockerUrl: this.createDockerImageUrl(deploySource.dockerImage || app.app.entity.docker_image)
};
}
return deploySource;
} else if (app.app.entity.docker_image) {
return {
type: 'docker',
dockerImage: app.app.entity.docker_image,
dockerUrl: this.createDockerImageUrl(app.app.entity.docker_image)
};
} else {
return null;
}
}),
switchMap((deploySource: CustomEnvVarStratosProjectSource) => {
const res: Observable<any>[] = [
of(deploySource),
];
if (deploySource && deploySource.type === 'gitscm') {
// Add gitscm info... add async info in next section
const scmType = deploySource.scm as GitSCMType;
const scm = this.scmService.getSCM(scmType, deploySource.endpointGuid);
deploySource.label = scm.getLabel();
deploySource.icon = scm.getIcon();
res.push(gitEntityCatalog.commit.store.getEntityService(null, scm.endpointGuid, {
projectName: deploySource.project,
scm,
commitSha: deploySource.commit
}).entityObs$);
} else {
res.push(of(null));
}
return observableCombineLatest(res);
}),
map(([deploySource, commit]: [CustomEnvVarStratosProjectSource, EntityInfo<GitCommit>]) => {
if (deploySource) {
deploySource.commitURL = commit?.entity?.html_url;
}
return deploySource;
}),
startWith({ type: 'loading' })
);
this.deploySource$ = canSeeEnvVars$.pipe(
switchMap(canSeeEnvVars => canSeeEnvVars ? deploySource$ : of(null)),
);
}
private updatingSectionBusy(section: ActionState) {
return section && section.busy;
}
private createDockerImageUrl(dockerImage: string): string {
// https://docs.cloudfoundry.org/devguide/deploy-apps/push-docker.html
// Private Registry: MY-PRIVATE-REGISTRY.DOMAIN:PORT/REPO/IMAGE:TAG
// GCP: docker://MY-REGISTRY-URL/MY-PROJECT/MY-IMAGE-NAME
// DockerHub: REPO/IMAGE:TAG
isDockerHubRegEx.lastIndex = 0;
const res = isDockerHubRegEx.exec(dockerImage);
return res && res.length === 4 ? `https://hub.docker.com/r/${res[1]}/${res[2]}` : null;
}
// -----------
// App Actions
// -----------
private dispatchAppStats = () => {
const { cfGuid, appGuid } = this.applicationService;
cfEntityCatalog.appStats.api.getMultiple(appGuid, cfGuid);
};
restartApplication() {
this.confirmDialog.open(appRestartConfirmation, () => {
this.applicationService.application$.pipe(
first(),
mergeMap(appData => {
this.applicationService.updateApplication({ state: 'STOPPED' }, [], appData.app.entity);
return observableCombineLatest(
observableOf(appData),
this.pollEntityService('stopping', 'STOPPED').pipe(first())
);
}),
mergeMap(([appData, updateData]) => {
this.applicationService.updateApplication({ state: 'STARTED' }, [], appData.app.entity);
return this.pollEntityService('starting', 'STARTED').pipe(first());
}),
).subscribe({
error: this.dispatchAppStats,
complete: this.dispatchAppStats
});
});
}
private confirmAndPollForState(
confirmConfig: ConfirmationDialogConfig,
onConfirm: (appData: ApplicationData) => void,
updateKey: string,
requiredAppState: string,
onSuccess: () => void) {
this.applicationService.application$.pipe(
first(),
tap(appData => {
this.confirmDialog.open(confirmConfig, () => {
onConfirm(appData);
this.pollEntityService(updateKey, requiredAppState).pipe(
first(),
).subscribe(onSuccess);
});
})
).subscribe();
}
private updateApp(confirmConfig: ConfirmationDialogConfig, updateKey: string, requiredAppState: string, onSuccess: () => void) {
this.confirmAndPollForState(
confirmConfig,
appData => this.applicationService.updateApplication({ state: requiredAppState }, [AppMetadataTypes.STATS], appData.app.entity),
updateKey,
requiredAppState,
onSuccess
);
}
stopApplication() {
this.updateApp(appStopConfirmation, 'stopping', 'STOPPED', () => {
// On app reaching the 'STOPPED' state clear the app's stats pagination section
const { cfGuid, appGuid } = this.applicationService;
const getAppStatsAction = cfEntityCatalog.appStats.actions.getMultiple(appGuid, cfGuid);
this.store.dispatch(new ResetPagination(getAppStatsAction, getAppStatsAction.paginationKey));
});
}
restageApplication() {
const { cfGuid, appGuid } = this.applicationService;
this.confirmAndPollForState(
appRestageConfirmation,
() => cfEntityCatalog.application.api.restage(appGuid, cfGuid),
'starting',
'STARTED',
() => { }
);
}
pollEntityService(state, stateString): Observable<any> {
return this.applicationService.entityService
.poll(1000, state).pipe(
delay(1),
filter(({ resource }) => {
return resource.entity.state === stateString;
}),
);
}
startApplication() {
this.updateApp(appStartConfirmation, 'starting', 'STARTED', () => { });
}
redirectToDeletePage() {
this.router.navigate(['../delete'], { relativeTo: this.route });
}
}