src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/application-tabs-base.component.ts
import { Component, NgZone, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { GitSCMService, GitSCMType } from '@stratosui/git';
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
import { filter, first, map, startWith, switchMap, withLatestFrom } from 'rxjs/operators';
import { CFAppState } from '../../../../../../cloud-foundry/src/cf-app-state';
import { applicationEntityType } from '../../../../../../cloud-foundry/src/cf-entity-types';
import { EndpointsService } from '../../../../../../core/src/core/endpoints.service';
import {
getActionsFromExtensions,
getTabsFromExtensions,
StratosActionMetadata,
StratosActionType,
StratosTabType,
} from '../../../../../../core/src/core/extension/extension-service';
import { CurrentUserPermissionsService } from '../../../../../../core/src/core/permissions/current-user-permissions.service';
import { safeUnsubscribe } from '../../../../../../core/src/core/utils.service';
import { IPageSideNavTab } from '../../../../../../core/src/features/dashboard/page-side-nav/page-side-nav.component';
import { IHeaderBreadcrumb } from '../../../../../../core/src/shared/components/page-header/page-header.types';
import { RouterNav } from '../../../../../../store/src/actions/router.actions';
import { entityCatalog } from '../../../../../../store/src/entity-catalog/entity-catalog';
import { EntitySchema } from '../../../../../../store/src/helpers/entity-schema';
import { ActionState } from '../../../../../../store/src/reducers/api-request-reducer/types';
import { endpointEntitiesSelector } from '../../../../../../store/src/selectors/endpoint.selectors';
import { APIResource } from '../../../../../../store/src/types/api.types';
import { EndpointModel } from '../../../../../../store/src/types/endpoint.types';
import { IFavoriteMetadata } from '../../../../../../store/src/types/user-favorites.types';
import { UserFavoriteManager } from '../../../../../../store/src/user-favorite-manager';
import { UpdateExistingApplication } from '../../../../actions/application.actions';
import { IApp, IOrganization, ISpace } from '../../../../cf-api.types';
import { CF_ENDPOINT_TYPE } from '../../../../cf-types';
import { ApplicationStateData } from '../../../../shared/services/application-state.service';
import { CfCurrentUserPermissions } from '../../../../user-permissions/cf-user-permissions-checkers';
import { ApplicationService } from '../../application.service';
import { ApplicationPollingService } from './application-polling.service';
@Component({
selector: 'app-application-tabs-base',
templateUrl: './application-tabs-base.component.html',
styleUrls: ['./application-tabs-base.component.scss'],
providers: [ApplicationPollingService]
})
export class ApplicationTabsBaseComponent implements OnInit, OnDestroy {
public appState$: Observable<ApplicationStateData>;
public schema: EntitySchema;
public favorite$ = this.applicationService.app$.pipe(
filter(app => !!app),
map(app => this.userFavoriteManager.getFavorite<IFavoriteMetadata>(app.entity, applicationEntityType, CF_ENDPOINT_TYPE))
);
isBusyUpdating$: Observable<{ updating: boolean; }>;
public extensionActions: StratosActionMetadata[] = getActionsFromExtensions(StratosActionType.Application);
constructor(
public applicationService: ApplicationService,
private store: Store<CFAppState>,
private endpointsService: EndpointsService,
private ngZone: NgZone,
private currentUserPermissionsService: CurrentUserPermissionsService,
scmService: GitSCMService,
private userFavoriteManager: UserFavoriteManager,
private appPollingService: ApplicationPollingService
) {
const catalogEntity = entityCatalog.getEntity(CF_ENDPOINT_TYPE, applicationEntityType);
this.schema = catalogEntity.getSchema();
const endpoints$ = store.select(endpointEntitiesSelector);
this.breadcrumbs$ = applicationService.waitForAppEntity$.pipe(
withLatestFrom(
endpoints$,
applicationService.appOrg$,
applicationService.appSpace$
),
map(([app, endpoints, org, space]) => {
return this.getBreadcrumbs(
app.entity.entity,
endpoints[app.entity.entity.cfGuid],
org,
space
);
}),
first()
);
const appDoesNotHaveEnvVars$ = this.applicationService.appSpace$.pipe(
switchMap(space => this.currentUserPermissionsService.can(CfCurrentUserPermissions.APPLICATION_VIEW_ENV_VARS,
this.applicationService.cfGuid, space.metadata.guid)
),
map(can => !can),
);
this.tabLinks = [
{ link: 'summary', label: 'Summary', iconFont: 'stratos-icons', icon: 'application' },
{ link: 'instances', label: 'Instances', iconFont: 'stratos-icons', icon: 'application_instance' },
{ link: 'routes', label: 'Routes', iconFont: 'stratos-icons', icon: 'route' },
{ link: 'log-stream', label: 'Log Stream', icon: 'featured_play_list' },
{ link: 'services', label: 'Services', iconFont: 'stratos-icons', icon: 'service' },
{ link: 'variables', label: 'Variables', icon: 'list', hidden$: appDoesNotHaveEnvVars$ },
{ link: 'events', label: 'Events', icon: 'watch_later' }
];
this.endpointsService.hasMetrics(applicationService.cfGuid).subscribe(hasMetrics => {
if (hasMetrics) {
this.tabLinks = [
...this.tabLinks,
{
link: 'metrics',
label: 'Metrics',
icon: 'equalizer'
}
];
}
});
// Add any tabs from extensions
const tabs = getTabsFromExtensions(StratosTabType.Application);
tabs.map((extensionTab) => {
this.tabLinks.push(extensionTab);
});
// Ensure Git SCM tab gets updated if the app is redeployed from a different SCM Type
this.stratosProjectSub = this.applicationService.applicationStratProject$
.subscribe(stratProject => {
if (
stratProject &&
stratProject.deploySource &&
(stratProject.deploySource.type === 'github' || stratProject.deploySource.type === 'gitscm')
) {
const gitscm = stratProject.deploySource.scm || stratProject.deploySource.type;
const scm = scmService.getSCM(gitscm as GitSCMType, stratProject.deploySource.endpointGuid);
const iconInfo = scm.getIcon();
// Add tab or update existing tab
const tab = this.tabLinks.find(t => t.link === 'gitscm');
if (!tab) {
this.tabLinks.push({ link: 'gitscm', label: scm.getLabel(), iconFont: iconInfo.fontName, icon: iconInfo.iconName });
} else {
tab.label = scm.getLabel();
tab.iconFont = iconInfo.fontName;
tab.icon = iconInfo.iconName;
}
this.tabLinks = [...this.tabLinks];
}
});
}
public breadcrumbs$: Observable<IHeaderBreadcrumb[]>;
isFetching$: Observable<boolean>;
applicationActions$: Observable<string[]>;
summaryDataChanging$: Observable<boolean>;
appSub$: Subscription;
stratosProjectSub: Subscription;
tabLinks: IPageSideNavTab[];
private getBreadcrumbs(
application: IApp,
endpoint: EndpointModel,
org: APIResource<IOrganization>,
space: APIResource<ISpace>
) {
const baseCFUrl = `/cloud-foundry/${application.cfGuid}`;
const baseOrgUrl = `${baseCFUrl}/organizations/${org.metadata.guid}`;
const baseSpaceBreadcrumbs = [
{ value: endpoint.name, routerLink: `${baseCFUrl}/organizations` },
{ value: org.entity.name, routerLink: `${baseOrgUrl}/spaces` }
];
return [
{
breadcrumbs: [{ value: 'Applications', routerLink: '/applications' }]
},
{
key: 'space',
breadcrumbs: [
...baseSpaceBreadcrumbs,
{ value: space.entity.name, routerLink: `${baseOrgUrl}/spaces/${space.metadata.guid}/apps` }
]
},
{
key: 'space-services',
breadcrumbs: [
...baseSpaceBreadcrumbs,
{ value: space.entity.name, routerLink: `${baseOrgUrl}/spaces/${space.metadata.guid}/service-instances` }
]
},
{
key: 'space-user-services',
breadcrumbs: [
...baseSpaceBreadcrumbs,
{ value: space.entity.name, routerLink: `${baseOrgUrl}/spaces/${space.metadata.guid}/user-service-instances` }
]
},
{
key: 'space-routes',
breadcrumbs: [
...baseSpaceBreadcrumbs,
{ value: space.entity.name, routerLink: `${baseOrgUrl}/spaces/${space.metadata.guid}/routes` }
]
},
{
key: 'marketplace-services',
breadcrumbs: [
{ value: 'Marketplace', routerLink: `/marketplace` }
]
},
{
key: 'service-wall',
breadcrumbs: [
{ value: 'Services', routerLink: `/services` }
]
},
{
key: 'space-summary',
breadcrumbs: [
...baseSpaceBreadcrumbs,
{ value: space.entity.name, routerLink: `${baseOrgUrl}/spaces/${space.metadata.guid}/summary` }
]
},
{
key: 'org',
breadcrumbs: [
{ value: endpoint.name, routerLink: `${baseCFUrl}/organizations` },
{ value: org.entity.name, routerLink: `${baseOrgUrl}/summary` },
]
},
{
key: 'cf',
breadcrumbs: [
{ value: endpoint.name, routerLink: `${baseCFUrl}/summary` }
]
}
];
}
private updatingSectionBusy(section: ActionState) {
return section && section.busy;
}
ngOnInit() {
this.appSub$ = this.applicationService.entityService.entityMonitor.entityRequest$.subscribe(requestInfo => {
if (
requestInfo.deleting.deleted ||
requestInfo.error
) {
this.store.dispatch(new RouterNav({ path: ['applications'] }));
}
});
this.isFetching$ = this.applicationService.isFetchingApp$;
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 })
);
const initialFetch$ = observableCombineLatest(
this.applicationService.isFetchingApp$,
this.applicationService.isFetchingEnvVars$,
this.applicationService.isFetchingStats$
).pipe(
map(([isFetchingApp, isFetchingEnvVars, isFetchingStats]) => {
return isFetchingApp || isFetchingEnvVars || isFetchingStats;
}));
this.summaryDataChanging$ = observableCombineLatest(
initialFetch$,
this.applicationService.isUpdatingApp$,
this.appPollingService.isPolling$
).pipe(map(([isFetchingApp, isUpdating, isPolling]) => {
if (isPolling) {
return false;
}
return !!(isFetchingApp || isUpdating);
}));
}
ngOnDestroy() {
safeUnsubscribe(this.appSub$, this.stratosProjectSub);
this.appPollingService.stop();
}
}