Skip to content

Commit 1354d3a

Browse files
committed
Distinct page for each Trial in the UI
1 parent 2c8758b commit 1354d3a

18 files changed

+588
-89
lines changed

cmd/new-ui/v1beta1/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ func main() {
5555
http.HandleFunc("/katib/delete_experiment/", kuh.DeleteExperiment)
5656

5757
http.HandleFunc("/katib/fetch_experiment/", kuh.FetchExperiment)
58+
http.HandleFunc("/katib/fetch_trial/", kuh.FetchTrial)
5859
http.HandleFunc("/katib/fetch_suggestion/", kuh.FetchSuggestion)
5960

6061
http.HandleFunc("/katib/fetch_hp_job_info/", kuh.FetchHPJobInfo)

pkg/new-ui/v1beta1/backend.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,3 +397,27 @@ func (k *KatibUIHandler) FetchSuggestion(w http.ResponseWriter, r *http.Request)
397397
return
398398
}
399399
}
400+
401+
// FetchTrial gets trial in specific namespace.
402+
func (k *KatibUIHandler) FetchTrial(w http.ResponseWriter, r *http.Request) {
403+
trialName := r.URL.Query()["trialName"][0]
404+
namespace := r.URL.Query()["namespace"][0]
405+
406+
trial, err := k.katibClient.GetTrial(trialName, namespace)
407+
if err != nil {
408+
log.Printf("GetTrial failed: %v", err)
409+
http.Error(w, err.Error(), http.StatusInternalServerError)
410+
return
411+
}
412+
response, err := json.Marshal(trial)
413+
if err != nil {
414+
log.Printf("Marshal Trial failed: %v", err)
415+
http.Error(w, err.Error(), http.StatusInternalServerError)
416+
return
417+
}
418+
if _, err = w.Write(response); err != nil {
419+
log.Printf("Write trial failed: %v", err)
420+
http.Error(w, err.Error(), http.StatusInternalServerError)
421+
return
422+
}
423+
}

pkg/new-ui/v1beta1/frontend/src/app/app-routing.module.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@ import { Routes, RouterModule } from '@angular/router';
33
import { ExperimentsComponent } from './pages/experiments/experiments.component';
44
import { ExperimentDetailsComponent } from './pages/experiment-details/experiment-details.component';
55
import { ExperimentCreationComponent } from './pages/experiment-creation/experiment-creation.component';
6+
import { TrialModalComponent } from './pages/experiment-details/trials-table/trial-modal/trial-modal.component';
67

78
const routes: Routes = [
89
{ path: '', component: ExperimentsComponent },
910
{ path: 'experiment/:experimentName', component: ExperimentDetailsComponent },
1011
{ path: 'new', component: ExperimentCreationComponent },
12+
{
13+
path: 'experiment/:experimentName/trial/:trialName',
14+
component: TrialModalComponent,
15+
},
1116
];
1217

