From 3562e5abcb281052c784e37ef77674f7f39b24e9 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 ++ .../trial-modal-overview.component.html | 20 ++ .../trial-modal-overview.component.spec.ts | 24 ++ .../trial-modal-overview.component.ts | 111 ++++++++++ .../overview/trial-modal-overview.module.ts | 12 + .../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 | 38 ++++ .../trials-table/trials-table.component.ts | 8 +- .../trials-table/trials-table.module.ts | 3 +- .../src/app/services/backend.service.ts | 6 + 18 files changed, 588 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/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 afbf2fd0e39..a03b796d7ad 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 ae9aa655825..61d256896ca 100644 --- a/pkg/new-ui/v1beta1/backend.go +++ b/pkg/new-ui/v1beta1/backend.go @@ -396,3 +396,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..04087f21cc5 --- /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: number; + min: number; + 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/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..84a90542416 --- /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,20 @@ + + {{ trialName }} + + + + {{ experimentName }} + + + + {{ status }} + + + + + + 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..a0d7fc3f8f3 --- /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,111 @@ +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; + 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; + } + + 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..59a94b751ab --- /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,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ConditionsTableModule, DetailsListModule } from 'kubeflow'; + +import { TrialModalOverviewComponent } from './trial-modal-overview.component'; + +@NgModule({ + declarations: [TrialModalOverviewComponent], + imports: [CommonModule, ConditionsTableModule, DetailsListModule], + 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 @@ -