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