Skip to content

Commit

Permalink
Merge pull request #3630 from cloudfoundry-incubator/quota-details
Browse files Browse the repository at this point in the history
org/space quota: details page
  • Loading branch information
richard-cox authored Jul 18, 2019
2 parents 2610798 + 37ef34e commit fc2cfc6
Show file tree
Hide file tree
Showing 45 changed files with 1,110 additions and 79 deletions.
2 changes: 2 additions & 0 deletions src/frontend/packages/core/sass/_all-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
@import '../src/shared/components/log-viewer/log-viewer.component.theme';
@import '../src/shared/components/chips/chips.component.theme';
@import '../src/shared/components/cards/card-number-metric/card-number-metric.component.theme';
@import '../src/shared/components/cards/card-boolean-metric/card-boolean-metric.component.theme';
@import '../src/shared/components/simple-usage-chart/simple-usage-chart.component.theme';
@import '../src/core/dot-content/dot-content.component.theme';
@import '../src/shared/components/stratos-title/stratos-title.component.theme';
Expand Down Expand Up @@ -119,6 +120,7 @@ $side-nav-light-active: #484848;
@include app-deploy-app-theme($theme, $app-theme);
@include app-cloud-foundry-firehose-theme($theme, $app-theme);
@include app-card-number-metric-theme($theme, $app-theme);
@include app-card-boolean-metric-theme($theme, $app-theme);
@include app-dot-content($theme, $app-theme);
@include stratos-title-component-theme($theme, $app-theme);
@include app-page-header-theme($theme, $app-theme);
Expand Down
17 changes: 11 additions & 6 deletions src/frontend/packages/core/src/core/cf-api.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,15 +245,20 @@ export interface IPrivateDomain {
}

export interface IQuotaDefinition {
memory_limit: number;
app_instance_limit: number;
instance_memory_limit: number;
guid?: string;
name: string;
organization_guid?: string;
total_services?: number;
total_routes?: number;
total_private_domains?: number;
app_instance_limit: number;
app_task_limit?: number;
memory_limit: number;
instance_memory_limit: number;
total_services: number;
total_service_keys?: number;
non_basic_services_allowed?: boolean;
trial_db_allowed?: boolean;
total_routes: number;
total_reserved_route_ports?: number;
total_private_domains?: number;
}

