Skip to content

Commit f83a668

Browse files
committed
Distinct page for each Trial in the UI
1 parent f5abfd0 commit f83a668

18 files changed

+592
-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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,3 +396,23 @@ func (k *KatibUIHandler) FetchSuggestion(w http.ResponseWriter, r *http.Request)
396396
return
397397
}
398398
}
399+
400+
// FetchTrial gets trial in specific namespace.
401+
func (k *KatibUIHandler) FetchTrial(w http.ResponseWriter, r *http.Request) {
402+
trialName := r.URL.Query()["trialName"][0]
403+
namespace := r.URL.Query()["namespace"][0]
404+
405+
trial, err := k.katibClient.GetTrial(trialName, namespace)
406+
if err != nil {
407+
log.Printf("GetTrial failed: %v", err)
408+
http.Error(w, err.Error(), http.StatusInternalServerError)
409+
return
410+
}
411+
response, err := json.Marshal(trial)
412+
if err != nil {
413+
log.Printf("Marshal Trial failed: %v", err)
414+
http.Error(w, err.Error(), http.StatusInternalServerError)
415+
return
416+
}
417+
w.Write(response)
418+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ 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+
{ path: 'experiment/:experimentName/trial/:trialName', component: TrialModalComponent },
1113
];
1214

1315
@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: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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+
61+
/*
62+
* status
63+
*/
64+
65+
interface TrialStatus {
66+
startTime: string;
67+
completionTime: string;
68+
conditions: TrialStatusCondition[];
69+
observation: {
70+
metrics: {
71+
name: string;
72+
latest: number;
73+
min: number;
74+
max: string;
75+
}[];
76+
}
77+
}
78+
79+
interface TrialStatusCondition {
80+
type: string;
81+
status: boolean;
82+
reason: string;
83+
message: string;
84+
lastUpdateTime: string;
85+
lastTransitionTime: string;
86+
}

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

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

3535
<div class="tab-height-fix">
36-
<mat-tab-group dynamicHeight animationDuration="0ms">
36+
<mat-tab-group dynamicHeight animationDuration="0ms"
37+
[(selectedIndex)]="selectedTab"
38+
(selectedTabChange)="tabChanged($event)">
3739
<mat-tab label="OVERVIEW">
3840
<app-experiment-overview
3941
[experimentName]="name"
@@ -45,6 +47,7 @@
4547
(leaveMouseFromTrial)="mouseLeftTrial()"
4648
(mouseOnTrial)="mouseOverTrial($event)"
4749
[data]="details"
50+
[experimentName]="name"
4851
[displayedColumns]="columns"
4952
[namespace]="namespace"
5053
[bestTrialName]="bestTrialName"

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

Lines changed: 16 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,10 @@ 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(this.activatedRoute.snapshot.queryParams['tab']);
75+
}
76+
6577
this.subs.add(
6678
this.namespaceService.getSelectedNamespace().subscribe(namespace => {
6779
this.namespace = namespace;
@@ -70,6 +82,10 @@ export class ExperimentDetailsComponent implements OnInit, OnDestroy {
7082
);
7183
}
7284

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

0 commit comments

Comments
 (0)