Skip to content

Commit 460f113

Browse files
saffaalvibrendangadd
authored andcommitted
fix: add check for kubecost error response + translation
1 parent f938db9 commit 460f113

10 files changed

+140
-108
lines changed

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
</p>
1010

1111
<mat-divider></mat-divider>
12-
<table style="width: auto">
12+
<table
13+
*ngIf="getStatus() == AsyncStatus.SUCCESS; else error"
14+
style="width: auto"
15+
>
1316
<tr class="mat-header-row" style="text-align: left">
1417
<th class="mat-header-cell">{{ "costTable.thCompute" | translate }}</th>
1518
<th class="mat-header-cell">{{ "costTable.thGpus" | translate }}</th>
@@ -18,13 +21,23 @@
1821
{{ "costTable.thTotal" | translate }}
1922
</th>
2023
</tr>
21-
<tr *ngIf="aggregatedCost?.data[currNamespace]; let cost" class="mat-row" style="text-align: left;">
24+
<tr
25+
*ngIf="aggregatedCost.data[currNamespace]; let cost"
26+
class="mat-row"
27+
style="text-align: left"
28+
>
2229
<td class="mat-cell">{{ formatCost(cost.cpuCost + cost.ramCost) }}</td>
2330
<td class="mat-cell">{{ formatCost(cost.gpuCost) }}</td>
2431
<td class="mat-cell">{{ formatCost(cost.pvCost) }}</td>
25-
<td class="mat-cell" style="font-weight: 500;">
32+
<td class="mat-cell" style="font-weight: 500">
2633
{{ formatCost(cost.totalCost) }}
2734
</td>
2835
</tr>
2936
</table>
37+
38+
<ng-template #error>
39+
<div *ngIf="getStatus() == AsyncStatus.FAILURE" class="costerror">
40+
{{ "costTable.errMessage" | translate }}
41+
</div>
42+
</ng-template>
3043
</div>

frontend/src/app/main-table/cost-table/cost-table.component.scss

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,21 @@ p {
33
padding-right: 16px;
44
}
55

6+
.costerror {
7+
padding: 20px;
8+
padding-left: 24px;
9+
color: #cc0033;
10+
}
11+
612
.header p {
713
padding: 12px 0 0px;
814
font-weight: 400;
915
font-size: 20px;
1016
}
1117

