Skip to content

Commit b08cb88

Browse files
committed
feat: add cost estimate table
1 parent 2e9daf6 commit b08cb88

12 files changed

+542
-17
lines changed

frontend/src/app/app.module.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { SnackBarComponent } from "./services/snack-bar/snack-bar.component";
3131
import { ResourceFormComponent } from "./resource-form/resource-form.component";
3232
import { ConfirmDialogComponent } from "./main-table/confirm-dialog/confirm-dialog.component";
3333
import { VolumeComponent } from "./resource-form/volume/volume.component";
34+
import { CostTableComponent } from "./main-table/cost-table/cost-table.component";
3435
import { FormNameComponent } from "./resource-form/form-name/form-name.component";
3536
import { FormImageComponent } from "./resource-form/form-image/form-image.component";
3637
import { FormSpecsComponent } from "./resource-form/form-specs/form-specs.component";
@@ -49,7 +50,7 @@ import { RokErrorMsgComponent } from "./uis/rok/rok-error-msg/rok-error-msg.comp
4950
import { FormConfigurationsComponent } from "./resource-form/form-configurations/form-configurations.component";
5051
import { FormGpusComponent } from "./resource-form/form-gpus/form-gpus.component";
5152
import { VolumeTableComponent } from "./main-table/volumes-table/volume-table.component";
52-
53+
import { KubecostService } from './services/kubecost.service';
5354