export interface IUpdateSpace {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ import { EditOrganizationStepComponent } from './edit-organization/edit-organiza
import { EditOrganizationComponent } from './edit-organization/edit-organization.component';
import { EditSpaceStepComponent } from './edit-space/edit-space-step/edit-space-step.component';
import { EditSpaceComponent } from './edit-space/edit-space.component';
import { QuotaDefinitionComponent } from './quota-definition/quota-definition.component';
import { CloudFoundryEndpointService } from './services/cloud-foundry-endpoint.service';
import { CloudFoundryOrganizationService } from './services/cloud-foundry-organization.service';
import { SpaceQuotaDefinitionComponent } from './space-quota-definition/space-quota-definition.component';
import { CfAdminAddUserWarningComponent } from './tabs/cf-admin-add-user-warning/cf-admin-add-user-warning.component';
import { CloudFoundryBuildPacksComponent } from './tabs/cloud-foundry-build-packs/cloud-foundry-build-packs.component';
import {
Expand Down Expand Up @@ -99,7 +101,6 @@ import {
import { UserInviteService } from './user-invites/user-invite.service';
import { InviteUsersCreateComponent } from './users/invite-users/invite-users-create/invite-users-create.component';
import { InviteUsersComponent } from './users/invite-users/invite-users.component';
import { RemoveUserComponent } from './users/remove-user/remove-user.component';
import { CfRolesService } from './users/manage-users/cf-roles.service';
import { UsersRolesConfirmComponent } from './users/manage-users/manage-users-confirm/manage-users-confirm.component';
import { UsersRolesModifyComponent } from './users/manage-users/manage-users-modify/manage-users-modify.component';
Expand All @@ -108,6 +109,7 @@ import {
} from './users/manage-users/manage-users-modify/space-roles-list-wrapper/space-roles-list-wrapper.component';
import { UsersRolesSelectComponent } from './users/manage-users/manage-users-select/manage-users-select.component';
import { UsersRolesComponent } from './users/manage-users/manage-users.component';
import { RemoveUserComponent } from './users/remove-user/remove-user.component';

@NgModule({
imports: [
Expand Down Expand Up @@ -167,6 +169,8 @@ import { UsersRolesComponent } from './users/manage-users/manage-users.component
CloudFoundryInviteUserLinkComponent,
CfAdminAddUserWarningComponent,
RemoveUserComponent,
QuotaDefinitionComponent,
SpaceQuotaDefinitionComponent,
],
providers: [
EndpointListHelper,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { CloudFoundryTabsBaseComponent } from './cloud-foundry-tabs-base/cloud-f
import { CloudFoundryComponent } from './cloud-foundry/cloud-foundry.component';
import { EditOrganizationComponent } from './edit-organization/edit-organization.component';
import { EditSpaceComponent } from './edit-space/edit-space.component';
import { QuotaDefinitionComponent } from './quota-definition/quota-definition.component';
import { SpaceQuotaDefinitionComponent } from './space-quota-definition/space-quota-definition.component';
import { CloudFoundryBuildPacksComponent } from './tabs/cloud-foundry-build-packs/cloud-foundry-build-packs.component';
import {
CloudFoundryCellAppsComponent,
Expand Down Expand Up @@ -73,8 +75,8 @@ import { CloudFoundryStacksComponent } from './tabs/cloud-foundry-stacks/cloud-f
import { CloudFoundrySummaryTabComponent } from './tabs/cloud-foundry-summary-tab/cloud-foundry-summary-tab.component';
import { CloudFoundryUsersComponent } from './tabs/cloud-foundry-users/cloud-foundry-users.component';
import { InviteUsersComponent } from './users/invite-users/invite-users.component';
import { RemoveUserComponent } from './users/remove-user/remove-user.component';
import { UsersRolesComponent } from './users/manage-users/manage-users.component';
import { RemoveUserComponent } from './users/remove-user/remove-user.component';


/* tslint:enable:max-line-length */
Expand Down Expand Up @@ -142,6 +144,14 @@ const cloudFoundry: Routes = [{
// Root for attaching CF wide actions (i.e assignments, tabs)
component: CloudFoundryBaseComponent,
children: [
{
path: 'quota-definitions/:quotaId',
component: QuotaDefinitionComponent
},
{
path: 'organizations/:orgId/space-quota-definitions/:quotaId',
component: SpaceQuotaDefinitionComponent
},
{
path: 'organizations/:orgId/edit-org',
component: EditOrganizationComponent
Expand Down Expand Up @@ -277,6 +287,10 @@ const cloudFoundry: Routes = [{
path: 'users',
component: CloudFoundryOrganizationUsersComponent
},
{
path: 'quota',
component: QuotaDefinitionComponent
},
{
path: '**',
component: PageNotFoundComponentComponent,
Expand Down Expand Up @@ -323,6 +337,14 @@ const cloudFoundry: Routes = [{
path: 'users',
component: CloudFoundrySpaceUsersComponent
},
{
path: 'quota',
component: QuotaDefinitionComponent
},
{
path: 'space-quota',
component: SpaceQuotaDefinitionComponent
},
{
path: '**',
component: PageNotFoundComponentComponent,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.quota-definition-base {
&__name-sub-text {
font-size: 14px;
opacity: .6;
}
&__title {
margin: 10px 0;
}
&__name {
margin: 0;
}
&__section-header {
margin: 10px 0;
opacity: .6;
}
&__basic-services {
display: flex;
margin: 8px 0;
}
&__basic-services-label {
margin-right: 10px;
opacity: .6;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { ActivatedRoute } from '@angular/router';
import { Store } from '@ngrx/store';
import { combineLatest, Observable, of, Subscription } from 'rxjs';
import { first, map } from 'rxjs/operators';

import { GetOrganization } from '../../../../../store/src/actions/organization.actions';
import { GetSpace } from '../../../../../store/src/actions/space.actions';
import { AppState } from '../../../../../store/src/app-state';
import { entityFactory, organizationSchemaKey, spaceSchemaKey } from '../../../../../store/src/helpers/entity-factory';
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 { IOrganization, IQuotaDefinition, ISpace } from '../../../core/cf-api.types';
import { EntityServiceFactory } from '../../../core/entity-service-factory.service';
import { IHeaderBreadcrumb } from '../../../shared/components/page-header/page-header.types';
import { ActiveRouteCfOrgSpace } from '../cf-page.types';

export class QuotaDefinitionBaseComponent {
breadcrumbs$: Observable<IHeaderBreadcrumb[]>;
quotaDefinition$: Observable<APIResource<IQuotaDefinition>>;
org$: Observable<APIResource<IOrganization>>;
space$: Observable<APIResource<ISpace>>;
cfGuid: string;
orgGuid: string;
spaceGuid: string;
quotaGuid: string;
detailsLoading$: Observable<boolean>;
orgSubscriber: Subscription;

constructor(
protected entityServiceFactory: EntityServiceFactory,
protected store: Store<AppState>,
protected activeRouteCfOrgSpace: ActiveRouteCfOrgSpace,
protected activatedRoute: ActivatedRoute,
) {
this.cfGuid = activeRouteCfOrgSpace.cfGuid || activatedRoute.snapshot.queryParams.cfGuid;
this.orgGuid = activeRouteCfOrgSpace.orgGuid || activatedRoute.snapshot.queryParams.orgGuid;
this.spaceGuid = activeRouteCfOrgSpace.spaceGuid || activatedRoute.snapshot.queryParams.spaceGuid;
this.quotaGuid = activatedRoute.snapshot.params.quotaId || activatedRoute.snapshot.queryParams.quotaGuid;
this.setupOrgObservable();
this.setupSpaceObservable();
this.setupBreadcrumbs();
}

setupOrgObservable() {
if (this.orgGuid) {
this.org$ = this.entityServiceFactory.create<APIResource<IOrganization>>(
organizationSchemaKey,
entityFactory(organizationSchemaKey),
this.orgGuid,
new GetOrganization(this.orgGuid, this.cfGuid),
true
).waitForEntity$.pipe(
map(data => data.entity),
);
}
}

setupSpaceObservable() {
if (this.spaceGuid) {
this.space$ = this.entityServiceFactory.create<APIResource<ISpace>>(
spaceSchemaKey,
entityFactory(spaceSchemaKey),
this.spaceGuid,
new GetSpace(this.spaceGuid, this.cfGuid),
true
).waitForEntity$.pipe(
map(data => data.entity),
);
}
}

private setupBreadcrumbs() {
const endpoints$ = this.store.select(endpointEntitiesSelector);
const org$ = this.org$ ? this.org$ : of(null);
const space$ = this.space$ ? this.space$ : of(null);
this.breadcrumbs$ = combineLatest(endpoints$, org$, space$).pipe(
map(([endpoints, org, space]) => this.getBreadcrumbs(endpoints[this.cfGuid], org, space)),
first()
);
}

protected setupQuotaDefinitionObservable() {
throw new Error('Method not implemented.');
}

protected getBreadcrumbs(
endpoint: EndpointModel,
org: APIResource<IOrganization>,
space: APIResource<ISpace>
) {
throw new Error('Method not implemented.');
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<app-page-header [breadcrumbs]="breadcrumbs$ | async" *ngIf="quotaGuid">
<h1>{{ (quotaDefinition$ | async)?.entity?.name }}</h1>
</app-page-header>

<div class="quota-definition-base quota-definition-page" *ngIf="quotaDefinition$ | async as quotaDefinition">

<app-loading-page [isLoading]="detailsLoading$" text="Retrieving details">
<div class="quota-definition-base__name-sub-text">Name</div>
<h2 class="quota-definition-base__name">{{ quotaDefinition.entity.name }}</h2>
<div class="quota-definition-base__basic-services">
<div class="quota-definition-base__basic-services-label">Non Basic Services Allowed:</div>
<app-boolean-indicator [isTrue]="quotaDefinition.entity?.non_basic_services_allowed" type="yes-no" subtle="true">
</app-boolean-indicator>
</div>
<app-tile-grid fit="true">
<h3 class="quota-definition-base__title">Quota Limits</h3>
<h4 class="quota-definition-base__section-header">Memory</h4>
<app-tile-group>
<app-tile>
<app-card-number-metric labelAtTop="true" icon="memory" label="Maximum Memory Usage" units="mb"
value="{{ quotaDefinition.entity?.memory_limit }}">
</app-card-number-metric>
</app-tile>
<app-tile>
<app-card-number-metric labelAtTop="true" icon="memory" label="Maximum Application Instance Memory Usage"
value="{{ quotaDefinition.entity?.instance_memory_limit }}">
</app-card-number-metric>
</app-tile>
<app-tile></app-tile>
</app-tile-group>

<h4 class="quota-definition-base__section-header">Application</h4>
<app-tile-group>
<app-tile>
<app-card-number-metric labelAtTop="true" icon="apps" label="Maximum Application Instances"
value="{{ quotaDefinition.entity?.app_instance_limit }}">
</app-card-number-metric>
</app-tile>
<app-tile>
<app-card-number-metric labelAtTop="true" icon="apps" label="Maximum Application Tasks"
value="{{ quotaDefinition.entity?.app_task_limit }}">
</app-card-number-metric>
</app-tile>
<app-tile></app-tile>
</app-tile-group>

<h4 class="quota-definition-base__section-header">Service</h4>
<app-tile-group>
<app-tile>
<app-card-number-metric labelAtTop="true" icon="service" iconFont="stratos-icons" label="Maximum Services"
value="{{ quotaDefinition.entity?.total_services }}">
</app-card-number-metric>
</app-tile>
<app-tile>
<app-card-number-metric labelAtTop="true" icon="service" iconFont="stratos-icons" label="Maximum Service Keys"
value="{{ quotaDefinition.entity?.total_service_keys }}">
</app-card-number-metric>
</app-tile>
<app-tile></app-tile>
</app-tile-group>

<h4 class="quota-definition-base__section-header">Routes & Domains</h4>
<app-tile-group>
<app-tile>
<app-card-number-metric labelAtTop="true" icon="network_route" iconFont="stratos-icons" label="Maximum Routes"
value="{{ quotaDefinition.entity?.total_routes }}">
</app-card-number-metric>
</app-tile>
<app-tile>
<app-card-number-metric labelAtTop="true" icon="network_route" iconFont="stratos-icons"
label="Maximum Reserved Route Ports" value="{{ quotaDefinition.entity?.total_reserved_route_ports }}">
</app-card-number-metric>
</app-tile>
<app-tile>
<app-card-number-metric labelAtTop="true" icon="domain" label="Maximum Private Domains"
value="{{ quotaDefinition.entity?.total_private_domains }}">
</app-card-number-metric>
</app-tile>
</app-tile-group>
</app-tile-grid>
</app-loading-page>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router';

import { TabNavService } from '../../../../tab-nav.service';
import {
BaseTestModules,
generateTestCfEndpointServiceProvider,
} from '../../../../test-framework/cloud-foundry-endpoint-service.helper';
import { testSCFGuid } from '../../../../test-framework/store-test-helper';
import { QuotaDefinitionComponent } from './quota-definition.component';

describe('QuotaDefinitionComponent', () => {
let component: QuotaDefinitionComponent;
let fixture: ComponentFixture<QuotaDefinitionComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [QuotaDefinitionComponent],
imports: [...BaseTestModules],
providers: [
{
provide: ActivatedRoute,
useValue: {
snapshot: {
queryParams: { cfGuid: testSCFGuid },
params: { quotaId: 'guid' }
}
}
},
generateTestCfEndpointServiceProvider(),
TabNavService
]
})
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(QuotaDefinitionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Loading

0 comments on commit fc2cfc6

Please sign in to comment.