1318
@NgModule({

pkg/new-ui/v1beta1/frontend/src/app/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { AppComponent } from './app.component';
88
import { ExperimentsModule } from './pages/experiments/experiments.module';
99
import { ExperimentDetailsModule } from './pages/experiment-details/experiment-details.module';
1010
import { ExperimentCreationModule } from './pages/experiment-creation/experiment-creation.module';
11+
import { TrialModalModule } from './pages/experiment-details/trials-table/trial-modal/trial-modal.module';
1112

1213
@NgModule({
1314
declarations: [AppComponent],
@@ -19,6 +20,7 @@ import { ExperimentCreationModule } from './pages/experiment-creation/experiment
1920
ExperimentDetailsModule,
2021
ReactiveFormsModule,
2122
ExperimentCreationModule,
23+
TrialModalModule,
2224
],
2325
providers: [],
2426
bootstrap: [AppComponent],
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { K8sObject } from 'kubeflow';
2+
import { V1Container } from '@kubernetes/client-node';
3+
4+
/*
5+
* K8s object definitions
6+
*/
7+
export const TRIAL_KIND = 'Trial';
8+
export const TRIAL_APIVERSION = 'kubeflow.org/v1beta1';
9+
10+
export interface TrialK8s extends K8sObject {
11+
spec?: TrialSpec;
12+
status?: TrialStatus;
13+
}
14+
15+
export interface TrialSpec {
16+
metricsCollector: MetricsCollector;
17+
objective: Objective;
18+
parameterAssignments: { name: string; value: number }[];
19+
primaryContainerName: string;
20+
successCondition: string;
21+
failureCondition: string;
22+
runSpec: K8sObject;
23+
}
24+
25+
export interface MetricsCollector {
26+
collector?: CollectorSpec;
27+
}
28+
29+
export interface CollectorSpec {
30+
kind: CollectorKind;
31+
customCollector: V1Container;
32+
}
33+
34+
export type CollectorKind =
35+
| 'StdOut'
36+
| 'File'
37+
| 'TensorFlowEvent'
38+
| 'PrometheusMetric'
39+
| 'Custom'
40+
| 'None';
41+
42+
export interface Objective {
43+
type: ObjectiveType;
44+
goal: number;
45+
objectiveMetricName: string;
46+
additionalMetricNames: string[];
47+
metricStrategies: MetricStrategy[];
48+
}
49+
50+
export type ObjectiveType = 'maximize' | 'minimize';
51+
52+
export interface MetricStrategy {
53+
name: string;
54+
value: string;
55+
}
56+
57+
export interface RunSpec {}
58+
59+
/*
60+
* status
61+
*/
62+
63+
interface TrialStatus {
64+
startTime: string;
65+
completionTime: string;
66+
conditions: TrialStatusCondition[];
67+
observation: {
68+
metrics: {
69+
name: string;
70+
latest: number;
71+
min: number;
72+
max: string;
73+
}[];
74+
};
75+
}
76+
77+
interface TrialStatusCondition {
78+
type: string;
79+
status: boolean;
80+
reason: string;
81+
message: string;
82+
lastUpdateTime: string;
83+
lastTransitionTime: string;
84+
}

pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/experiment-details.component.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@
3333
</ng-template>
3434

3535
<div class="tab-height-fix">
36-
<mat-tab-group dynamicHeight animationDuration="0ms">
36+
<mat-tab-group
37+
dynamicHeight
38+
animationDuration="0ms"
39+
[(selectedIndex)]="selectedTab"
40+
(selectedTabChange)="tabChanged($event)"
41+
>
3742
<mat-tab label="OVERVIEW">
3843
<app-experiment-overview
3944
[experimentName]="name"
@@ -45,6 +50,7 @@
4550
(leaveMouseFromTrial)="mouseLeftTrial()"
4651
(mouseOnTrial)="mouseOverTrial($event)"
4752
[data]="details"
53+
[experimentName]="name"
4854
[displayedColumns]="columns"
4955
[namespace]="namespace"
5056
[bestTrialName]="bestTrialName"

pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/experiment-details.component.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Component, OnDestroy, OnInit } from '@angular/core';
22
import { ActivatedRoute, Router } from '@angular/router';
3+
import { MatTabChangeEvent } from '@angular/material/tabs';
34
import {
45
ConfirmDialogService,
56
DIALOG_RESP,
@@ -35,6 +36,13 @@ export class ExperimentDetailsComponent implements OnInit, OnDestroy {
3536
showGraph: boolean;
3637
bestTrialName: string;
3738
pageLoading = true;
39+
selectedTab = 0;
40+
tabs = new Map<string, number>([
41+
['overview', 0],
42+
['trials', 1],
43+
['details', 2],
44+
['yaml', 3],
45+
]);
3846

3947
constructor(
4048
private activatedRoute: ActivatedRoute,
@@ -62,6 +70,12 @@ export class ExperimentDetailsComponent implements OnInit, OnDestroy {
6270
ngOnInit() {
6371
this.name = this.activatedRoute.snapshot.params.experimentName;
6472

73+
if (this.activatedRoute.snapshot.queryParams['tab']) {
74+
this.selectedTab = this.tabs.get(
75+
this.activatedRoute.snapshot.queryParams['tab'],
76+
);
77+
}
78+
6579
this.subs.add(
6680
this.namespaceService.getSelectedNamespace().subscribe(namespace => {
6781
this.namespace = namespace;
@@ -70,6 +84,10 @@ export class ExperimentDetailsComponent implements OnInit, OnDestroy {
7084
);
7185
}
7286

87+
tabChanged(event: MatTabChangeEvent) {
88+
this.selectedTab = event.index;
89+
}
90+
7391
ngOnDestroy(): void {
7492
this.subs.unsubscribe();
7593
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<lib-details-list-item key="Trial Name">
2+
{{ trialName }}
3+
</lib-details-list-item>
4+
5+
<lib-details-list-item key="Experiment Name">
6+
{{ experimentName }}
7+
</lib-details-list-item>
8+
9+
<lib-details-list-item key="Status" [icon]="statusIcon">
10+
{{ status }}
11+
</lib-details-list-item>
12+
13+
<lib-details-list-item [chipsList]="performance" key="Performance">
14+
</lib-details-list-item>
15+
16+
<lib-conditions-table
17+
*ngIf="trial"
18+
[conditions]="trial.status.conditions"
19+
title="Trial Conditions"
20+
></lib-conditions-table>
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 { TrialModalOverviewComponent } from './trial-modal-overview.component';
4+
5+
describe('TrialModalOverviewComponent', () => {
6+
let component: TrialModalOverviewComponent;
7+
let fixture: ComponentFixture<TrialModalOverviewComponent>;
8+
9+
beforeEach(async(() => {
10+
TestBed.configureTestingModule({
11+
declarations: [TrialModalOverviewComponent],
12+
}).compileComponents();
13+
}));
14+
15+
beforeEach(() => {
16+
fixture = TestBed.createComponent(TrialModalOverviewComponent);
17+
component = fixture.componentInstance;
18+
fixture.detectChanges();
19+
});
20+
21+
it('should create', () => {
22+
expect(component).toBeTruthy();
23+
});
24+
});
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
Input,
5+
OnChanges,
6+
} from '@angular/core';
7+
import { ChipDescriptor, getCondition } from 'kubeflow';
8+
import { StatusEnum } from 'src/app/enumerations/status.enum';
9+
import { TrialK8s } from 'src/app/models/trial.k8s.model';
10+
import { numberToExponential } from 'src/app/shared/utils';
11+
12+
@Component({
13+
selector: 'app-trial-modal-overview',
14+
templateUrl: './trial-modal-overview.component.html',
15+
changeDetection: ChangeDetectionStrategy.OnPush,
16+
})
17+
export class TrialModalOverviewComponent implements OnChanges {
18+
status: string;
19+
statusIcon: string;
20+
performance: ChipDescriptor[];
21+
22+
@Input()
23+
trialName: string;
24+
25+
@Input()
26+
trial: TrialK8s;
27+
28+
@Input()
29+
experimentName: string;
30+
31+
constructor() {}
32+
33+
ngOnInit() {
34+
if (this.trial) {
35+
const { status, statusIcon } = this.generateTrialStatus(this.trial);
36+
this.status = status;
37+
this.statusIcon = statusIcon;
38+
}
39+
}
40+
41+
ngOnChanges(): void {
42+
if (this.trial) {
43+
this.generateTrialPropsList(this.trial);
44+
}
45+
}
46+
47+
private generateTrialPropsList(trial: TrialK8s): void {
48+
this.performance = this.generateTrialMetrics(this.trial);
49+
50+
const { status, statusIcon } = this.generateTrialStatus(trial);
51+
this.status = status;
52+
this.statusIcon = statusIcon;
53+
}
54+
55+
private generateTrialStatus(trial: TrialK8s): {
56+
status: string;
57+
statusIcon: string;
58+
} {
59+
const succeededCondition = getCondition(trial, StatusEnum.SUCCEEDED);
60+
61+
if (succeededCondition && succeededCondition.status === 'True') {
62+
return { status: succeededCondition.message, statusIcon: 'check_circle' };
63+
}
64+
65+
const failedCondition = getCondition(trial, StatusEnum.FAILED);
66+
67+
if (failedCondition && failedCondition.status === 'True') {
68+
return { status: failedCondition.message, statusIcon: 'warning' };
69+
}
70+
71+
const runningCondition = getCondition(trial, StatusEnum.RUNNING);
72+
73+
if (runningCondition && runningCondition.status === 'True') {
74+
return { status: runningCondition.message, statusIcon: 'schedule' };
75+
}
76+
77+
const restartingCondition = getCondition(trial, StatusEnum.RESTARTING);
78+
79+
if (restartingCondition && restartingCondition.status === 'True') {
80+
return { status: restartingCondition.message, statusIcon: 'loop' };
81+
}
82+
83+
const createdCondition = getCondition(trial, StatusEnum.CREATED);
84+
85+
if (createdCondition && createdCondition.status === 'True') {
86+
return {
87+
status: createdCondition.message,
88+
statusIcon: 'add_circle_outline',
89+
};
90+
}
91+
}
92+
93+
private generateTrialMetrics(trial: TrialK8s): ChipDescriptor[] {
94+
if (!trial.status.observation || !trial.status.observation.metrics) {
95+
return [];
96+
}
97+
98+
const metrics = trial.status.observation.metrics.map(
99+
metric =>
100+
`${metric.name}: ${
101+
!isNaN(+metric.latest)
102+
? numberToExponential(+metric.latest, 6)
103+
: metric.latest
104+
}`,
105+
);
106+
107+
return metrics.map(m => {
108+
return { value: m, color: 'primary', tooltip: 'Latest value' };
109+
});
110+
}
111+
}

0 commit comments

Comments
 (0)