5455
@NgModule({
5556
declarations: [
@@ -79,6 +80,7 @@ import { VolumeTableComponent } from "./main-table/volumes-table/volume-table.co
7980
FormConfigurationsComponent,
8081
FormGpusComponent,
8182
VolumeTableComponent,
83+
CostTableComponent
8284
],
8385
imports: [
8486
BrowserModule,
@@ -90,7 +92,7 @@ import { VolumeTableComponent } from "./main-table/volumes-table/volume-table.co
9092
FormsModule,
9193
ReactiveFormsModule
9294
],
93-
providers: [NamespaceService, KubernetesService, SnackBarService],
95+
providers: [NamespaceService, KubecostService, KubernetesService, SnackBarService],
9496
bootstrap: [AppComponent],
9597
entryComponents: [SnackBarComponent, ConfirmDialogComponent]
9698
})
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<div class="card mat-elevation-z2 mat-typography">
2+
<div class="header">
3+
<mat-icon>attach_money</mat-icon>
4+
<p>Cost</p>
5+
</div>
6+
7+
<p>
8+
Estimated cost per day of notebook server resources – summed values may not
9+
match total due to rounding
10+
</p>
11+
12+
<mat-divider></mat-divider>
13+
<table style="width: auto">
14+
<tr class="mat-header-row" style="text-align: left;">
15+
<th class="mat-header-cell">Compute</th>
16+
<th class="mat-header-cell">GPUs</th>
17+
<th class="mat-header-cell">Storage</th>
18+
<th class="mat-header-cell">Total</th>
19+
</tr>
20+
<tr *ngIf="aggregatedCost?.data[currNamespace]; let cost" class="mat-row" style="text-align: left;">
21+
<td class="mat-cell">{{ formatCost(cost.cpuCost + cost.ramCost) }}</td>
22+
<td class="mat-cell">{{ formatCost(cost.gpuCost) }}</td>
23+
<td class="mat-cell">{{ formatCost(cost.pvCost) }}</td>
24+
<td class="mat-cell" style="font-weight: 500;">{{ formatCost(cost.totalCost) }}</td>
25+
</tr>
26+
</table>
27+
</div>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
.card {
2+
width: 900px;
3+
padding: 0px;
4+
border-radius: 5px;
5+
background: white;
6+
}
7+
8+
p {
9+
padding-left: 24px;
10+
padding-right: 16px;
11+
}
12+
13+
table {
14+
width: 100%;
15+
}
16+
17+
.header {
18+
display: flex;
19+
align-items: center;
20+
padding: 0px 16px 0px 16px;
21+
height: 64px;
22+
}
23+
24+
.header p {
25+
padding: 19px 0 0px;
26+
font-weight: 400;
27+
font-size: 20px;
28+
}
29+
30+
.mat-icon {
31+
line-height: 0.85;
32+
}
33+
34+
.header > mat-icon {
35+
margin: 10px 5px 0 0;
36+
}
37+
38+
.mat-cell, .mat-header-cell, .mat-footer-cell {
39+
padding-left: 12px;
40+
padding-right: 195px;
41+
}
42+
43+
td.mat-cell:last-of-type,
44+
td.mat-footer-cell:last-of-type,
45+
th.mat-header-cell:last-of-type {
46+
padding-right: 5px;
47+
}
48+
49+
th,
50+
td {
51+
overflow: hidden;
52+
text-overflow: ellipsis;
53+
}
54+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { async, ComponentFixture, TestBed } from "@angular/core/testing";
2+
3+
import { CostTableComponent } from "./cost-table.component";
4+
5+
describe("CostTableComponent", () => {
6+
let component: CostTableComponent;
7+
let fixture: ComponentFixture<CostTableComponent>;
8+
9+
beforeEach(async(() => {
10+
TestBed.configureTestingModule({
11+
declarations: [CostTableComponent]
12+
}).compileComponents();
13+
}));
14+
15+
beforeEach(() => {
16+
fixture = TestBed.createComponent(CostTableComponent);
17+
component = fixture.componentInstance;
18+
fixture.detectChanges();
19+
});
20+
21+
it("should create", () => {
22+
expect(component).toBeTruthy();
23+
});
24+
});
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { Component, OnInit, ViewChild } from "@angular/core";
2+
import { MatSort } from "@angular/material/sort";
3+
import { MatTableDataSource } from "@angular/material/table";
4+
import { Subscription } from "rxjs";
5+
import { isEqual } from "lodash";
6+
7+
import { NamespaceService } from "src/app/services/namespace.service";
8+
import { KubernetesService } from "src/app/services/kubernetes.service";
9+
import { ExponentialBackoff } from "src/app/utils/polling";
10+
import { KubecostService, AggregateCostResponse } from 'src/app/services/kubecost.service';
11+
12+
@Component({
13+
selector: "app-cost-table",
14+
templateUrl: "./cost-table.component.html",
15+
styleUrls: ["./cost-table.component.scss"]
16+
})
17+
export class CostTableComponent implements OnInit {
18+
@ViewChild(MatSort) sort: MatSort;
19+
20+
// Logic data
21+
aggregatedCost: AggregateCostResponse = null;
22+
resources = [];
23+
currNamespace = "";
24+
25+
subscriptions = new Subscription();
26+
poller: ExponentialBackoff;
27+
28+
dataSource = new MatTableDataSource();
29+
30+
constructor(
31+
private namespaceService: NamespaceService,
32+
private k8s: KubernetesService,
33+
private kubecostService: KubecostService,
34+
) { }
35+
36+
ngOnInit() {
37+
this.dataSource.sort = this.sort;
38+
39+
// Create the exponential backoff poller
40+
this.poller = new ExponentialBackoff({ interval: 2000, retries: 3 });
41+
const resourcesSub = this.poller.start().subscribe(() => {
42+
// NOTE: We are using both the 'trackBy' feature in the Table for performance
43+
// and also detecting with lodash if the new data is different from the old
44+
// one. This is because, if the data changes we want to reset the poller
45+
if (!this.currNamespace) {
46+
return;
47+
}
48+
49+
this.k8s.getResource(this.currNamespace).subscribe(resources => {
50+
if (!isEqual(this.resources, resources)) {
51+
this.resources = resources;
52+
this.dataSource.data = this.resources;
53+
this.poller.reset();
54+
}
55+
});
56+
});
57+
58+
// Keep track of the selected namespace
59+
const namespaceSub = this.namespaceService
60+
.getSelectedNamespace()
61+
.subscribe(this.onNamespaceChange.bind(this));
62+
63+
this.subscriptions.add(resourcesSub);
64+
this.subscriptions.add(namespaceSub);
65+
}
66+
67+
ngOnDestroy() {
68+
this.subscriptions.unsubscribe();
69+
}
70+
71+
onNamespaceChange(namespace: string) {
72+
this.currNamespace = namespace;
73+
this.dataSource.data = [];
74+
this.resources = [];
75+
this.poller.reset();
76+
this.getAggregatedCost();
77+
}
78+
79+
formatCost(value: number): string {
80+
return '$' + (value > 0 ? Math.max(value, 0.01) : 0).toFixed(2)
81+
}
82+
83+
getAggregatedCost() {
84+
this.kubecostService.getAggregateCost(this.currNamespace).subscribe(
85+
aggCost => this.aggregatedCost = aggCost
86+
)
87+
}
88+
}

