diff --git a/frontend/src/app/main-table/cost-table/cost-table.component.html b/frontend/src/app/main-table/cost-table/cost-table.component.html index 6756cf4d..bf355492 100644 --- a/frontend/src/app/main-table/cost-table/cost-table.component.html +++ b/frontend/src/app/main-table/cost-table/cost-table.component.html @@ -9,7 +9,10 @@

- +
@@ -18,13 +21,23 @@ {{ "costTable.thTotal" | translate }} - + -
{{ "costTable.thCompute" | translate }} {{ "costTable.thGpus" | translate }}
{{ formatCost(cost.cpuCost + cost.ramCost) }} {{ formatCost(cost.gpuCost) }} {{ formatCost(cost.pvCost) }} + {{ formatCost(cost.totalCost) }}
+ + +
+ {{ "costTable.errMessage" | translate }} +
+
diff --git a/frontend/src/app/main-table/cost-table/cost-table.component.scss b/frontend/src/app/main-table/cost-table/cost-table.component.scss index c1337000..77c95371 100644 --- a/frontend/src/app/main-table/cost-table/cost-table.component.scss +++ b/frontend/src/app/main-table/cost-table/cost-table.component.scss @@ -3,13 +3,21 @@ p { padding-right: 16px; } +.costerror { + padding: 20px; + padding-left: 24px; + color: #cc0033; +} + .header p { padding: 12px 0 0px; font-weight: 400; font-size: 20px; } -.mat-cell, .mat-header-cell, .mat-footer-cell { +.mat-cell, +.mat-header-cell, +.mat-footer-cell { padding-left: 12px; padding-right: 195px; } diff --git a/frontend/src/app/main-table/cost-table/cost-table.component.ts b/frontend/src/app/main-table/cost-table/cost-table.component.ts index be2bc3d8..6ca37dd8 100644 --- a/frontend/src/app/main-table/cost-table/cost-table.component.ts +++ b/frontend/src/app/main-table/cost-table/cost-table.component.ts @@ -1,17 +1,36 @@ -import { Component, Input } from "@angular/core"; -import { AggregateCostResponse } from 'src/app/services/kubecost.service'; +import {Component, Input} from "@angular/core"; +import {AggregateCostResponse} from "src/app/services/kubecost.service"; + +enum AsyncStatus { + PENDING, + SUCCESS, + FAILURE +} @Component({ selector: "app-cost-table", templateUrl: "./cost-table.component.html", styleUrls: ["./cost-table.component.scss", "../main-table.component.scss"] }) -export class CostTableComponent{ - @Input() aggregatedCost: AggregateCostResponse; - @Input() currNamespace:string; +export class CostTableComponent { + @Input() aggregatedCost: AggregateCostResponse; + @Input() currNamespace: string; + + AsyncStatus = AsyncStatus; formatCost(value: number): string { - return '$' + (value > 0 ? Math.max(value, 0.01) : 0).toFixed(2) + return "$" + (value > 0 ? Math.max(value, 0.01) : 0).toFixed(2); } + getStatus(): AsyncStatus { + if (this.aggregatedCost == null) { + return AsyncStatus.PENDING; + } + + if (this.aggregatedCost instanceof Error) { + return AsyncStatus.FAILURE; + } + + return AsyncStatus.SUCCESS; + } } diff --git a/frontend/src/app/main-table/main-table.component.html b/frontend/src/app/main-table/main-table.component.html index 657387d0..4c61bbee 100644 --- a/frontend/src/app/main-table/main-table.component.html +++ b/frontend/src/app/main-table/main-table.component.html @@ -16,24 +16,22 @@ -
-
- - -
-
- - -
-
- -
-
- +
+
+ + +
+
+ +
+
+ +
+
diff --git a/frontend/src/app/main-table/main-table.component.ts b/frontend/src/app/main-table/main-table.component.ts index 7d8049bf..1deb98a9 100644 --- a/frontend/src/app/main-table/main-table.component.ts +++ b/frontend/src/app/main-table/main-table.component.ts @@ -5,11 +5,11 @@ import {KubernetesService} from "src/app/services/kubernetes.service"; import {Subscription} from "rxjs"; import {isEqual} from "lodash"; import {first} from "rxjs/operators"; -import { KubecostService, AggregateCostResponse } from 'src/app/services/kubecost.service'; +import {AggregateCostResponse, KubecostService} from "src/app/services/kubecost.service"; import {ExponentialBackoff} from "src/app/utils/polling"; import {Volume, Resource} from "../utils/types"; -import {PvcWithStatus} from "./volumes-table/volume-table.component" +import {PvcWithStatus} from "./volumes-table/volume-table.component"; @Component({ selector: "app-main-table", @@ -24,15 +24,14 @@ export class MainTableComponent implements OnInit { pvcs: Volume[] = []; pvcProperties: PvcWithStatus[] = []; - aggregatedCost: AggregateCostResponse = null; - subscriptions = new Subscription(); poller: ExponentialBackoff; + aggregatedCost: AggregateCostResponse = null; constructor( public ns: NamespaceService, private k8s: KubernetesService, - private kubecostService: KubecostService, + private kubecostService: KubecostService ) {} ngOnInit() { @@ -46,7 +45,10 @@ export class MainTableComponent implements OnInit { this.k8s.getResource(this.currNamespace).toPromise(), this.k8s.getVolumes(this.currNamespace).toPromise() ]).then(([notebooks, volumes]) => { - if (!isEqual(notebooks, this.resources) || !isEqual(volumes, this.pvcs)) { + if ( + !isEqual(notebooks, this.resources) || + !isEqual(volumes, this.pvcs) + ) { this.poller.reset(); } this.resources = notebooks; @@ -92,7 +94,8 @@ export class MainTableComponent implements OnInit { getAggregatedCost() { this.kubecostService.getAggregateCost(this.currNamespace).subscribe( - aggCost => this.aggregatedCost = aggCost - ) + aggCost => (this.aggregatedCost = aggCost), + error => (this.aggregatedCost = error) + ); } } diff --git a/frontend/src/app/main-table/volumes-table/volume-table.component.html b/frontend/src/app/main-table/volumes-table/volume-table.component.html index 3a61c012..6352bf37 100644 --- a/frontend/src/app/main-table/volumes-table/volume-table.component.html +++ b/frontend/src/app/main-table/volumes-table/volume-table.component.html @@ -3,7 +3,7 @@ storage

{{ "volumeTable.title" | translate }}

- + diff --git a/frontend/src/app/main-table/volumes-table/volume-table.component.ts b/frontend/src/app/main-table/volumes-table/volume-table.component.ts index 250857a9..2be7b88b 100644 --- a/frontend/src/app/main-table/volumes-table/volume-table.component.ts +++ b/frontend/src/app/main-table/volumes-table/volume-table.component.ts @@ -9,7 +9,7 @@ import {TranslateService} from "@ngx-translate/core"; export type PvcWithStatus = { pvc: Volume; mountedBy: string | null; -} +}; enum PvcStatus { DELETING, diff --git a/frontend/src/app/services/kubecost.service.ts b/frontend/src/app/services/kubecost.service.ts index 90d8a4bc..e0e42210 100644 --- a/frontend/src/app/services/kubecost.service.ts +++ b/frontend/src/app/services/kubecost.service.ts @@ -1,84 +1,73 @@ -import { Injectable } from "@angular/core"; -import { HttpClient, HttpErrorResponse } from "@angular/common/http"; +import {Injectable} from "@angular/core"; +import {HttpClient, HttpErrorResponse} from "@angular/common/http"; -import { Observable, throwError } from "rxjs"; -import { tap, catchError } from "rxjs/operators"; -import { environment } from "src/environments/environment"; - -import { - Resp, - SnackType -} from "../utils/types"; -import { SnackBarService } from "./snack-bar.service"; +import {Observable, throwError} from "rxjs"; +import {tap, catchError} from "rxjs/operators"; +import {environment} from "src/environments/environment"; +import {Resp} from "../utils/types"; export type AggregateCostResponse = { - code: number, + code: number; data: { [namespace: string]: { - aggregation: string, - environment: string, - cpuAllocationAverage: number, - cpuCost: number, - cpuEfficiency: number, - efficiency: number, - gpuAllocationAverage: number, - gpuCost: number, - ramAllocationAverage: number, - ramCost: number, - ramEfficiency: number, - pvAllocationAverage: number, - pvCost: number, - networkCost: number, - sharedCost: number, - totalCost: number - } - }, - message: string -} + aggregation: string; + environment: string; + cpuAllocationAverage: number; + cpuCost: number; + cpuEfficiency: number; + efficiency: number; + gpuAllocationAverage: number; + gpuCost: number; + ramAllocationAverage: number; + ramCost: number; + ramEfficiency: number; + pvAllocationAverage: number; + pvCost: number; + networkCost: number; + sharedCost: number; + totalCost: number; + }; + }; + message: string; +}; @Injectable() export class KubecostService { - - constructor(private http: HttpClient, private snackBar: SnackBarService) { } + constructor(private http: HttpClient) {} getAggregateCost(ns: string): Observable { - const url = environment.apiUrl + `/api/namespaces/${ns}/cost/aggregated` - - return this.http.get(url, { - params: { - aggregation: 'namespace', - namespace: ns, - window: '1d' - } - }).pipe( - tap(res => this.handleBackendError(res)), - catchError(err => this.handleError(err)) - ); - } + const url = environment.apiUrl + `/api/namespaces/${ns}/cost/aggregated`; + return this.http + .get(url, { + params: { + aggregation: "namespace", + namespace: ns, + window: "1d" + } + }) + .pipe( + tap(res => this.handleBackendError(res)), + catchError(err => this.handleError(err)) + ) as Observable; + } - // ---------------------------Error Handling---------------------------------- - private handleBackendError(response: { code: number }) { - if (response.code < 200 || response.code >= 300) { + private handleBackendError(response: AggregateCostResponse | Resp) { + if (this.isResp(response) || response.code < 200 || response.code >= 300) { throw response; } } - private handleError(error: HttpErrorResponse | Resp): Observable { - // The backend returned an unsuccessful response code. - // The response body may contain clues as to what went wrong, - if (error instanceof HttpErrorResponse) { - this.snackBar.show( - `${error.status}: There was an error trying to connect ` + - `to the backend API. ${error.message}`, - SnackType.Error - ); - return throwError(error.message); - } else { - // Backend error thrown from handleBackendError - const backendError = error as Resp; - this.snackBar.show(backendError.log, SnackType.Error); - return throwError(backendError.log); - } + private handleError( + error: HttpErrorResponse | AggregateCostResponse | Resp + ): Observable { + const message = this.isResp(error) ? error.log : error.message; + return throwError(new Error(message)); + } + + private isResp( + obj: HttpErrorResponse | AggregateCostResponse | Resp + ): obj is Resp { + return (obj as Resp).success !== undefined; } } diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index 1fc7803d..6068ee6f 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -5,7 +5,8 @@ "thCompute": "Compute", "thGpus": "GPUs", "thStorage": "Storage", - "thTotal": "Total" + "thTotal": "Total", + "errMessage": "Failed to retrieve cost information" }, "namespaceSelect": { "lblSelectNamespace": "Select Namespace", diff --git a/frontend/src/assets/i18n/fr.json b/frontend/src/assets/i18n/fr.json index cd587bb4..6e195ad7 100644 --- a/frontend/src/assets/i18n/fr.json +++ b/frontend/src/assets/i18n/fr.json @@ -5,7 +5,8 @@ "thCompute": "Calculé", "thGpus": "GPUs", "thStorage": "Stockage", - "thTotal": "Total" + "thTotal": "Total", + "errMessage": "Échec de la récupération des informations des coûts." }, "namespaceSelect": { "lblSelectNamespace": "Sélectionner l'espace de noms",