You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@submarine.apache.org by pi...@apache.org on 2020/12/30 06:15:18 UTC
[submarine] branch master updated: SUBMARINE-690. Enable
pre-defined web interface in submarine workbench
This is an automated email from the ASF dual-hosted git repository.
pingsutw pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/submarine.git
The following commit(s) were added to refs/heads/master by this push:
new 706ded5 SUBMARINE-690. Enable pre-defined web interface in submarine workbench
706ded5 is described below
commit 706ded5e487ba56b9122f9926b2e4b7f6da0663e
Author: ByronHsu <by...@gmail.com>
AuthorDate: Wed Dec 9 17:27:02 2020 +0800
SUBMARINE-690. Enable pre-defined web interface in submarine workbench
### What is this PR for?
**Add pre-defined web interface.**
The steps for users to create a pre-defined experiment are as following:
1. Click pre-defined button
2. Pre-defined data will be fetched from db
3. Render configurable params and spec on ng-modal
4. When the form is validated, the experiment can be submitted.
### What type of PR is it?
[Feature]
### Todos
- [ ] Test predefined component (The e2e test failed now due to unknown reason)
- [ ] Real-time fetch experiment status
- [ ] The implementation logic of the experiment page should be improved (e.g. split components correctly, use resolver to avoid long-time waiting of data)
### What is the Jira issue?
https://issues.apache.org/jira/projects/SUBMARINE/issues/SUBMARINE-690
### How should this be tested?
https://github.com/ByronHsu/submarine/actions/runs/404158134
### Screenshots (if appropriate)
![Kapture 2020-12-07 at 17 18 34](https://user-images.githubusercontent.com/24364830/101353091-04360680-38ce-11eb-9ccd-89d383d09d99.gif)
Author: ByronHsu <by...@gmail.com>
Closes #470 from ByronHsu/SUBMARINE-690 and squashes the following commits:
f188f94 [ByronHsu] Restore temporary fix
c8e7722 [ByronHsu] Update submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-predefined-form/experiment-predefined-form.component.ts
b4421d3 [ByronHsu] Merge branch 'master' into SUBMARINE-690
f760f62 [ByronHsu] Fix license
8a048d3 [ByronHsu] add predefined form component
89b36c4 [ByronHsu] update template doc
52d29d3 [ByronHsu] no need to export reactiveform module in experiment
1d3be1e [ByronHsu] use canActivate as authguard
---
docs/userdocs/k8s/run-experiment-template-rest.md | 16 +-
.../app/interfaces/experiment-template-submit.ts | 22 +++
.../src/app/interfaces/experiment-template.ts | 40 +++++
.../experiment-customized-form.component.html | 28 ++--
.../experiment-customized-form.component.ts | 10 +-
.../experiment-home/experiment-home.component.html | 21 +++
.../experiment-home/experiment-home.component.scss | 18 +++
.../experiment-home/experiment-home.component.ts | 31 ++++
.../experiment-predefined-form.component.html | 64 ++++++++
.../experiment-predefined-form.component.scss | 18 +++
.../experiment-predefined-form.component.ts | 171 +++++++++++++++++++++
.../experiment/experiment-routing.module.ts | 48 ++++++
.../workbench/experiment/experiment.component.html | 17 +-
.../workbench/experiment/experiment.component.ts | 8 +-
.../workbench/experiment/experiment.module.ts | 12 +-
.../pages/workbench/workbench-routing.module.ts | 11 +-
.../src/app/pages/workbench/workbench.module.ts | 3 +-
.../src/app/services/experiment.form.service.ts | 4 +-
.../src/app/services/experiment.service.ts | 44 +++++-
19 files changed, 534 insertions(+), 52 deletions(-)
diff --git a/docs/userdocs/k8s/run-experiment-template-rest.md b/docs/userdocs/k8s/run-experiment-template-rest.md
index 183f760..e20f4b4 100644
--- a/docs/userdocs/k8s/run-experiment-template-rest.md
+++ b/docs/userdocs/k8s/run-experiment-template-rest.md
@@ -20,27 +20,29 @@ under the License.
# Experiment Template REST API Reference
## Experiment Template Spec
-The experiment is represented in [JSON](https://www.json.org) or [YAML](https://yaml.org) format.
+The experiment is represented in [JSON](https://www.json.org) or [YAML](https://yaml.org) format.
### Use existing experiment template to create a experiment
-`POST /api/v1/environment/{name}`
+
+`POST /api/v1/experiment/{template-name}`
**Example Request:**
+
```sh
curl -X POST -H "Content-Type: application/json" -d '
{
- "name": "tf-mnist",
"params": {
- "learning_rate":"0.01",
- "batch_size":"150",
+ "learning_rate":"0.01",
+ "batch_size":"150",
"experiment_name":"newexperiment1"
}
}
-' http://127.0.0.1:8080/api/v1/experiment/my-tf-mnist-template
+' http://127.0.0.1:32080/api/v1/experiment/tf-mnist
```
**Example Request:**
+
```sh
curl -X POST -H "Content-Type: application/json" -d '
{
@@ -48,7 +50,7 @@ curl -X POST -H "Content-Type: application/json" -d '
"experiment_name":"new-pytorch-mnist"
}
}
-' http://127.0.0.1:8080/api/v1/experiment/pytorch-mnist
+' http://127.0.0.1:32080/api/v1/experiment/pytorch-mnist
```
Register experiment template and more info see [Experiment Template API Reference](api/experiment-template.md).
diff --git a/submarine-workbench/workbench-web/src/app/interfaces/experiment-template-submit.ts b/submarine-workbench/workbench-web/src/app/interfaces/experiment-template-submit.ts
new file mode 100644
index 0000000..5eb16c0
--- /dev/null
+++ b/submarine-workbench/workbench-web/src/app/interfaces/experiment-template-submit.ts
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export interface ExperimentTemplateSubmit {
+ params: { [key: string]: string };
+}
diff --git a/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts b/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts
new file mode 100644
index 0000000..d88cbf2
--- /dev/null
+++ b/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { ExperimentSpec } from '@submarine/interfaces/experiment-spec';
+
+interface ExperimentTemplateParamSpec {
+ name: string;
+ required: string;
+ description: string;
+ value: string;
+}
+
+interface ExperimentTemplateSpec {
+ name: string;
+ author: string;
+ description: string;
+ parameters: ExperimentTemplateParamSpec[];
+ experimentSpec: ExperimentSpec;
+}
+
+export interface ExperimentTemplate {
+ experimentTemplateId: string;
+ experimentTemplateSpec: ExperimentTemplateSpec;
+}
diff --git a/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.html b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.html
index c9afe7c..4d7b2ea 100644
--- a/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.html
+++ b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.html
@@ -26,7 +26,7 @@
</div>
<div>
<form [formGroup]="experiment">
- <div [ngSwitch]="step" style="margin-top: 30px;">
+ <div [ngSwitch]="step" style="margin-top: 30px">
<div *ngSwitchCase="0" id="firstStep">
<div class="single-field-group">
<label for="experimentName">
@@ -86,10 +86,10 @@
</div>
</ng-template>
</div>
- <div style="margin-bottom: 10px;">
+ <div style="margin-bottom: 10px">
<button
nz-button
- style="display: block; margin: auto;"
+ style="display: block; margin: auto"
id="advancedBtn"
nzType="default"
(click)="ADVANCED = !ADVANCED"
@@ -127,11 +127,11 @@
<ul formArrayName="envs" class="list-container">
<ng-container *ngFor="let env of envs.controls; index as i">
<li [formGroupName]="i" class="input-group">
- <div style="margin-left: 30%;">
+ <div style="margin-left: 30%">
<label for="key{{ i }}">Key</label>
<input nz-input name="key{{ i }}" placeholder="Key" formControlName="key" id="key{{ i }}" />
</div>
- <div style="margin-left: 0;">
+ <div style="margin-left: 0">
<label for="value{{ i }}">Value</label>
<input nz-input name="value{{ i }}" placeholder="Value" formControlName="value" id="value{{ i }}" />
</div>
@@ -151,7 +151,7 @@
</div>
</ng-container>
</ul>
- <button nz-button id="env-btn" style="display: block; margin: auto;" nzType="primary" (click)="onCreateEnv()">
+ <button nz-button id="env-btn" style="display: block; margin: auto" nzType="primary" (click)="onCreateEnv()">
Add new environment variable
</button>
<br />
@@ -269,26 +269,26 @@
</div>
<div *ngSwitchCase="2" id="previewPage">
<nz-descriptions nzTitle="{{ jobTypes }}" nzBordered [nzColumn]="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 2, xs: 1 }">
- <nz-descriptions-item nzTitle="Name">{{ finialExperimentSpec.meta.name }}</nz-descriptions-item>
- <nz-descriptions-item nzTitle="Namespace">{{ finialExperimentSpec.meta.namespace }}</nz-descriptions-item>
+ <nz-descriptions-item nzTitle="Name">{{ finalExperimentSpec.meta.name }}</nz-descriptions-item>
+ <nz-descriptions-item nzTitle="Namespace">{{ finalExperimentSpec.meta.namespace }}</nz-descriptions-item>
<nz-descriptions-item nzTitle="Command" [nzSpan]="2">
- {{ finialExperimentSpec.meta.cmd }}
+ {{ finalExperimentSpec.meta.cmd }}
</nz-descriptions-item>
<nz-descriptions-item nzTitle="Image" [nzSpan]="2">
- {{ finialExperimentSpec.environment.image }}
+ {{ finalExperimentSpec.environment.image }}
</nz-descriptions-item>
<nz-descriptions-item nzTitle="Environment Variables" [nzSpan]="2">
- <span *ngFor="let item of finialExperimentSpec.meta.envVars | keyvalue; let isLast = last">
+ <span *ngFor="let item of finalExperimentSpec.meta.envVars | keyvalue; let isLast = last">
{{ item.key }}={{ item.value }}{{ isLast ? '' : ', ' }}
</span>
</nz-descriptions-item>
- <div *ngFor="let item of finialExperimentSpec.spec | keyvalue">
+ <div *ngFor="let item of finalExperimentSpec.spec | keyvalue">
<nz-descriptions-item nzTitle="{{ item.key }}" [nzSpan]="2">
{{ item.value.resources }}
</nz-descriptions-item>
</div>
- <div *ngIf="finialExperimentSpec.code">
- <nz-descriptions-item nzTitle="Git repository">{{ finialExperimentSpec.code.url }}</nz-descriptions-item>
+ <div *ngIf="finalExperimentSpec.code">
+ <nz-descriptions-item nzTitle="Git repository">{{ finalExperimentSpec.code.url }}</nz-descriptions-item>
</div>
</nz-descriptions>
</div>
diff --git a/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.ts b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.ts
index bae3d7f..7cde4dd 100644
--- a/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.ts
+++ b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.ts
@@ -43,7 +43,7 @@ export class ExperimentCustomizedFormComponent implements OnInit, OnDestroy {
// About new experiment
experiment: FormGroup;
- finialExperimentSpec: ExperimentSpec;
+ finalExperimentSpec: ExperimentSpec;
step: number = 0;
subscriptions: Subscription[] = [];
@@ -199,14 +199,14 @@ export class ExperimentCustomizedFormComponent implements OnInit, OnDestroy {
}
}
onPreview() {
- this.finialExperimentSpec = this.constructSpec();
+ this.finalExperimentSpec = this.constructSpec();
}
/**
* Event handler for Next step/Submit button
*/
handleSubmit() {
if (this.mode === 'create') {
- this.experimentService.createExperiment(this.finialExperimentSpec).subscribe({
+ this.experimentService.createExperiment(this.finalExperimentSpec).subscribe({
next: () => {},
error: (msg) => {
this.nzMessageService.error(`${msg}, please try again`, {
@@ -220,7 +220,7 @@ export class ExperimentCustomizedFormComponent implements OnInit, OnDestroy {
}
});
} else if (this.mode === 'update') {
- this.experimentService.updateExperiment(this.targetId, this.finialExperimentSpec).subscribe(
+ this.experimentService.updateExperiment(this.targetId, this.finalExperimentSpec).subscribe(
null,
(msg) => {
this.nzMessageService.error(`${msg}, please try again`, {
@@ -234,7 +234,7 @@ export class ExperimentCustomizedFormComponent implements OnInit, OnDestroy {
}
);
} else if (this.mode === 'clone') {
- this.experimentService.createExperiment(this.finialExperimentSpec).subscribe(
+ this.experimentService.createExperiment(this.finalExperimentSpec).subscribe(
null,
(msg) => {
this.nzMessageService.error(`${msg}, please try again`, {
diff --git a/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-home/experiment-home.component.html b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-home/experiment-home.component.html
new file mode 100644
index 0000000..2581d72
--- /dev/null
+++ b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-home/experiment-home.component.html
@@ -0,0 +1,21 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+<!-- <p>experiment-home works!</p> -->
+<!-- TODO: should split home page from experiment root components -->
diff --git a/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-home/experiment-home.component.scss b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-home/experiment-home.component.scss
new file mode 100644
index 0000000..042f3ce
--- /dev/null
+++ b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-home/experiment-home.component.scss
@@ -0,0 +1,18 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
diff --git a/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-home/experiment-home.component.ts b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-home/experiment-home.component.ts
new file mode 100644
index 0000000..4f46d91
--- /dev/null
+++ b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-home/experiment-home.component.ts
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'app-experiment-home',
+ templateUrl: './experiment-home.component.html',
+ styleUrls: ['./experiment-home.component.scss'],
+})
+export class ExperimentHomeComponent implements OnInit {
+ constructor() {}
+
+ ngOnInit() {}
+}
diff --git a/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-predefined-form/experiment-predefined-form.component.html b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-predefined-form/experiment-predefined-form.component.html
new file mode 100644
index 0000000..dc4faeb
--- /dev/null
+++ b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-predefined-form/experiment-predefined-form.component.html
@@ -0,0 +1,64 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+<form nz-form [formGroup]="predefinedForm">
+ <nz-form-item>
+ <nz-form-label [nzSpan]="5" nzFor="predefined-template" nzRequired>predefined-template</nz-form-label>
+ <nz-form-control [nzSpan]="12" nzErrorTip="Please select templates.">
+ <nz-select
+ id="predefined-template"
+ nzPlaceHolder="Select a predefined experiment template"
+ formControlName="templateName"
+ (ngModelChange)="onTemplateChange()"
+ >
+ <nz-option *ngFor="let item of optionList" [nzLabel]="item" [nzValue]="item"></nz-option>
+ </nz-select>
+ </nz-form-control>
+ </nz-form-item>
+
+ <div formGroupName="params" *ngIf="currentOption != null">
+ <h4 nz-typography>Configurable Parameters</h4>
+ <nz-form-item *ngFor="let item of paramList">
+ <nz-form-label [nzSpan]="5" [nzRequired]="item.required" [nzFor]="item.name">{{ item.name }}</nz-form-label>
+ <nz-form-control [nzSpan]="12" nzErrorTip="Please provide the value!">
+ <input type="text" nz-input [id]="item.name" [formControlName]="item.name" />
+ </nz-form-control>
+ </nz-form-item>
+ </div>
+
+ <nz-form-item *ngIf="currentOption != null">
+ <nz-descriptions nzTitle="Spec" nzBordered [nzColumn]="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 2, xs: 1 }">
+ <nz-descriptions-item nzTitle="Name">
+ {{ templates[currentOption].experimentName }}
+ </nz-descriptions-item>
+ <nz-descriptions-item nzTitle="Namespace">
+ {{ templates[currentOption].experimentNamespace }}
+ </nz-descriptions-item>
+ <nz-descriptions-item nzTitle="Command" [nzSpan]="2">
+ {{ templates[currentOption].experimentCommand }}
+ </nz-descriptions-item>
+ <nz-descriptions-item nzTitle="Image" [nzSpan]="2">
+ {{ templates[currentOption].experimentImage }}
+ </nz-descriptions-item>
+ <nz-descriptions-item nzTitle="Environment Variables" [nzSpan]="2">
+ {{ templates[currentOption].experimentVars }}
+ </nz-descriptions-item>
+ </nz-descriptions>
+ </nz-form-item>
+</form>
diff --git a/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-predefined-form/experiment-predefined-form.component.scss b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-predefined-form/experiment-predefined-form.component.scss
new file mode 100644
index 0000000..042f3ce
--- /dev/null
+++ b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-predefined-form/experiment-predefined-form.component.scss
@@ -0,0 +1,18 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
diff --git a/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-predefined-form/experiment-predefined-form.component.ts b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-predefined-form/experiment-predefined-form.component.ts
new file mode 100644
index 0000000..0bedc75
--- /dev/null
+++ b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-predefined-form/experiment-predefined-form.component.ts
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { ExperimentTemplate } from '@submarine/interfaces/experiment-template';
+import { ExperimentFormService } from '@submarine/services/experiment.form.service';
+import { ExperimentService } from '@submarine/services/experiment.service';
+import { interval, Subscription } from 'rxjs';
+import { ExperimentTemplateSubmit } from '@submarine/interfaces/experiment-template-submit';
+import { NzMessageService } from 'ng-zorro-antd';
+
+interface ParsedTemplate {
+ templateParams: {
+ name: string;
+ required: string;
+ description: string;
+ value: string;
+ }[];
+ experimentName: string;
+ experimentNamespace: string;
+ experimentCommand: string;
+ experimentImage: string;
+ experimentVars: string;
+}
+
+interface TemplateTable {
+ [templateName: string]: ParsedTemplate;
+}
+
+@Component({
+ selector: 'submarine-experiment-predefined-form',
+ templateUrl: './experiment-predefined-form.component.html',
+ styleUrls: ['./experiment-predefined-form.component.scss'],
+})
+export class ExperimentPredefinedFormComponent implements OnInit, OnDestroy {
+ /* states that are bond to html template */
+ paramList: { name: string; required: string }[];
+
+ /* inner states */
+ templates: TemplateTable = {};
+ predefinedForm: FormGroup;
+ subs: Subscription[] = [];
+
+ constructor(
+ private experimentService: ExperimentService,
+ private experimentFormService: ExperimentFormService,
+ private fb: FormBuilder,
+ private nzMessageService: NzMessageService
+ ) {}
+
+ ngOnInit() {
+ this.experimentService.fetchExperimentTemplateList().subscribe((res) => {
+ this.templates = this.parseTemplateRespond(res);
+
+ if (Object.keys(this.templates).length != 0) {
+ // default: switch to first template
+ const defaultTemplate = Object.keys(this.templates)[0];
+ this.predefinedForm.get('templateName').setValue(defaultTemplate);
+ console.log(this.predefinedForm.get('templateName').value);
+ this.onTemplateChange();
+ }
+ });
+
+ this.predefinedForm = this.fb.group({
+ templateName: [null],
+ params: this.fb.group({}),
+ });
+
+ this.experimentFormService.modalPropsChange({
+ okText: 'Submit',
+ });
+
+ this.predefinedForm.statusChanges.subscribe((status) => {
+ this.experimentFormService.btnStatusChange(status === 'INVALID');
+ });
+
+ const sub = this.experimentFormService.stepService.subscribe((step) => {
+ // handle submit
+ this.onSubmit();
+ });
+ const sub2 = interval(1000).subscribe(() => {
+ console.log(this.templates);
+ });
+ this.subs.push(sub);
+ this.subs.push(sub2);
+ }
+ ngOnDestroy() {
+ this.subs.forEach((sub) => sub.unsubscribe());
+ }
+ onSubmit() {
+ // construct spec
+ const finalSpec: ExperimentTemplateSubmit = {
+ params: this.predefinedForm.get('params').value,
+ };
+ const templateName = this.predefinedForm.get('templateName').value;
+
+ // send http post request
+ this.experimentService.createExperimentfromTemplate(finalSpec, templateName).subscribe({
+ next: () => {},
+ error: (msg) => {
+ this.nzMessageService.error(`${msg}, please try again`, {
+ nzPauseOnHover: true,
+ });
+ },
+ complete: () => {
+ this.nzMessageService.success('Experiment creation succeeds');
+ this.experimentFormService.fetchList();
+ this.experimentFormService.modalPropsClear();
+ this.predefinedForm.reset();
+ },
+ });
+ }
+ parseTemplateRespond(res: ExperimentTemplate[]): TemplateTable {
+ let templates: TemplateTable = {};
+ for (let item of res) {
+ // iterate template list
+ let template: ParsedTemplate = {
+ templateParams: item.experimentTemplateSpec.parameters.filter((item) => !item.name.startsWith('spec.')),
+ experimentName: item.experimentTemplateSpec.experimentSpec.meta.name,
+ experimentNamespace: item.experimentTemplateSpec.experimentSpec.meta.namespace,
+ experimentCommand: item.experimentTemplateSpec.experimentSpec.meta.cmd,
+ experimentImage: item.experimentTemplateSpec.experimentSpec.environment.image,
+ experimentVars: JSON.stringify(item.experimentTemplateSpec.experimentSpec.meta.envVars),
+ };
+ templates[item.experimentTemplateSpec.name] = template;
+ }
+ return templates;
+ }
+
+ onTemplateChange() {
+ if (this.currentOption == null) return;
+ /* update paramList */
+ let tmpList = [];
+ for (let item of this.templates[this.currentOption].templateParams)
+ tmpList.push({ name: item.name, required: item.required });
+ this.paramList = tmpList;
+
+ let controls = {};
+ for (let item of this.templates[this.currentOption].templateParams) {
+ controls[item.name] = [item.value];
+ if (item.required === 'true') controls[item.name].push([Validators.required]);
+ }
+ const new_param_group = this.fb.group(controls);
+ this.predefinedForm.setControl('params', new_param_group);
+ }
+
+ /* sugar syntax for option */
+ get currentOption(): string {
+ return this.predefinedForm.get('templateName').value;
+ }
+ get optionList(): string[] {
+ return Object.keys(this.templates);
+ }
+}
diff --git a/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-routing.module.ts b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-routing.module.ts
new file mode 100644
index 0000000..7fc2287
--- /dev/null
+++ b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment-routing.module.ts
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { ExperimentHomeComponent } from './experiment-home/experiment-home.component';
+import { ExperimentInfoComponent } from './experiment-info/experiment-info.component';
+import { ExperimentComponent } from './experiment.component';
+
+const routes: Routes = [
+ {
+ path: '',
+ component: ExperimentComponent,
+ children: [
+ {
+ path: '',
+ pathMatch: 'full',
+ component: ExperimentHomeComponent,
+ },
+ {
+ path: 'info/:id',
+ component: ExperimentInfoComponent,
+ },
+ ],
+ },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule],
+})
+export class ExperimentRoutingModule {}
diff --git a/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment.component.html b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment.component.html
index 4436cef..82a2b1b 100644
--- a/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment.component.html
+++ b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment.component.html
@@ -17,7 +17,7 @@
~ under the License.
-->
-<nz-layout style="margin: -24px -24px 16px;">
+<nz-layout style="margin: -24px -24px 16px">
<nz-layout class="inner-layout">
<div id="experimentOuter">
<nz-breadcrumb>
@@ -50,7 +50,7 @@
</nz-radio-group>
<nz-input-group
nzSearch
- style="width: 300px; margin-top: 15px; margin-left: 10px; margin-right: 5px;"
+ style="width: 300px; margin-top: 15px; margin-left: 10px; margin-right: 5px"
[nzAddOnAfter]="suffixIconButton"
>
<input type="text" nz-input placeholder="input search text" />
@@ -63,7 +63,7 @@
nz-button
id="openExperiment"
nzType="primary"
- style="margin-right: 5px; margin-bottom: 15px; margin-top: 15px;"
+ style="margin-right: 5px; margin-bottom: 15px; margin-top: 15px"
(click)="initModal('create')"
>
<i nz-icon nzType="plus"></i>
@@ -72,7 +72,7 @@
<button
nz-button
nzType="primary"
- style="margin-bottom: 15px; margin-top: 15px;"
+ style="margin-bottom: 15px; margin-top: 15px"
nz-popconfirm
nzTitle="Confirm to delete?"
nzCancelText="Cancel"
@@ -143,7 +143,7 @@
</tbody>
</nz-table>
</div>
- <router-outlet (experimentInfoChange)="isInfo = true"></router-outlet>
+ <router-outlet></router-outlet>
</div>
<nz-modal
[(nzVisible)]="modalProps.isVisible"
@@ -155,7 +155,7 @@
<button nz-button nzType="primary" id="customized" (click)="modalProps.formType = 'customized'">
Define your experiment
</button>
- <button nz-button nzType="primary" id="pre" (click)="modalProps.formtype = 'predefined'" [disabled]="true">
+ <button nz-button nzType="primary" id="pre" (click)="modalProps.formType = 'predefined'">
From predefined experiment library
</button>
</nz-button-group>
@@ -171,7 +171,7 @@
>
{{ modalProps.okText }}
</button>
- <button *ngIf="modalProps.currentStep > 0" nz-button nzType="default" style="float: left;" (click)="prevForm()">
+ <button *ngIf="modalProps.currentStep > 0" nz-button nzType="default" style="float: left" (click)="prevForm()">
Prev Step
</button>
</div>
@@ -181,5 +181,8 @@
[targetSpec]="targetSpec"
*ngIf="this.modalProps.formType === 'customized'"
></submarine-experiment-customized-form>
+ <submarine-experiment-predefined-form
+ *ngIf="this.modalProps.formType === 'predefined'"
+ ></submarine-experiment-predefined-form>
</nz-modal>
</nz-layout>
diff --git a/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment.component.ts b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment.component.ts
index b81741f..46cac01 100644
--- a/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment.component.ts
+++ b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment.component.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { Component, OnInit } from '@angular/core';
+import { Component, OnChanges, OnInit } from '@angular/core';
import { ActivatedRoute, NavigationStart, Router } from '@angular/router';
import { ExperimentInfo } from '@submarine/interfaces/experiment-info';
import { ExperimentSpec } from '@submarine/interfaces/experiment-spec';
@@ -30,7 +30,6 @@ import { NzMessageService } from 'ng-zorro-antd';
selector: 'submarine-experiment',
templateUrl: './experiment.component.html',
styleUrls: ['./experiment.component.scss'],
- providers: [ExperimentFormService]
})
export class ExperimentComponent implements OnInit {
experimentList: ExperimentInfo[] = [];
@@ -50,7 +49,7 @@ export class ExperimentComponent implements OnInit {
okText: 'Next step',
isVisible: false,
currentStep: 0,
- formType: null
+ formType: null,
};
nextBtnDisable: boolean = true;
@@ -63,7 +62,7 @@ export class ExperimentComponent implements OnInit {
Accepted: 'gold',
Created: 'white',
Running: 'green',
- Succeeded: 'blue'
+ Succeeded: 'blue',
};
constructor(
@@ -78,6 +77,7 @@ export class ExperimentComponent implements OnInit {
this.fetchExperimentList();
this.isInfo = this.router.url !== '/workbench/experiment';
this.experimentID = this.route.snapshot.params.id;
+
this.router.events.subscribe((val) => {
if (val instanceof NavigationStart) {
console.log(val.url);
diff --git a/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment.module.ts b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment.module.ts
index 6db316a..06b2eb7 100644
--- a/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment.module.ts
+++ b/submarine-workbench/workbench-web/src/app/pages/workbench/experiment/experiment.module.ts
@@ -12,6 +12,9 @@ import { HyperParamsComponent } from './experiment-info/hyper-params/hyper-param
import { MetricsComponent } from './experiment-info/metrics/metrics.component';
import { OutputsComponent } from './experiment-info/outputs/outputs.component';
import { ExperimentComponent } from './experiment.component';
+import { ExperimentRoutingModule } from './experiment-routing.module';
+import { ExperimentHomeComponent } from './experiment-home/experiment-home.component';
+import { ExperimentPredefinedFormComponent } from './experiment-predefined-form/experiment-predefined-form.component';
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
@@ -32,7 +35,7 @@ import { ExperimentComponent } from './experiment.component';
*/
@NgModule({
- exports: [ReactiveFormsModule, ExperimentComponent],
+ exports: [ExperimentComponent],
imports: [
ReactiveFormsModule,
NgZorroAntdModule,
@@ -40,7 +43,8 @@ import { ExperimentComponent } from './experiment.component';
FormsModule,
NgxChartsModule,
RouterModule,
- PipeSharedModule
+ PipeSharedModule,
+ ExperimentRoutingModule
],
declarations: [
ExperimentComponent,
@@ -49,7 +53,9 @@ import { ExperimentComponent } from './experiment.component';
MetricsComponent,
ChartsComponent,
OutputsComponent,
- ExperimentCustomizedFormComponent
+ ExperimentCustomizedFormComponent,
+ ExperimentHomeComponent,
+ ExperimentPredefinedFormComponent
]
})
export class ExperimentModule {}
diff --git a/submarine-workbench/workbench-web/src/app/pages/workbench/workbench-routing.module.ts b/submarine-workbench/workbench-web/src/app/pages/workbench/workbench-routing.module.ts
index 8d92fa3..c7c7313 100644
--- a/submarine-workbench/workbench-web/src/app/pages/workbench/workbench-routing.module.ts
+++ b/submarine-workbench/workbench-web/src/app/pages/workbench/workbench-routing.module.ts
@@ -57,15 +57,8 @@ const routes: Routes = [
},
{
path: 'experiment',
- component: ExperimentComponent,
- children: [
- {
- path: 'info/:id',
- component: ExperimentInfoComponent
- }
- ],
- canActivate: ['canActivatePage'],
- canActivateChild: ['canActivatePage']
+ loadChildren: () => import('./experiment/experiment.module').then((m) => m.ExperimentModule),
+ canActivate: ['canActivatePage']
},
{
path: 'environment',
diff --git a/submarine-workbench/workbench-web/src/app/pages/workbench/workbench.module.ts b/submarine-workbench/workbench-web/src/app/pages/workbench/workbench.module.ts
index bf1e748..908b48d 100644
--- a/submarine-workbench/workbench-web/src/app/pages/workbench/workbench.module.ts
+++ b/submarine-workbench/workbench-web/src/app/pages/workbench/workbench.module.ts
@@ -19,7 +19,7 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
-import { FormsModule } from '@angular/forms';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { WorkbenchRoutingModule } from '@submarine/pages/workbench/workbench-routing.module';
import { PipeSharedModule } from '@submarine/pipe/pipe-shared.module';
@@ -52,6 +52,7 @@ import { NotebookComponent } from './notebook/notebook.component';
NgZorroAntdModule,
RouterModule,
FormsModule,
+ ReactiveFormsModule,
WorkspaceModule,
ExperimentModule,
InterpreterModule,
diff --git a/submarine-workbench/workbench-web/src/app/services/experiment.form.service.ts b/submarine-workbench/workbench-web/src/app/services/experiment.form.service.ts
index f937357..9537879 100644
--- a/submarine-workbench/workbench-web/src/app/services/experiment.form.service.ts
+++ b/submarine-workbench/workbench-web/src/app/services/experiment.form.service.ts
@@ -21,7 +21,9 @@ import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
import { ModalProps } from '@submarine/interfaces/modal-props';
-@Injectable()
+@Injectable({
+ providedIn: 'root'
+})
export class ExperimentFormService {
// Subject(observable source)
private stepServiceSource = new Subject<number>();
diff --git a/submarine-workbench/workbench-web/src/app/services/experiment.service.ts b/submarine-workbench/workbench-web/src/app/services/experiment.service.ts
index 30f5735..71f8d9c 100644
--- a/submarine-workbench/workbench-web/src/app/services/experiment.service.ts
+++ b/submarine-workbench/workbench-web/src/app/services/experiment.service.ts
@@ -22,12 +22,14 @@ import { Injectable } from '@angular/core';
import { Rest } from '@submarine/interfaces';
import { ExperimentInfo } from '@submarine/interfaces/experiment-info';
import { ExperimentSpec } from '@submarine/interfaces/experiment-spec';
+import { ExperimentTemplate } from '@submarine/interfaces/experiment-template';
+import { ExperimentTemplateSubmit } from '@submarine/interfaces/experiment-template-submit';
import { BaseApiService } from '@submarine/services/base-api.service';
import { of, throwError, Observable } from 'rxjs';
import { catchError, map, switchMap } from 'rxjs/operators';
@Injectable({
- providedIn: 'root'
+ providedIn: 'root',
})
export class ExperimentService {
constructor(private baseApi: BaseApiService, private httpClient: HttpClient) {}
@@ -159,6 +161,46 @@ export class ExperimentService {
);
}
+ fetchExperimentTemplateList(): Observable<ExperimentTemplate[]> {
+ const apiUrl = this.baseApi.getRestApi('/v1/template');
+ return this.httpClient.get<Rest<ExperimentTemplate[]>>(apiUrl).pipe(
+ map((res) => {
+ if (res.success) {
+ return res.result;
+ } else {
+ throw this.baseApi.createRequestError(res.message, res.code, apiUrl, 'get');
+ }
+ })
+ );
+ }
+
+ createExperimentfromTemplate(
+ experimentSpec: ExperimentTemplateSubmit,
+ templateName: string
+ ): Observable<ExperimentInfo> {
+ const apiUrl = this.baseApi.getRestApi(`/v1/experiment/${templateName}`);
+ return this.httpClient.post<Rest<ExperimentInfo>>(apiUrl, experimentSpec).pipe(
+ map((res) => res.result),
+ catchError((e) => {
+ let message: string;
+ if (e.error instanceof ErrorEvent) {
+ // client side error
+ message = 'Something went wrong with network or workbench';
+ } else {
+ console.log(e);
+ if (e.status === 409) {
+ message = 'You might have a duplicate experiment name';
+ } else if (e.status >= 500) {
+ message = `${e.message}`;
+ } else {
+ message = e.error.message;
+ }
+ }
+ return throwError(message);
+ })
+ );
+ }
+
durationHandle(secs: number) {
const hr = Math.floor(secs / 3600);
const min = Math.floor((secs - hr * 3600) / 60);
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@submarine.apache.org
For additional commands, e-mail: dev-help@submarine.apache.org