Skip to content

Commit b676b2f

Browse files
committed
Show future availabilities for each flavour in a new admin UI.
1 parent ada3800 commit b676b2f

File tree

14 files changed

+560
-4
lines changed

14 files changed

+560
-4
lines changed

src/app/admin/admin.module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ import {
8383
ExtensionRequestHandlerComponent,
8484
HypervisorsComponent,
8585
HypervisorComponent,
86+
AvailabilitiesComponent,
87+
AvailabilityComponent,
8688
} from './components';
8789
import {ClrSpinnerModule} from "@clr/angular";
8890

@@ -165,6 +167,8 @@ import {ClrSpinnerModule} from "@clr/angular";
165167
ExtensionRequestHandlerComponent,
166168
HypervisorsComponent,
167169
HypervisorComponent,
170+
AvailabilitiesComponent,
171+
AvailabilityComponent,
168172
CloudDevicePipe,
169173
CloudDeviceAllocationPipe,
170174
InstanceDeviceAllocationPipe,

src/app/admin/admin.routing.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ import {
1515
SessionsComponent,
1616
UserComponent,
1717
ExtensionRequestsComponent,
18+
HypervisorsComponent,
19+
AvailabilitiesComponent,
1820
instanceActivate,
19-
userActivate, HypervisorsComponent,
21+
userActivate,
2022
} from './components';
2123

