Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Org and Space status bar to Org/Space Cards #3265

Merged
merged 12 commits into from
Jan 10, 2019
Merged
1 change: 1 addition & 0 deletions src/frontend/app/core/cf-api.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ export interface IQuotaDefinition {
total_services?: number;
total_routes?: number;
total_private_domains?: number;
non_basic_services_allowed?: boolean;
}

export interface IUpdateSpace {
Expand Down
9 changes: 8 additions & 1 deletion src/frontend/app/core/cf.helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { APIResource } from '../store/types/api.types';
import { IApp } from './cf-api.types';
import { CfApplicationState } from '../store/types/application.types';
import { IApp } from './cf-api.types';

export function getStartedAppInstanceCount(apps: APIResource<IApp>[]): number {
if (!apps || !apps.length) {
Expand All @@ -11,3 +11,10 @@ export function getStartedAppInstanceCount(apps: APIResource<IApp>[]): number {
.map(app => app.entity.instances)
.reduce((x, sum) => x + sum, 0);
}

export function getEntityFlattenedList<T>(property: string, entities: APIResource<any>[]): T[] {
const all = entities
.map(s => s.entity[property])
.filter(s => !!s);
return [].concat.apply([], all);
}
3 changes: 3 additions & 0 deletions src/frontend/app/core/utils.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,9 @@ export const safeUnsubscribe = (...subs: Subscription[]) => {
});
};

export const truthyIncludingZero = (obj: any): boolean => !!obj || obj === 0;
export const truthyIncludingZeroString = (obj: any): string => truthyIncludingZero(obj) ? obj.toString() : null;

export const sortStringify = (obj: { [key: string]: string }): string => {
const keys = Object.keys(obj).sort();
return keys.reduce((res, key) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { combineLatest, Observable, of as observableOf } from 'rxjs';
import { filter, first, map, switchMap } from 'rxjs/operators';

import { IApp, IOrganization, ISpace } from '../../../core/cf-api.types';
import { truthyIncludingZero } from '../../../core/utils.service';
import { determineCardStatus } from '../../../shared/components/cards/card-status/card-status.component';
import { EntityMonitorFactory } from '../../../shared/monitors/entity-monitor.factory.service';
import { CardStatus } from '../../../shared/shared.types';
import { entityFactory } from '../../../store/helpers/entity-factory';
import { APIResource } from '../../../store/types/api.types';
import { CloudFoundryEndpointService } from './cloud-foundry-endpoint.service';


export abstract class OrgSpaceQuotaHelper<T = IOrganization | ISpace> {

constructor(
protected cfEndpointService: CloudFoundryEndpointService,
emf: EntityMonitorFactory,
orgOrSpaceGuid: string,
orgOrSpaceSchemaKey: string,
) {
this.orgOrSpace$ = emf.create<APIResource<T>>(
orgOrSpaceGuid,
orgOrSpaceSchemaKey,
entityFactory(orgOrSpaceSchemaKey),
false).entity$.pipe(filter(orgOrSpace => !!orgOrSpace));
}

protected orgOrSpace$: Observable<APIResource<T>>;

protected abstract quotaPropertyName: 'quota_definition' | 'space_quota_definition';
protected abstract fetchAppsFn: (orgOrSpace: APIResource<T>) => Observable<APIResource<IApp>[]>;
protected abstract getOrgOrSpaceCardStatus: (orgOrSpace: APIResource<T>, apps: APIResource<IApp>[]) => CardStatus;

public createStateObs(): Observable<CardStatus> {
return combineLatest(
this.hasQuotas(),
this.cfEndpointService.hasAllApps$
).pipe(
switchMap(([validQuotas, hasApps]) =>
// It can be expensive to iterate over apps to determine usage, so cut out early if there's no quotas or we can't determine all apps
validQuotas && hasApps ?
this.internalCreateStateObs() :
observableOf(CardStatus.NONE))
);
}

private internalCreateStateObs(): Observable<CardStatus> {
return combineLatest(
this.orgOrSpace$,
this.createAllAppsObs()
).pipe(
first(),
map(([orgOrSpace, apps]) => this.getOrgOrSpaceCardStatus(orgOrSpace, apps))
);
}

protected handleQuotaStatus(value: number, limit: number): CardStatus {
const status = determineCardStatus(value, limit);
return status === CardStatus.WARNING || status === CardStatus.ERROR ? CardStatus.WARNING : null;
}

private hasQuotas(): Observable<boolean> {
return this.orgOrSpace$.pipe(
map(resource =>
!!resource.entity[this.quotaPropertyName] && (
truthyIncludingZero(resource.entity[this.quotaPropertyName].entity.total_routes) ||
truthyIncludingZero(resource.entity[this.quotaPropertyName].entity.total_services) ||
truthyIncludingZero(resource.entity[this.quotaPropertyName].entity.total_private_domains) ||
truthyIncludingZero(resource.entity[this.quotaPropertyName].entity.app_instance_limit) ||
truthyIncludingZero(resource.entity[this.quotaPropertyName].entity.memory_limit))
)
);
}

private createAllAppsObs(): Observable<APIResource<IApp>[]> {
return this.orgOrSpace$.pipe(
switchMap(this.fetchAppsFn)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Observable } from 'rxjs';

import { IApp, IOrganization } from '../../../core/cf-api.types';
import { getEntityFlattenedList, getStartedAppInstanceCount } from '../../../core/cf.helpers';
import { EntityMonitorFactory } from '../../../shared/monitors/entity-monitor.factory.service';
import { CardStatus } from '../../../shared/shared.types';
import { organizationSchemaKey } from '../../../store/helpers/entity-factory';
import { APIResource } from '../../../store/types/api.types';
import { CloudFoundryEndpointService } from './cloud-foundry-endpoint.service';
import { OrgSpaceQuotaHelper } from './cloud-foundry-org-space-quota';

export class OrgQuotaHelper extends OrgSpaceQuotaHelper<IOrganization> {
constructor(
cfEndpointService: CloudFoundryEndpointService,
emf: EntityMonitorFactory,
orgGuid: string) {
super(
cfEndpointService,
emf,
orgGuid,
organizationSchemaKey,
);
}

protected quotaPropertyName: 'quota_definition' | 'space_quota_definition' = 'quota_definition';
protected fetchAppsFn = (orgOrSpace: APIResource<IOrganization>): Observable<APIResource<IApp>[]> =>
this.cfEndpointService.getAppsInOrgViaAllApps(orgOrSpace)
protected getOrgOrSpaceCardStatus = (org: APIResource<IOrganization>, apps: APIResource<IApp>[]): CardStatus => {
const orgQuota = org.entity.quota_definition;
// Ensure we check each on in turn
return this.handleQuotaStatus(getEntityFlattenedList('routes', org.entity.spaces).length, orgQuota.entity.total_routes) ||
this.handleQuotaStatus(getEntityFlattenedList('service_instances', org.entity.spaces).length, orgQuota.entity.total_services) ||
this.handleQuotaStatus(org.entity.private_domains.length, orgQuota.entity.total_private_domains) ||
this.handleQuotaStatus(getStartedAppInstanceCount(apps), orgQuota.entity.app_instance_limit) ||
this.handleQuotaStatus(this.cfEndpointService.getMetricFromApps(apps, 'memory'), orgQuota.entity.memory_limit) ?
CardStatus.WARNING :
CardStatus.NONE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { Injectable } from '@angular/core';
import { Route } from '@angular/router';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { filter, map, publishReplay, refCount, switchMap, startWith } from 'rxjs/operators';
import { filter, map, publishReplay, refCount, switchMap } from 'rxjs/operators';

import { IServiceInstance } from '../../../core/cf-api-svc.types';
import { IApp, IOrganization, IPrivateDomain, IQuotaDefinition, ISpace } from '../../../core/cf-api.types';
import { getStartedAppInstanceCount } from '../../../core/cf.helpers';
import { getStartedAppInstanceCount, getEntityFlattenedList } from '../../../core/cf.helpers';
import { EntityServiceFactory } from '../../../core/entity-service-factory.service';
import { CfUserService } from '../../../shared/data-services/cf-user.service';
import { PaginationMonitorFactory } from '../../../shared/monitors/pagination-monitor.factory';
Expand Down Expand Up @@ -196,12 +196,7 @@ export class CloudFoundryOrganizationService {
);
}

private getFlattenedList(property: string): (source: Observable<APIResource<ISpace>[]>) => Observable<any> {
return map(entities => {
const allInstances = entities
.map(s => s.entity[property])
.filter(s => !!s);
return [].concat.apply([], allInstances);
});
private getFlattenedList(property: string): (source: Observable<APIResource<any>[]>) => Observable<any> {
return map(entities => getEntityFlattenedList(property, entities));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Observable } from 'rxjs';

import { IApp, ISpace } from '../../../core/cf-api.types';
import { getStartedAppInstanceCount } from '../../../core/cf.helpers';
import { EntityMonitorFactory } from '../../../shared/monitors/entity-monitor.factory.service';
import { CardStatus } from '../../../shared/shared.types';
import { spaceSchemaKey } from '../../../store/helpers/entity-factory';
import { APIResource } from '../../../store/types/api.types';
import { CloudFoundryEndpointService } from './cloud-foundry-endpoint.service';
import { OrgSpaceQuotaHelper } from './cloud-foundry-org-space-quota';

export class SpaceQuotaHelper extends OrgSpaceQuotaHelper<ISpace> {
constructor(
cfEndpointService: CloudFoundryEndpointService,
emf: EntityMonitorFactory,
spaceGuid: string) {
super(
cfEndpointService,
emf,
spaceGuid,
spaceSchemaKey,
);
}

protected quotaPropertyName: 'quota_definition' | 'space_quota_definition' = 'space_quota_definition';
protected fetchAppsFn = (space: APIResource<ISpace>): Observable<APIResource<IApp>[]> =>
this.cfEndpointService.getAppsInSpaceViaAllApps(space)
protected getOrgOrSpaceCardStatus = (space: APIResource<ISpace>, apps: APIResource<IApp>[]): CardStatus => {
const spaceQuota = space.entity.space_quota_definition;
// Ensure we check each on in turn
return this.handleQuotaStatus(space.entity.routes.length, spaceQuota.entity.total_routes) ||
this.handleQuotaStatus(space.entity.service_instances.length, spaceQuota.entity.total_services) ||
this.handleQuotaStatus(getStartedAppInstanceCount(apps), spaceQuota.entity.app_instance_limit) ||
this.handleQuotaStatus(this.cfEndpointService.getMetricFromApps(apps, 'memory'), spaceQuota.entity.memory_limit) ?
CardStatus.WARNING :
CardStatus.NONE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { CardStatus } from '../../../../../../shared/components/application-state/application-state.service';
import {
CfCellHealthListConfigService,
} from '../../../../../../shared/components/list/list-types/cf-cell-health/cf-cell-health-list-config.service';
import { ListConfig } from '../../../../../../shared/components/list/list.component.types';
import { CardStatus } from '../../../../../../shared/shared.types';
import { CloudFoundryCellService } from '../cloud-foundry-cell.service';

@Component({
Expand Down
4 changes: 2 additions & 2 deletions src/frontend/app/features/service-catalog/services-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import {
} from '../../core/cf-api-svc.types';
import { EntityService } from '../../core/entity-service';
import { EntityServiceFactory } from '../../core/entity-service-factory.service';
import { CardStatus } from '../../shared/components/application-state/application-state.service';
import { safeStringToObj } from '../../core/utils.service';
import { PaginationMonitorFactory } from '../../shared/monitors/pagination-monitor.factory';
import { CardStatus } from '../../shared/shared.types';
import { GetServiceBroker } from '../../store/actions/service-broker.actions';
import { GetServiceInstances } from '../../store/actions/service-instances.actions';
import { GetService, GetServicePlansForService } from '../../store/actions/service.actions';
Expand All @@ -31,7 +32,6 @@ import { getPaginationObservables } from '../../store/reducers/pagination-reduce
import { APIResource } from '../../store/types/api.types';
import { getIdFromRoute } from '../cloud-foundry/cf.helpers';
import { ServicePlanAccessibility } from './services.service';
import { safeStringToObj } from '../../core/utils.service';

export const getSvcAvailability = (servicePlan: APIResource<IServicePlan>,
serviceBroker: APIResource<IServiceBroker>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,12 @@ import {
import { AppState } from '../../../../store/app-state';
import { selectCreateServiceInstance } from '../../../../store/selectors/create-service-instance.selectors';
import { APIResource } from '../../../../store/types/api.types';
import { CardStatus } from '../../application-state/application-state.service';
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 { CsiModeService } from '../csi-mode.service';
import { NoServicePlansComponent } from '../no-service-plans/no-service-plans.component';

import { CardStatus } from '../../../shared.types';

@Component({
selector: 'app-select-plan-step',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component, Input } from '@angular/core';

import { CardStatus } from '../application-state.service';
import { CardStatus } from '../../../shared.types';


@Component({
selector: 'app-application-state-icon',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Component, Input, OnInit } from '@angular/core';
import { CardStatus, ApplicationStateData } from './application-state.service';
import { Observable } from 'rxjs';
import { map, tap, startWith } from 'rxjs/operators';
import { map, startWith } from 'rxjs/operators';

import { CardStatus } from '../../shared.types';
import { ApplicationStateData } from './application-state.service';

@Component({
selector: 'app-application-state',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Injectable } from '@angular/core';

import { CardStatus } from '../../shared.types';



export interface ApplicationStateData {
label: string;
subLabel?: string;
Expand All @@ -9,16 +13,6 @@ export interface ApplicationStateData {
};
}

export enum CardStatus {
NONE = 'none',
OK = 'ok',
WARNING = 'warning',
TENTATIVE = 'tentative',
INCOMPLETE = 'incomplete',
ERROR = 'error',
BUSY = 'busy'
}

@Injectable()
export class ApplicationStateService {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Component, ElementRef, Input, OnDestroy, OnInit, Renderer, ViewChild } from '@angular/core';
import { MatSnackBar, MatSnackBarRef, SimpleSnackBar } from '@angular/material';
import { Observable, Subscription } from 'rxjs';
import { map, first, tap } from 'rxjs/operators';
import { first, map } from 'rxjs/operators';

import { ApplicationService } from '../../../../features/applications/application.service';
import { AppMetadataTypes } from '../../../../store/actions/app-metadata.actions';
import { CardStatus } from '../../../shared.types';
import { ConfirmationDialogConfig } from '../../confirmation-dialog.config';
import { ConfirmationDialogService } from '../../confirmation-dialog.service';
import { CardStatus } from './../../application-state/application-state.service';
import { MatSnackBar, MatSnackBarRef, SimpleSnackBar } from '@angular/material';

const appInstanceScaleToZeroConfirmation = new ConfirmationDialogConfig('Set Instance count to 0',
'Are you sure you want to set the instance count to 0?', 'Confirm', true);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ApplicationService } from '../../../../features/applications/application.service';
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { CardStatus } from '../../application-state/application-state.service';

import { ApplicationService } from '../../../../features/applications/application.service';
import { CardStatus } from '../../../shared.types';

@Component({
selector: 'app-card-app-status',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@

import { combineLatest as observableCombineLatest, Observable } from 'rxjs';

import { startWith, map, share } from 'rxjs/operators';
import { Component, OnInit } from '@angular/core';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { map, share, startWith } from 'rxjs/operators';

import { pathGet } from '../../../../core/utils.service';
import { ApplicationMonitorService } from '../../../../features/applications/application-monitor.service';
import { ApplicationService } from '../../../../features/applications/application.service';
import { CardStatus } from '../../application-state/application-state.service';
import { pathGet } from '../../../../core/utils.service';
import { CardStatus } from '../../../shared.types';

@Component({
selector: 'app-card-app-usage',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
<app-meta-card-value>{{ (cfOrgService.quotaDefinition$ | async)?.name }}
</app-meta-card-value>
</app-meta-card-item>
<app-meta-card-item>
<app-meta-card-key>Provision Paid Services</app-meta-card-key>
<app-meta-card-value>
<app-boolean-indicator [isTrue]="(cfOrgService.quotaDefinition$ | async)?.non_basic_services_allowed" type="yes-no"></app-boolean-indicator>
</app-meta-card-value>
</app-meta-card-item>
<app-meta-card-item>
<app-meta-card-key>Org Status</app-meta-card-key>
<app-meta-card-value>{{ ((cfOrgService.org$ | async)?.entity?.entity?.status) | capitalizeFirst}}</app-meta-card-value>
Expand Down
Loading