12-
.mat-cell, .mat-header-cell, .mat-footer-cell {
18+
.mat-cell,
19+
.mat-header-cell,
20+
.mat-footer-cell {
1321
padding-left: 12px;
1422
padding-right: 195px;
1523
}
Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,36 @@
1-
import { Component, Input } from "@angular/core";
2-
import { AggregateCostResponse } from 'src/app/services/kubecost.service';
1+
import {Component, Input} from "@angular/core";
2+
import {AggregateCostResponse} from "src/app/services/kubecost.service";
3+
4+
enum AsyncStatus {
5+
PENDING,
6+
SUCCESS,
7+
FAILURE
8+
}
39

410
@Component({
511
selector: "app-cost-table",
612
templateUrl: "./cost-table.component.html",
713
styleUrls: ["./cost-table.component.scss", "../main-table.component.scss"]
814
})
9-
export class CostTableComponent{
10-
@Input() aggregatedCost: AggregateCostResponse;
11-
@Input() currNamespace:string;
15+
export class CostTableComponent {
16+
@Input() aggregatedCost: AggregateCostResponse;
17+
@Input() currNamespace: string;
18+
19+
AsyncStatus = AsyncStatus;
1220

1321
formatCost(value: number): string {
14-
return '$' + (value > 0 ? Math.max(value, 0.01) : 0).toFixed(2)
22+
return "$" + (value > 0 ? Math.max(value, 0.01) : 0).toFixed(2);
1523
}
1624

25+
getStatus(): AsyncStatus {
26+
if (this.aggregatedCost == null) {
27+
return AsyncStatus.PENDING;
28+
}
29+
30+
if (this.aggregatedCost instanceof Error) {
31+
return AsyncStatus.FAILURE;
32+
}
33+
34+
return AsyncStatus.SUCCESS;
35+
}
1736
}

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

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,22 @@
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>
28-
29-
<!-- The Table showing our Costs-->
30-
<div class="parent spacing">
31-
<div class="spacer"></div>
32-
<app-cost-table
33-
[aggregatedCost]="aggregatedCost"
34-
[currNamespace]="currNamespace"
35-
></app-cost-table>
36-
<div class="spacer"></div>
37-
</div>
38-
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>
3928

29+
<!-- The Table showing our Costs-->
30+
<div class="parent spacing">
31+
<div class="spacer"></div>
32+
<app-cost-table
33+
[aggregatedCost]="aggregatedCost"
34+
[currNamespace]="currNamespace"
35+
></app-cost-table>
36+
<div class="spacer"></div>
37+
</div>

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

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import {KubernetesService} from "src/app/services/kubernetes.service";
55
import {Subscription} from "rxjs";
66
import {isEqual} from "lodash";
77
import {first} from "rxjs/operators";
8-
import { KubecostService, AggregateCostResponse } from 'src/app/services/kubecost.service';
8+
import {AggregateCostResponse, KubecostService} from "src/app/services/kubecost.service";
99

1010
import {ExponentialBackoff} from "src/app/utils/polling";
1111
import {Volume, Resource} from "../utils/types";
12-
import {PvcWithStatus} from "./volumes-table/volume-table.component"
12+
import {PvcWithStatus} from "./volumes-table/volume-table.component";
1313

1414
@Component({
1515
selector: "app-main-table",
@@ -24,15 +24,14 @@ export class MainTableComponent implements OnInit {
2424
pvcs: Volume[] = [];
2525
pvcProperties: PvcWithStatus[] = [];
2626

27-
aggregatedCost: AggregateCostResponse = null;
28-
2927
subscriptions = new Subscription();
3028
poller: ExponentialBackoff;
29+
aggregatedCost: AggregateCostResponse = null;
3130

3231
constructor(
3332
public ns: NamespaceService,
3433
private k8s: KubernetesService,
35-
private kubecostService: KubecostService,
34+
private kubecostService: KubecostService
3635
) {}
3736

3837
ngOnInit() {
@@ -46,7 +45,10 @@ export class MainTableComponent implements OnInit {
4645
this.k8s.getResource(this.currNamespace).toPromise(),
4746
this.k8s.getVolumes(this.currNamespace).toPromise()
4847
]).then(([notebooks, volumes]) => {
49-
if (!isEqual(notebooks, this.resources) || !isEqual(volumes, this.pvcs)) {
48+
if (
49+
!isEqual(notebooks, this.resources) ||
50+
!isEqual(volumes, this.pvcs)
51+
) {
5052
this.poller.reset();
5153
}
5254
this.resources = notebooks;
@@ -92,7 +94,8 @@ export class MainTableComponent implements OnInit {
9294

9395
getAggregatedCost() {
9496
this.kubecostService.getAggregateCost(this.currNamespace).subscribe(
95-
aggCost => this.aggregatedCost = aggCost
96-
)
97+
aggCost => (this.aggregatedCost = aggCost),
98+
error => (this.aggregatedCost = error)
99+
);
97100
}
98101
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<mat-icon>storage</mat-icon>
44
<p>{{ "volumeTable.title" | translate }}</p>
55
</div>
6-
6+
<mat-divider></mat-divider>
77
<table mat-table [dataSource]="pvcProperties" matSort>
88
<!-- Status Column -->
99
<ng-container matColumnDef="status">

frontend/src/app/main-table/volumes-table/volume-table.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {TranslateService} from "@ngx-translate/core";
99
export type PvcWithStatus = {
1010
pvc: Volume;
1111
mountedBy: string | null;
12-
}
12+
};
1313

1414
enum PvcStatus {
1515
DELETING,
Lines changed: 55 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,73 @@
1-
import { Injectable } from "@angular/core";
2-
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
1+
import {Injectable} from "@angular/core";
2+
import {HttpClient, HttpErrorResponse} from "@angular/common/http";
33

4-
import { Observable, throwError } from "rxjs";
5-
import { tap, catchError } from "rxjs/operators";
6-
import { environment } from "src/environments/environment";
7-
8-
import {
9-
Resp,
10-
SnackType
11-
} from "../utils/types";
12-
import { SnackBarService } from "./snack-bar.service";
4+
import {Observable, throwError} from "rxjs";
5+
import {tap, catchError} from "rxjs/operators";
6+
import {environment} from "src/environments/environment";
7+
import {Resp} from "../utils/types";
138

149
export type AggregateCostResponse = {
15-
code: number,
10+
code: number;
1611
data: {
1712
[namespace: string]: {
18-
aggregation: string,
19-
environment: string,
20-
cpuAllocationAverage: number,
21-
cpuCost: number,
22-
cpuEfficiency: number,
23-
efficiency: number,
24-
gpuAllocationAverage: number,
25-
gpuCost: number,
26-
ramAllocationAverage: number,
27-
ramCost: number,
28-
ramEfficiency: number,
29-
pvAllocationAverage: number,
30-
pvCost: number,
31-
networkCost: number,
32-
sharedCost: number,
33-
totalCost: number
34-
}
35-
},
36-
message: string
37-
}
13+
aggregation: string;
14+
environment: string;
15+
cpuAllocationAverage: number;
16+
cpuCost: number;
17+
cpuEfficiency: number;
18+
efficiency: number;
19+
gpuAllocationAverage: number;
20+
gpuCost: number;
21+
ramAllocationAverage: number;
22+
ramCost: number;
23+
ramEfficiency: number;
24+
pvAllocationAverage: number;
25+
pvCost: number;
26+
networkCost: number;
27+
sharedCost: number;
28+
totalCost: number;
29+
};
30+
};
31+
message: string;
32+
};
3833

3934
@Injectable()
4035
export class KubecostService {
41-
42-
constructor(private http: HttpClient, private snackBar: SnackBarService) { }
36+
constructor(private http: HttpClient) {}
4337

4438
getAggregateCost(ns: string): Observable<AggregateCostResponse> {
45-
const url = environment.apiUrl + `/api/namespaces/${ns}/cost/aggregated`
46-
47-
return this.http.get<AggregateCostResponse>(url, {
48-
params: {
49-
aggregation: 'namespace',
50-
namespace: ns,
51-
window: '1d'
52-
}
53-
}).pipe(
54-
tap(res => this.handleBackendError(res)),
55-
catchError(err => this.handleError(err))
56-
);
57-
}
39+
const url = environment.apiUrl + `/api/namespaces/${ns}/cost/aggregated`;
5840

41+
return this.http
42+
.get<AggregateCostResponse | Resp>(url, {
43+
params: {
44+
aggregation: "namespace",
45+
namespace: ns,
46+
window: "1d"
47+
}
48+
})
49+
.pipe(
50+
tap(res => this.handleBackendError(res)),
51+
catchError(err => this.handleError(err))
52+
) as Observable<AggregateCostResponse>;
53+
}
5954

60-
// ---------------------------Error Handling----------------------------------
61-
private handleBackendError(response: { code: number }) {
62-
if (response.code < 200 || response.code >= 300) {
55+
private handleBackendError(response: AggregateCostResponse | Resp) {
56+
if (this.isResp(response) || response.code < 200 || response.code >= 300) {
6357
throw response;
6458
}
6559
}
6660

67-
private handleError(error: HttpErrorResponse | Resp): Observable<never> {
68-
// The backend returned an unsuccessful response code.
69-
// The response body may contain clues as to what went wrong,
70-
if (error instanceof HttpErrorResponse) {
71-
this.snackBar.show(
72-
`${error.status}: There was an error trying to connect ` +
73-
`to the backend API. ${error.message}`,
74-
SnackType.Error
75-
);
76-
return throwError(error.message);
77-
} else {
78-
// Backend error thrown from handleBackendError
79-
const backendError = error as Resp;
80-
this.snackBar.show(backendError.log, SnackType.Error);
81-
return throwError(backendError.log);
82-
}
61+
private handleError(
62+
error: HttpErrorResponse | AggregateCostResponse | Resp
63+
): Observable<never> {
64+
const message = this.isResp(error) ? error.log : error.message;
65+
return throwError(new Error(message));
66+
}
67+
68+
private isResp(
69+
obj: HttpErrorResponse | AggregateCostResponse | Resp
70+
): obj is Resp {
71+
return (obj as Resp).success !== undefined;
8372
}
8473
}

frontend/src/assets/i18n/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"thCompute": "Compute",
66
"thGpus": "GPUs",
77
"thStorage": "Storage",
8-
"thTotal": "Total"
8+
"thTotal": "Total",
9+
"errMessage": "Failed to retrieve cost information"
910
},
1011
"namespaceSelect": {
1112
"lblSelectNamespace": "Select Namespace",

frontend/src/assets/i18n/fr.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"thCompute": "Calculé",
66
"thGpus": "GPUs",
77
"thStorage": "Stockage",
8-
"thTotal": "Total"
8+
"thTotal": "Total",
9+
"errMessage": "Échec de la récupération des informations des coûts."
910
},
1011
"namespaceSelect": {
1112
"lblSelectNamespace": "Sélectionner l'espace de noms",

0 commit comments

Comments
 (0)