2224
export const ROUTES: Routes = [
@@ -38,6 +40,9 @@ export const ROUTES: Routes = [
3840
{
3941
path: 'compute/sessions', component: SessionsComponent,
4042
},
43+
{
44+
path: 'compute/availabilities', component: AvailabilitiesComponent,
45+
},
4146
{
4247
path: 'compute/extension_requests', component: ExtensionRequestsComponent,
4348
},

src/app/admin/common/components/compute-header/compute-header.component.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
<clr-icon shape="wrench" class="is-solid"></clr-icon>
1212
Sessions
1313
</a>
14+
<a class="admin-tab" routerLink="/admin/compute/availabilities" routerLinkActive="admin-tab--selected">
15+
<clr-icon shape="calendar" class="is-solid"></clr-icon>
16+
Availabilities
17+
</a>
1418
<a class="admin-tab" routerLink="/admin/compute/extension_requests" routerLinkActive="admin-tab--selected">
1519
<clr-icon shape="clock" class="is-solid"></clr-icon>
1620
Extension Requests
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<visa-admin-header></visa-admin-header>
2+
<visa-compute-admin-header></visa-compute-admin-header>
3+
4+
5+
<div class="availabilities-spinner-outer" [hidden]="!loading">
6+
<div class="availabilities-spinner">
7+
<clr-spinner></clr-spinner>
8+
</div>
9+
</div>
10+
11+
<div *ngIf="!loading" class="availabilities-overview">
12+
<div class="admin-container flavours-main">
13+
<div class="admin-container--main">
14+
15+
<clr-select-container *ngIf="multiCloudEnabled" style="margin-bottom: 16px; margin-top: 8px;">
16+
<select clrSelect id="cloud" [(ngModel)]="selectedCloudClient">
17+
<option *ngFor="let cloudClient of cloudClients" [ngValue]="cloudClient">{{cloudClient? cloudClient.name + ' cloud provider' : ''}}</option>
18+
</select>
19+
<clr-control-helper>Please select the cloud provider</clr-control-helper>
20+
</clr-select-container>
21+
22+
<div class="availabilities-header">
23+
<h3 class="filters-box-title">Flavour Availabilities</h3>
24+
<button type="button" class="btn btn-outline" [disabled]="resetDisabled" (click)="resetCharts()">Reset charts</button>
25+
</div>
26+
27+
<div class="availabilities-content">
28+
<visa-admin-availability *ngFor="let availability of availabilities"
29+
[availability]="availability"
30+
[axisData]="axisData$"
31+
[reset]="resetCharts$">
32+
</visa-admin-availability>
33+
</div>
34+
</div>
35+
</div>
36+
</div>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
.availabilities-overview {
2+
}
3+
4+
.availabilities-main {
5+
margin: 0;
6+
}
7+
8+
.availabilities-header {
9+
display: flex;
10+
flex-direction: row;
11+
justify-content: space-between;
12+
align-items: center;
13+
}
14+
15+
.availabilities-spinner-outer {
16+
position: relative;
17+
width: 100%;
18+
}
19+
20+
.availabilities-spinner {
21+
height: 150px;
22+
top: 16px;
23+
position: absolute;
24+
display: flex;
25+
justify-content: center;
26+
width: 100%;
27+
z-index: 999999;
28+
}
29+
30+
.availabilities-content {
31+
display: flex;
32+
flex-direction: column;
33+
justify-content: flex-start;
34+
align-items: stretch;
35+
row-gap: 16px;
36+
37+
&__button {
38+
display: flex;
39+
flex-direction: row;
40+
justify-content: flex-end;
41+
align-items: center;
42+
}
43+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import {Component, OnDestroy, OnInit} from '@angular/core';
2+
import {BehaviorSubject, min, Subject} from 'rxjs';
3+
import gql from 'graphql-tag';
4+
import {Apollo} from 'apollo-angular';
5+
import {delay, filter, map, startWith, switchMap, takeUntil, tap} from 'rxjs/operators';
6+
import {Title} from '@angular/platform-browser';
7+
import {Hypervisor, DevicePool, Flavour, CloudClient, FlavourAvailabilitiesFuture} from '../../../core/graphql';
8+
9+
@Component({
10+
selector: 'visa-admin-availabilities',
11+
templateUrl: './availabilities.component.html',
12+
styleUrls: ['./availabilities.component.scss'],
13+
})
14+
export class AvailabilitiesComponent implements OnInit, OnDestroy {
15+
16+
private _destroy$: Subject<boolean> = new Subject<boolean>();
17+
private _refresh$: Subject<void> = new Subject();
18+
private _resetCharts$: Subject<void> = new Subject();
19+
private _resetDisabled = true;
20+
private _allAvailabilities: FlavourAvailabilitiesFuture[] = [];
21+
private _availabilities: FlavourAvailabilitiesFuture[] = [];
22+
private _cloudClients: CloudClient[] = [];
23+
private _loading: boolean;
24+
private _multiCloudEnabled = false;
25+
private _selectedCloudClient$: BehaviorSubject<CloudClient> = new BehaviorSubject<CloudClient>(null);
26+
private _axisData$: Subject<{min: number, max: number}> = new Subject<{min: number; max: number}>();
27+
28+
constructor(private readonly _apollo: Apollo,
29+
private readonly _titleService: Title) {
30+
}
31+
32+
get loading(): boolean {
33+
return this._loading;
34+
}
35+
36+
get availabilities(): FlavourAvailabilitiesFuture[] {
37+
return this._availabilities;
38+
}
39+
40+
get cloudClients(): CloudClient[] {
41+
return this._cloudClients;
42+
}
43+
44+
get multiCloudEnabled(): boolean {
45+
return this._multiCloudEnabled;
46+
}
47+
48+
get selectedCloudClient(): CloudClient {
49+
return this._selectedCloudClient$.value;
50+
}
51+
52+
set selectedCloudClient(value: CloudClient) {
53+
this._selectedCloudClient$.next(value);
54+
}
55+
56+
get axisData$(): Subject<{ min: number; max: number }> {
57+
return this._axisData$;
58+
}
59+
60+
get resetCharts$(): Subject<void> {
61+
return this._resetCharts$;
62+
}
63+
64+
get resetDisabled(): boolean {
65+
return this._resetDisabled;
66+
}
67+
68+
public ngOnInit(): void {
69+
this._titleService.setTitle(`Availabilities | Cloud | Admin | VISA`);
70+
this._refresh$
71+
.pipe(
72+
startWith(0),
73+
takeUntil(this._destroy$),
74+
tap(() => this._loading = true),
75+
delay(250),
76+
switchMap(() => this._apollo.query<any>({
77+
query: gql`
78+
query flavourAvailabilitiesFutures {
79+
flavourAvailabilitiesFutures {
80+
flavour {
81+
id
82+
name
83+
memory
84+
cpu
85+
cloudId
86+
devices {
87+
devicePool {
88+
id
89+
name
90+
description
91+
resourceClass
92+
}
93+
unitCount
94+
}
95+
}
96+
confidence
97+
availabilities {
98+
date
99+
confidence
100+
units
101+
}
102+
}
103+
cloudClients {
104+
id
105+
name
106+
}
107+
}
108+
`
109+
})),
110+
map(({data}) => data),
111+
tap(() => this._loading = false)
112+
)
113+
.subscribe(({flavourAvailabilitiesFutures, cloudClients}) => {
114+
this._allAvailabilities = flavourAvailabilitiesFutures;
115+
this._cloudClients = cloudClients;
116+
this.selectedCloudClient = this._cloudClients[0];
117+
118+
this._multiCloudEnabled = cloudClients.length > 1 || flavourAvailabilitiesFutures
119+
.map(flavourAvailabilitiesFuture => flavourAvailabilitiesFuture.flavour)
120+
.map(flavour => flavour.cloudId || 0)
121+
.filter((value, index, array) => array.indexOf(value) === index)
122+
.length > 1;
123+
});
124+
125+
this._selectedCloudClient$.pipe(
126+
takeUntil(this._destroy$),
127+
filter(value => !!value),
128+
).subscribe(cloudClient => {
129+
const cloudId = cloudClient.id == null ? -1 : cloudClient.id;
130+
this._availabilities = this._allAvailabilities
131+
.filter(flavourAvailabilitiesFuture => {
132+
const flavour = flavourAvailabilitiesFuture.flavour;
133+
const flavourCloudId = flavour.cloudId == null ? -1 : flavour.cloudId;
134+
return flavourCloudId == cloudId;
135+
})
136+
});
137+
138+
this._axisData$.subscribe(() => {
139+
this._resetDisabled = false;
140+
})
141+
}
142+
143+
public ngOnDestroy(): void {
144+
this._destroy$.next(true);
145+
this._destroy$.unsubscribe();
146+
}
147+
148+
public onRefresh(): void {
149+
this._refresh$.next();
150+
}
151+
152+
public resetCharts(): void {
153+
this._resetDisabled = true;
154+
this._resetCharts$.next();
155+
}
156+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import {Flavour, FlavourAvailability} from "../../../core/graphql";
2+
import {BehaviorSubject} from "rxjs";
3+
4+
export class AvailabilityChartData {
5+
6+
7+
private _options: any = {
8+
chart: {
9+
type: 'area',
10+
zoomType: 'x',
11+
panning: true,
12+
panKey: 'shift',
13+
events: {
14+
selection: (event) => {
15+
if (event.xAxis) {
16+
const min = event.xAxis[0].min;
17+
const max = event.xAxis[0].max;
18+
this._axisData$.next({ min, max });
19+
}
20+
return true; // allow zooming
21+
},
22+
23+
},
24+
},
25+
title: {
26+
text: '',
27+
},
28+
xAxis: {
29+
type: 'datetime',
30+
dateTimeLabelFormats: {
31+
day: '%e %b %Y',
32+
week: '%e %b %Y',
33+
month: '%b %Y',
34+
year: '%Y'
35+
},
36+
tickPixelInterval: 150,
37+
labels: {
38+
style: {
39+
fontSize: 12,
40+
color: '#606060'
41+
}
42+
},
43+
lineColor: '#a0a0a0',
44+
tickColor: '#a0a0a0',
45+
events: {
46+
setExtremes: (event) => {
47+
if (event.trigger === 'pan') {
48+
const {min, max} = event;
49+
this._axisData$.next({ min, max });
50+
}
51+
}
52+
}
53+
},
54+
yAxis: {
55+
title: '',
56+
labels: {
57+
style: {
58+
fontSize: 12,
59+
color: '#606060'
60+
}
61+
},
62+
lineColor: '#a0a0a0',
63+
tickColor: '#a0a0a0',
64+
},
65+
tooltip: {
66+
// enabled: false,
67+
},
68+
plotOptions: {
69+
series: {
70+
step: true,
71+
color: '#00a65a',
72+
fillColor: '#edfbf1',
73+
},
74+
},
75+
legend: {
76+
enabled: false,
77+
},
78+
credits: {
79+
enabled: false,
80+
},
81+
series: [],
82+
}
83+
84+
get options(): any {
85+
return this._options;
86+
}
87+
88+
constructor(flavour: Flavour, availabilities: FlavourAvailability[], private _axisData$: BehaviorSubject<{min: number, max: number}>) {
89+
const data = availabilities.map(availability => {
90+
return { x: Date.parse(availability.date), y: availability.units };
91+
});
92+
93+
const xMin = Math.min(...data.map(p => p.x));
94+
const xMax = Math.max(...data.map(p => p.x)) + 24 * 60 * 60 * 1000;
95+
const yMax = Math.max(...data.map(p => p.y)) * 1.1;
96+
this._options.xAxis = {...this._options.xAxis, min: xMin, max: xMax};
97+
this._options.yAxis = {...this._options.yAxis, min: 0, max: yMax};
98+
99+
data.push({x: Date.parse('2050-01-01T00:00:00.000'), y: availabilities[availabilities.length - 1].units})
100+
this._options.series = [
101+
{name: flavour.name, data: data },
102+
];
103+
104+
}
105+
106+
}

0 commit comments

Comments
 (0)