frontend/src/app/main-table/main-table.component.html

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,21 @@
1616
</div>
1717

1818
<!-- The Table showing the persistent volume claims -->
19-
<div class="parent spacing">
20-
<div class="spacer"></div>
21-
<app-volume-table
22-
[pvcProperties]="pvcProperties"
23-
(deletePvcEvent)="deletePvc($event)"
24-
>
25-
</app-volume-table>
26-
<div class="spacer"></div>
27-
</div>
19+
<div class="parent spacing">
20+
<div class="spacer"></div>
21+
<app-volume-table
22+
[pvcProperties]="pvcProperties"
23+
(deletePvcEvent)="deletePvc($event)"
24+
>
25+
</app-volume-table>
26+
<div class="spacer"></div>
27+
</div>
28+
29+
<!-- The Table showing our Costs-->
30+
<div class="parent spacing">
31+
<div class="spacer"></div>
32+
<app-cost-table></app-cost-table>
33+
<div class="spacer"></div>
34+
</div>
35+
2836

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
.card {
2+
width: 900px;
3+
padding: 0px;
4+
border-radius: 5px;
5+
background: white;
6+
}
7+
8+
p {
9+
padding-left: 16px;
10+
padding-right: 16px;
11+
}
12+
13+
table {
14+
width: 100%;
15+
}
16+
17+
.header {
18+
display: flex;
19+
align-items: center;
20+
padding: 0px 16px 0px 16px;
21+
height: 64px;
22+
}
23+
24+
.header p {
25+
padding: 14px 0 6px;
26+
font-weight: 400;
27+
font-size: 20px;
28+
}
29+
30+
.cdk-column-actions {
31+
text-align: center;
32+
}
33+
34+
.mat-icon {
35+
line-height: 0.85;
36+
}
37+
38+
.header > mat-icon {
39+
margin: 10px 10px 0 0;
40+
}
41+
42+
.mat-cell, .mat-header-cell, .mat-footer-cell {
43+
padding-left: 12px;
44+
padding-right: 12px;
45+
}
46+
47+
td.mat-cell:last-of-type,
48+
td.mat-footer-cell:last-of-type,
49+
th.mat-header-cell:last-of-type {
50+
padding-right: 0px;
51+
}
52+
53+
.inline {
54+
display: inline-block;
55+
}
56+
57+
// Status Icons
58+
.running {
59+
color: green;
60+
}
61+
62+
.warning {
63+
color: orange;
64+
}
65+
66+
.error {
67+
color: red;
68+
}
69+
70+
.status {
71+
display: inline-flex;
72+
vertical-align: middle;
73+
}
74+
75+
.delete {
76+
color: red;
77+
}
78+
79+
// Flex
80+
.parent {
81+
display: flex;
82+
}
83+
84+
.spacer {
85+
flex-grow: 1;
86+
}
87+
88+
th,
89+
td {
90+
overflow: hidden;
91+
text-overflow: ellipsis;
92+
}
93+
94+
td.mat-column-image,
95+
td.mat-column-name {
96+
max-width: 200px;
97+
}
98+
99+
td.mat-column-cpu {
100+
width: 40px;
101+
}

0 commit comments

Comments
 (0)