From 793d8c8be553176f34b4f59437aeaa1f5948b4ca Mon Sep 17 00:00:00 2001 From: d-gol Date: Wed, 19 Jan 2022 13:16:01 +0100 Subject: [PATCH] Distinct page for each Trial in the UI --- cmd/new-ui/v1beta1/main.go | 1 + pkg/new-ui/v1beta1/backend.go | 24 ++ .../frontend/src/app/app-routing.module.ts | 5 + .../v1beta1/frontend/src/app/app.module.ts | 2 + .../src/app/models/trial.k8s.model.ts | 84 +++++++ .../experiment-details.component.html | 8 +- .../experiment-details.component.ts | 18 ++ .../overview/metrics/metrics.component.html | 11 + .../metrics/metrics.component.module.ts | 10 + .../metrics/metrics.component.spec.ts | 24 ++ .../overview/metrics/metrics.component.ts | 12 + .../trial-modal-overview.component.html | 42 ++++ .../trial-modal-overview.component.spec.ts | 24 ++ .../trial-modal-overview.component.ts | 114 ++++++++++ .../overview/trial-modal-overview.module.ts | 22 ++ .../trial-modal/trial-modal.component.html | 81 ++++--- .../trial-modal/trial-modal.component.scss | 26 +-- .../trial-modal/trial-modal.component.ts | 206 ++++++++++++++---- .../trial-modal/trial-modal.module.ts | 37 ++++ .../trials-table/trials-table.component.ts | 8 +- .../trials-table/trials-table.module.ts | 3 +- .../src/app/services/backend.service.ts | 6 + 22 files changed, 679 insertions(+), 89 deletions(-) create mode 100644 pkg/new-ui/v1beta1/frontend/src/app/models/trial.k8s.model.ts create mode 100644 pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/metrics/metrics.component.html create mode 100644 pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/metrics/metrics.component.module.ts create mode 100644 pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/metrics/metrics.component.spec.ts create mode 100644 pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/metrics/metrics.component.ts create mode 100644 pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/trial-modal-overview.component.html create mode 100644 pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/trial-modal-overview.component.spec.ts create mode 100644 pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/trial-modal-overview.component.ts create mode 100644 pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/trial-modal-overview.module.ts create mode 100644 pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/trial-modal.module.ts diff --git a/cmd/new-ui/v1beta1/main.go b/cmd/new-ui/v1beta1/main.go index 1566dfca864..68d2f1dcd2b 100644 --- a/cmd/new-ui/v1beta1/main.go +++ b/cmd/new-ui/v1beta1/main.go @@ -55,6 +55,7 @@ func main() { http.HandleFunc("/katib/delete_experiment/", kuh.DeleteExperiment) http.HandleFunc("/katib/fetch_experiment/", kuh.FetchExperiment) + http.HandleFunc("/katib/fetch_trial/", kuh.FetchTrial) http.HandleFunc("/katib/fetch_suggestion/", kuh.FetchSuggestion) http.HandleFunc("/katib/fetch_hp_job_info/", kuh.FetchHPJobInfo) diff --git a/pkg/new-ui/v1beta1/backend.go b/pkg/new-ui/v1beta1/backend.go index d0150697aa8..eb6583550ba 100644 --- a/pkg/new-ui/v1beta1/backend.go +++ b/pkg/new-ui/v1beta1/backend.go @@ -397,3 +397,27 @@ func (k *KatibUIHandler) FetchSuggestion(w http.ResponseWriter, r *http.Request) return } } + +// FetchTrial gets trial in specific namespace. +func (k *KatibUIHandler) FetchTrial(w http.ResponseWriter, r *http.Request) { + trialName := r.URL.Query()["trialName"][0] + namespace := r.URL.Query()["namespace"][0] + + trial, err := k.katibClient.GetTrial(trialName, namespace) + if err != nil { + log.Printf("GetTrial failed: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + response, err := json.Marshal(trial) + if err != nil { + log.Printf("Marshal Trial failed: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if _, err = w.Write(response); err != nil { + log.Printf("Write trial failed: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/pkg/new-ui/v1beta1/frontend/src/app/app-routing.module.ts b/pkg/new-ui/v1beta1/frontend/src/app/app-routing.module.ts index 8087c1ac477..6842ecb05f0 100644 --- a/pkg/new-ui/v1beta1/frontend/src/app/app-routing.module.ts +++ b/pkg/new-ui/v1beta1/frontend/src/app/app-routing.module.ts @@ -3,11 +3,16 @@ import { Routes, RouterModule } from '@angular/router'; import { ExperimentsComponent } from './pages/experiments/experiments.component'; import { ExperimentDetailsComponent } from './pages/experiment-details/experiment-details.component'; import { ExperimentCreationComponent } from './pages/experiment-creation/experiment-creation.component'; +import { TrialModalComponent } from './pages/experiment-details/trials-table/trial-modal/trial-modal.component'; const routes: Routes = [ { path: '', component: ExperimentsComponent }, { path: 'experiment/:experimentName', component: ExperimentDetailsComponent }, { path: 'new', component: ExperimentCreationComponent }, + { + path: 'experiment/:experimentName/trial/:trialName', + component: TrialModalComponent, + }, ]; @NgModule({ diff --git a/pkg/new-ui/v1beta1/frontend/src/app/app.module.ts b/pkg/new-ui/v1beta1/frontend/src/app/app.module.ts index 98af0db9cbd..844f5ca524d 100644 --- a/pkg/new-ui/v1beta1/frontend/src/app/app.module.ts +++ b/pkg/new-ui/v1beta1/frontend/src/app/app.module.ts @@ -8,6 +8,7 @@ import { AppComponent } from './app.component'; import { ExperimentsModule } from './pages/experiments/experiments.module'; import { ExperimentDetailsModule } from './pages/experiment-details/experiment-details.module'; import { ExperimentCreationModule } from './pages/experiment-creation/experiment-creation.module'; +import { TrialModalModule } from './pages/experiment-details/trials-table/trial-modal/trial-modal.module'; @NgModule({ declarations: [AppComponent], @@ -19,6 +20,7 @@ import { ExperimentCreationModule } from './pages/experiment-creation/experiment ExperimentDetailsModule, ReactiveFormsModule, ExperimentCreationModule, + TrialModalModule, ], providers: [], bootstrap: [AppComponent], diff --git a/pkg/new-ui/v1beta1/frontend/src/app/models/trial.k8s.model.ts b/pkg/new-ui/v1beta1/frontend/src/app/models/trial.k8s.model.ts new file mode 100644 index 00000000000..9bdf59ae753 --- /dev/null +++ b/pkg/new-ui/v1beta1/frontend/src/app/models/trial.k8s.model.ts @@ -0,0 +1,84 @@ +import { K8sObject } from 'kubeflow'; +import { V1Container } from '@kubernetes/client-node'; + +/* + * K8s object definitions + */ +export const TRIAL_KIND = 'Trial'; +export const TRIAL_APIVERSION = 'kubeflow.org/v1beta1'; + +export interface TrialK8s extends K8sObject { + spec?: TrialSpec; + status?: TrialStatus; +} + +export interface TrialSpec { + metricsCollector: MetricsCollector; + objective: Objective; + parameterAssignments: { name: string; value: number }[]; + primaryContainerName: string; + successCondition: string; + failureCondition: string; + runSpec: K8sObject; +} + +export interface MetricsCollector { + collector?: CollectorSpec; +} + +export interface CollectorSpec { + kind: CollectorKind; + customCollector: V1Container; +} + +export type CollectorKind = + | 'StdOut' + | 'File' + | 'TensorFlowEvent' + | 'PrometheusMetric' + | 'Custom' + | 'None'; + +export interface Objective { + type: ObjectiveType; + goal: number; + objectiveMetricName: string; + additionalMetricNames: string[]; + metricStrategies: MetricStrategy[]; +} + +export type ObjectiveType = 'maximize' | 'minimize'; + +export interface MetricStrategy { + name: string; + value: string; +} + +export interface RunSpec {} + +/* + * status + */ + +interface TrialStatus { + startTime: string; + completionTime: string; + conditions: TrialStatusCondition[]; + observation: { + metrics: { + name: string; + latest: string; + min: string; + max: string; + }[]; + }; +} + +interface TrialStatusCondition { + type: string; + status: boolean; + reason: string; + message: string; + lastUpdateTime: string; + lastTransitionTime: string; +} diff --git a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/experiment-details.component.html b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/experiment-details.component.html index 4d1233302cf..a310a71c079 100644 --- a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/experiment-details.component.html +++ b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/experiment-details.component.html @@ -33,7 +33,12 @@
- + ([ + ['overview', 0], + ['trials', 1], + ['details', 2], + ['yaml', 3], + ]); constructor( private activatedRoute: ActivatedRoute, @@ -62,6 +70,12 @@ export class ExperimentDetailsComponent implements OnInit, OnDestroy { ngOnInit() { this.name = this.activatedRoute.snapshot.params.experimentName; + if (this.activatedRoute.snapshot.queryParams['tab']) { + this.selectedTab = this.tabs.get( + this.activatedRoute.snapshot.queryParams['tab'], + ); + } + this.subs.add( this.namespaceService.getSelectedNamespace().subscribe(namespace => { this.namespace = namespace; @@ -70,6 +84,10 @@ export class ExperimentDetailsComponent implements OnInit, OnDestroy { ); } + tabChanged(event: MatTabChangeEvent) { + this.selectedTab = event.index; + } + ngOnDestroy(): void { this.subs.unsubscribe(); } diff --git a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/metrics/metrics.component.html b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/metrics/metrics.component.html new file mode 100644 index 00000000000..76ecf28135e --- /dev/null +++ b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/metrics/metrics.component.html @@ -0,0 +1,11 @@ + + {{ latest }} + + + + {{ min }} + + + + {{ max }} + diff --git a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/metrics/metrics.component.module.ts b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/metrics/metrics.component.module.ts new file mode 100644 index 00000000000..4c3ac97cac7 --- /dev/null +++ b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/metrics/metrics.component.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { ConditionsTableModule, DetailsListModule } from 'kubeflow'; +import { TrialModalMetricsComponent } from './metrics.component'; + +@NgModule({ + declarations: [TrialModalMetricsComponent], + imports: [ConditionsTableModule, DetailsListModule], + exports: [TrialModalMetricsComponent], +}) +export class TrialModalMetricsModule {} diff --git a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/metrics/metrics.component.spec.ts b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/metrics/metrics.component.spec.ts new file mode 100644 index 00000000000..1b5184c1ffd --- /dev/null +++ b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/metrics/metrics.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TrialModalMetricsComponent } from './metrics.component'; + +describe('TrialModalMetricsComponent', () => { + let component: TrialModalMetricsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [TrialModalMetricsComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TrialModalMetricsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/metrics/metrics.component.ts b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/metrics/metrics.component.ts new file mode 100644 index 00000000000..33bb66513b9 --- /dev/null +++ b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/metrics/metrics.component.ts @@ -0,0 +1,12 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-metrics-overview', + templateUrl: './metrics.component.html', +}) +export class TrialModalMetricsComponent { + @Input() name: string; + @Input() latest: string; + @Input() max: string; + @Input() min: string; +} diff --git a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/trial-modal-overview.component.html b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/trial-modal-overview.component.html new file mode 100644 index 00000000000..4d133ec1220 --- /dev/null +++ b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/trial-modal-overview.component.html @@ -0,0 +1,42 @@ +
+ + {{ trialName }} + + + + {{ experimentName }} + + + + {{ status }} + + + + {{ completionTime }} + + + + + + +
+ + + +
+
+ + +
diff --git a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/trial-modal-overview.component.spec.ts b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/trial-modal-overview.component.spec.ts new file mode 100644 index 00000000000..48ff1b349fb --- /dev/null +++ b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/trial-modal-overview.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TrialModalOverviewComponent } from './trial-modal-overview.component'; + +describe('TrialModalOverviewComponent', () => { + let component: TrialModalOverviewComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [TrialModalOverviewComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TrialModalOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/trial-modal-overview.component.ts b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/trial-modal-overview.component.ts new file mode 100644 index 00000000000..bdeb334b66b --- /dev/null +++ b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/trial-modal-overview.component.ts @@ -0,0 +1,114 @@ +import { + ChangeDetectionStrategy, + Component, + Input, + OnChanges, +} from '@angular/core'; +import { ChipDescriptor, getCondition } from 'kubeflow'; +import { StatusEnum } from 'src/app/enumerations/status.enum'; +import { TrialK8s } from 'src/app/models/trial.k8s.model'; +import { numberToExponential } from 'src/app/shared/utils'; + +@Component({ + selector: 'app-trial-modal-overview', + templateUrl: './trial-modal-overview.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TrialModalOverviewComponent implements OnChanges { + status: string; + statusIcon: string; + completionTime: string; + performance: ChipDescriptor[]; + + @Input() + trialName: string; + + @Input() + trial: TrialK8s; + + @Input() + experimentName: string; + + constructor() {} + + ngOnInit() { + if (this.trial) { + const { status, statusIcon } = this.generateTrialStatus(this.trial); + this.status = status; + this.statusIcon = statusIcon; + } + } + + ngOnChanges(): void { + if (this.trial) { + this.generateTrialPropsList(this.trial); + } + } + + private generateTrialPropsList(trial: TrialK8s): void { + this.performance = this.generateTrialMetrics(this.trial); + + const { status, statusIcon } = this.generateTrialStatus(trial); + this.status = status; + this.statusIcon = statusIcon; + this.statusIcon = statusIcon; + this.completionTime = trial.status?.completionTime; + } + + private generateTrialStatus(trial: TrialK8s): { + status: string; + statusIcon: string; + } { + const succeededCondition = getCondition(trial, StatusEnum.SUCCEEDED); + + if (succeededCondition && succeededCondition.status === 'True') { + return { status: succeededCondition.message, statusIcon: 'check_circle' }; + } + + const failedCondition = getCondition(trial, StatusEnum.FAILED); + + if (failedCondition && failedCondition.status === 'True') { + return { status: failedCondition.message, statusIcon: 'warning' }; + } + + const runningCondition = getCondition(trial, StatusEnum.RUNNING); + + if (runningCondition && runningCondition.status === 'True') { + return { status: runningCondition.message, statusIcon: 'schedule' }; + } + + const restartingCondition = getCondition(trial, StatusEnum.RESTARTING); + + if (restartingCondition && restartingCondition.status === 'True') { + return { status: restartingCondition.message, statusIcon: 'loop' }; + } + + const createdCondition = getCondition(trial, StatusEnum.CREATED); + + if (createdCondition && createdCondition.status === 'True') { + return { + status: createdCondition.message, + statusIcon: 'add_circle_outline', + }; + } + } + + private generateTrialMetrics(trial: TrialK8s): ChipDescriptor[] { + if (!trial.status.observation || !trial.status.observation.metrics) { + return []; + } + + const metrics = trial.status.observation.metrics.map( + metric => + `${metric.name}: ${ + !isNaN(+metric.latest) + ? numberToExponential(+metric.latest, 6) + : metric.latest + }`, + ); + + return metrics.map(m => { + return { value: m, color: 'primary', tooltip: 'Latest value' }; + }); + } +} diff --git a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/trial-modal-overview.module.ts b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/trial-modal-overview.module.ts new file mode 100644 index 00000000000..cfa3fbda241 --- /dev/null +++ b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/overview/trial-modal-overview.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { + ConditionsTableModule, + DetailsListModule, + HeadingSubheadingRowModule, +} from 'kubeflow'; +import { TrialModalMetricsModule } from './metrics/metrics.component.module'; +import { TrialModalOverviewComponent } from './trial-modal-overview.component'; + +@NgModule({ + declarations: [TrialModalOverviewComponent], + imports: [ + CommonModule, + ConditionsTableModule, + DetailsListModule, + HeadingSubheadingRowModule, + TrialModalMetricsModule, + ], + exports: [TrialModalOverviewComponent], +}) +export class TrialModalOverviewModule {} diff --git a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/trial-modal.component.html b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/trial-modal.component.html index dfbca18632b..d0bf1817c17 100644 --- a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/trial-modal.component.html +++ b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/trials-table/trial-modal/trial-modal.component.html @@ -1,31 +1,56 @@ -