You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@submarine.apache.org by li...@apache.org on 2020/09/22 11:47:18 UTC

[submarine] branch master updated: SUBMARINE-625. [WEB] Support running standalone script

This is an automated email from the ASF dual-hosted git repository.

liuxun 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 d5b5a26  SUBMARINE-625. [WEB] Support running standalone script
d5b5a26 is described below

commit d5b5a265765468888c0313f7434001d606dbb1b0
Author: pingsutw <pi...@gmail.com>
AuthorDate: Mon Sep 21 14:48:45 2020 +0800

    SUBMARINE-625. [WEB] Support running standalone script
    
    ### What is this PR for?
    Support running any python script in a single node.
    
    ### What type of PR is it?
    [Feature]
    
    ### Todos
    * [ ] - Task
    
    ### What is the Jira issue?
    https://issues.apache.org/jira/browse/SUBMARINE-625
    
    ### How should this be tested?
    https://travis-ci.org/github/pingsutw/hadoop-submarine/builds/727226256
    
    ### Screenshots (if appropriate)
    ![Screenshot from 2020-09-15 09-30-11](https://user-images.githubusercontent.com/37936015/93154945-88e21000-f737-11ea-8ce8-8a3def39a3b3.png)
    ![Screenshot from 2020-09-18 17-51-01](https://user-images.githubusercontent.com/37936015/93584977-b0a3d300-f9d8-11ea-97fc-ae4c309607ca.png)
    ![Screenshot from 2020-09-18 17-51-18](https://user-images.githubusercontent.com/37936015/93584978-b13c6980-f9d8-11ea-8fe0-dbd0bb780890.png)
    ![Screenshot from 2020-09-18 17-51-24](https://user-images.githubusercontent.com/37936015/93584982-b26d9680-f9d8-11ea-8b62-811051d7fd7f.png)
    ![Screenshot from 2020-09-18 17-51-32](https://user-images.githubusercontent.com/37936015/93584987-b39ec380-f9d8-11ea-8018-477ac501762a.png)
    
    ### Questions:
    * Does the licenses files need update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    Author: pingsutw <pi...@gmail.com>
    Author: Kevin Su <pi...@gmail.com>
    
    Closes #401 from pingsutw/standalone and squashes the following commits:
    
    6d5be07 [Kevin Su] Update ui
    7c6658e [pingsutw] Update test
    f1decfe [pingsutw] Update test
    ec00a93 [pingsutw] Update test
    0de9208 [pingsutw] Update test
    a786575 [pingsutw] Update test
    9c2855f [pingsutw] Update test
    f445724 [pingsutw] Update test
    f14efb2 [Kevin Su] Update ui
    a2c2eed [Kevin Su] Add standalone option
---
 .../submarine/server/api/spec/ExperimentSpec.java  |   6 +-
 .../apache/submarine/integration/experimentIT.java |  21 ++-
 .../integration/pages/ExperimentPage.java          |  51 ++++---
 .../src/app/interfaces/experiment-spec.ts          |  15 +-
 .../environment/environment.component.html         |  26 ++--
 .../experiment-customized-form.component.html      | 163 +++++++++++++++------
 .../experiment-customized-form.component.scss      |   8 +-
 .../experiment-customized-form.component.ts        | 108 +++++++++-----
 .../experiment-info/experiment-info.component.html |   8 +-
 .../experiment-info/experiment-info.component.ts   |  21 ++-
 .../experiment-info/outputs/outputs.component.html |   2 +-
 .../workbench/experiment/experiment.component.html |  14 +-
 .../workbench/experiment/experiment.component.ts   |   8 +-
 .../workbench/experiment/experiment.module.ts      |   8 +-
 .../src/app/services/environment.service.ts        |   5 +-
 .../app/services/experiment.validator.service.ts   |   4 +-
 16 files changed, 301 insertions(+), 167 deletions(-)

diff --git a/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/spec/ExperimentSpec.java b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/spec/ExperimentSpec.java
index 2a43f28..5e8b23d 100644
--- a/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/spec/ExperimentSpec.java
+++ b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/spec/ExperimentSpec.java
@@ -30,9 +30,7 @@ public class ExperimentSpec {
   private Map<String, ExperimentTaskSpec> spec;
   private CodeSpec code;
 
-  public ExperimentSpec() {
-
-  }
+  public ExperimentSpec() {}
 
   public ExperimentMeta getMeta() {
     return meta;
@@ -57,7 +55,7 @@ public class ExperimentSpec {
   public void setSpec(Map<String, ExperimentTaskSpec> spec) {
     this.spec = spec;
   }
-  
+
   public CodeSpec getCode() {
     return code;
   }
diff --git a/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/experimentIT.java b/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/experimentIT.java
index 477277e..ed8782a 100644
--- a/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/experimentIT.java
+++ b/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/experimentIT.java
@@ -61,20 +61,21 @@ public class experimentIT extends AbstractSubmarineIT {
     Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8080/workbench/experiment");
 
     // Test create new experiment
-    LOG.info("new experiment");
+    LOG.info("First step");
     experimentPage.newExperimentButtonClick();
     experimentPage.customizedBtnClick();
-    String experimentName = "experiment-e2e-test";
-    experimentPage.fillMeta(experimentName, "e2e des", "default", "python /var/tf_mnist/mnist_with_summaries.py --log_dir=/train/log --learning_rate=0.01 --batch_size=150", "gcr.io/kubeflow-ci/tf-mnist-with-summaries:1.0");
-    Assert.assertTrue(experimentPage.getGoButton().isEnabled());
-    experimentPage.goButtonClick();
-
-    LOG.info("In env");
+    experimentPage.advancedButtonCLick();
     experimentPage.envBtnClick();
-    experimentPage.fillEnv("ENV_1", "ENV1");
+    String experimentName = "experiment-e2e-test";
+    experimentPage.fillExperimentMeta(experimentName, "e2e des", "default",
+            "python /var/tf_mnist/mnist_with_summaries.py --log_dir=/train/log" +
+                    " --learning_rate=0.01 --batch_size=150",
+            "gcr.io/kubeflow-ci/tf-mnist-with-summaries:1.0",
+            "ENV_1", "ENV1");
     Assert.assertTrue(experimentPage.getGoButton().isEnabled());
     experimentPage.goButtonClick();
 
+    LOG.info("Second step");
     // Fail due to incorrect spec name
     LOG.info("In spec fail");
     experimentPage.fillTfSpec(1, new String[]{"Master"}, new int[]{-1}, new int[]{-1}, new int[]{512});
@@ -84,7 +85,11 @@ public class experimentIT extends AbstractSubmarineIT {
     experimentPage.deleteSpec();
     experimentPage.fillTfSpec(2, new String[]{"Ps", "Worker"}, new int[]{1, 1}, new int[]{1, 1}, new int[]{1024, 1024});
     Assert.assertTrue(experimentPage.getGoButton().isEnabled());
+    experimentPage.goButtonClick();
 
+    LOG.info("Preview experiment spec");
+    Assert.assertTrue(experimentPage.getGoButton().isEnabled());
+    experimentPage.goButtonClick();
   }
 
   /*
diff --git a/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/pages/ExperimentPage.java b/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/pages/ExperimentPage.java
index eefe45d..fabbdbb 100644
--- a/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/pages/ExperimentPage.java
+++ b/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/pages/ExperimentPage.java
@@ -26,6 +26,7 @@ import org.openqa.selenium.support.FindBy;
 import org.openqa.selenium.support.PageFactory;
 import org.openqa.selenium.support.pagefactory.AjaxElementLocatorFactory;
 import org.openqa.selenium.support.ui.ExpectedConditions;
+import org.openqa.selenium.support.ui.Select;
 import org.openqa.selenium.support.ui.WebDriverWait;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -38,10 +39,13 @@ public class ExperimentPage {
   private WebElement dataSection;
 
   @FindBy(id = "go")
-  private WebElement goButton;
+  private WebElement goBtn;
+
+  @FindBy(id = "advancedBtn")
+  private WebElement advancedBtn;
 
   @FindBy(id = "openExperiment")
-  private WebElement newExperimentButton;
+  private WebElement newExperimentBtn;
 
   @FindBy(id = "customized")
   private WebElement customizedBtn;
@@ -59,13 +63,13 @@ public class ExperimentPage {
   @FindBy(name = "description")
   private WebElement description;
 
-  @FindBy(name = "namespace")
+  @FindBy(id = "namespace")
   private WebElement namespace;
 
   @FindBy(name = "cmd")
   private WebElement cmd;
 
-  @FindBy(name = "image")
+  @FindBy(id = "image")
   private WebElement image;
 
   // Env form
@@ -116,16 +120,20 @@ public class ExperimentPage {
 
   // Getter
   public WebElement getGoButton() {
-    return goButton;
+    return goBtn;
   }
 
   // button click actions
   public void goButtonClick() {
-    wait.until(ExpectedConditions.elementToBeClickable(goButton)).click();
+    wait.until(ExpectedConditions.elementToBeClickable(goBtn)).click();
+  }
+
+  public void advancedButtonCLick() {
+    wait.until(ExpectedConditions.elementToBeClickable(advancedBtn)).click();
   }
 
   public void newExperimentButtonClick() {
-    wait.until(ExpectedConditions.elementToBeClickable(newExperimentButton)).click();
+    wait.until(ExpectedConditions.elementToBeClickable(newExperimentBtn)).click();
   }
 
   public void customizedBtnClick() {
@@ -141,22 +149,15 @@ public class ExperimentPage {
   }
 
   // Real actions
-  public void fillMeta(String name, String des, String namespaceStr, String cmdStr, String imageStr) {
-    experimentName.clear();
-    experimentName.sendKeys(name);
-    description.clear();
-    description.sendKeys(des);
-    namespace.clear();
-    namespace.sendKeys(namespaceStr);
-    cmd.clear();
-    cmd.sendKeys(cmdStr);
-    image.clear();
-    image.sendKeys(imageStr);
-  }
-
-  public void fillEnv(String key, String value) {
-    envKey.sendKeys(key);
-    envValue.sendKeys(value);
+  public void fillExperimentMeta(String name, String description, String namespace, String cmd, String image,
+              String envKey, String envValue) {
+    this.experimentName.clear();
+    this.experimentName.sendKeys(name);
+    this.description.clear();
+    this.description.sendKeys(description);
+    this.cmd.sendKeys(cmd);
+    this.envKey.sendKeys(envKey);
+    this.envValue.sendKeys(envValue);
   }
 
   public void deleteSpec() {
@@ -165,7 +166,8 @@ public class ExperimentPage {
     }
   }
 
-  public void fillTfSpec(int specCount, String[] inputNames, int[] replicaCount, int[] cpuCount, int[] inputMemory) {
+  public void fillTfSpec(int specCount, String[] inputNames,
+                         int[] replicaCount, int[] cpuCount, int[] inputMemory) {
     for (int i = 0; i < specCount; i++) {
       specBtnClick();
     }
@@ -183,6 +185,7 @@ public class ExperimentPage {
       replicas.get(i).sendKeys(Integer.toString(replicaCount[i]));
       cpus.get(i).clear();
       cpus.get(i).sendKeys(Integer.toString(cpuCount[i]));
+      memory.get(i).clear();
       memory.get(i).sendKeys(Integer.toString(inputMemory[i]));
     }
   }
diff --git a/submarine-workbench/workbench-web-ng/src/app/interfaces/experiment-spec.ts b/submarine-workbench/workbench-web-ng/src/app/interfaces/experiment-spec.ts
index e10ceb6..64524bb 100644
--- a/submarine-workbench/workbench-web-ng/src/app/interfaces/experiment-spec.ts
+++ b/submarine-workbench/workbench-web-ng/src/app/interfaces/experiment-spec.ts
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-export interface SpecMeta {
+export interface ExperimentMeta {
   name: string;
   description?: string;
   namespace: string;
@@ -28,7 +28,7 @@ export interface SpecMeta {
   };
 }
 
-export interface SpecEnviroment {
+export interface EnvironmentSpec {
   image: string;
 }
 
@@ -36,13 +36,16 @@ export interface Specs {
   [name: string]: {
     replicas: string;
     resources: string;
+    resourceMap?: {
+      memory: string;
+      cpu: string;
+      gpu: string;
+    };
   };
 }
 
 export interface ExperimentSpec {
-  meta: SpecMeta;
-  environment: {
-    image: string;
-  };
+  meta: ExperimentMeta;
+  environment: EnvironmentSpec;
   spec: Specs;
 }
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/environment/environment.component.html b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/environment/environment.component.html
index 8fcdd17..f750614 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/environment/environment.component.html
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/environment/environment.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="environmentOuter">
       <nz-breadcrumb>
@@ -44,7 +44,7 @@
       <div align="right">
         <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" />
@@ -57,7 +57,7 @@
           nz-button
           id="createEnvironment"
           nzType="primary"
-          style="margin-right: 5px; margin-bottom: 15px; margin-top: 15px;"
+          style="margin-right: 5px; margin-bottom: 15px; margin-top: 15px"
           (click)="initEnvForm()"
         >
           <i nz-icon nzType="plus"></i>
@@ -67,7 +67,7 @@
           nz-button
           id="deleteEnvironment"
           nzType="primary"
-          style="margin-bottom: 15px; margin-top: 15px;"
+          style="margin-bottom: 15px; margin-top: 15px"
           nz-popconfirm
           nzTitle="Confirm to delete?"
           nzCancelText="Cancel"
@@ -120,9 +120,7 @@
 <nz-modal [(nzVisible)]="isVisible" nzTitle="Create Environment" [nzWidth]="700">
   <div *nzModalFooter>
     <button nz-button nzType="default" (click)="closeModal()">Cancel</button>
-    <button id="go" nz-button nzType="primary" [disabled]="checkStatus()" (click)="createEnvironment()">
-      Create
-    </button>
+    <button id="go" nz-button nzType="primary" [disabled]="checkStatus()" (click)="createEnvironment()">Create</button>
   </div>
   <div>
     <form [formGroup]="environmentForm">
@@ -163,7 +161,7 @@
             <input required nz-input type="text" name="channel{{ i }}" id="channel{{ i }}" [formControlName]="i" />
             <i
               nz-icon
-              style="margin-left: 5px;"
+              style="margin-left: 5px"
               nzType="close-circle"
               nzTheme="fill"
               (click)="deleteItem(channels, i)"
@@ -171,10 +169,10 @@
           </div>
         </div>
       </div>
-      <div style="margin: 10px;">
+      <div style="margin: 10px">
         <button
           nz-button
-          style="display: block; margin: auto;"
+          style="display: block; margin: auto"
           id="addChannel-btn"
           type="default"
           (click)="addChannel()"
@@ -196,7 +194,7 @@
             />
             <i
               nz-icon
-              style="margin-left: 5px;"
+              style="margin-left: 5px"
               nzType="close-circle"
               nzTheme="fill"
               (click)="deleteItem(dependencies, i)"
@@ -204,11 +202,11 @@
           </div>
         </div>
       </div>
-      <div style="margin: 10px;">
+      <div style="margin: 10px">
         <button
-          style="margin-top: 10px;"
+          style="margin-top: 10px"
           nz-button
-          style="display: block; margin: auto;"
+          style="display: block; margin: auto"
           id="addDep-btn"
           type="default"
           (click)="addDependencies()"
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.html b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.html
index 493f274..8b2b58e 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.html
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.html
@@ -19,15 +19,15 @@
 
 <div>
   <nz-steps [nzCurrent]="step">
-    <nz-step nzTitle="Meta"></nz-step>
-    <nz-step nzTitle="Env"></nz-step>
-    <nz-step nzTitle="Spec"></nz-step>
+    <nz-step nzTitle="First Step"></nz-step>
+    <nz-step nzTitle="Second Step"></nz-step>
+    <nz-step nzTitle="Preview"></nz-step>
   </nz-steps>
 </div>
 <div>
   <form [formGroup]="experiment">
-    <div [ngSwitch]="step" style="margin-top: 30px;">
-      <div *ngSwitchCase="0">
+    <div [ngSwitch]="step" style="margin-top: 30px">
+      <div *ngSwitchCase="0" id="firstStep">
         <div class="single-field-group">
           <label for="experimentName">
             <span class="red-star">*</span>
@@ -36,53 +36,85 @@
           <input nz-input type="text" name="experimentName" id="experimentName" formControlName="experimentName" />
         </div>
         <div class="single-field-group">
-          <label for="description">
-            Description
-          </label>
+          <label for="description">Description</label>
           <textarea
             nz-input
-            [nzAutosize]="{ minRows: 3, maxRows: 6 }"
+            [nzAutosize]="{ minRows: 1, maxRows: 4 }"
             name="description"
             formControlName="description"
+            id="description"
           ></textarea>
         </div>
         <div class="single-field-group">
-          <label for="namespace">
-            <span class="red-star">*</span>
-            Namespace
-          </label>
-          <input nz-input name="namespace" formControlName="namespace" />
-        </div>
-        <div class="single-field-group">
           <label for="cmd">
             <span class="red-star">*</span>
             Command
           </label>
-          <input nz-input name="cmd" formControlName="cmd" placeholder="Command to run" />
+          <textarea
+            nz-input
+            name="cmd"
+            formControlName="cmd"
+            placeholder="Command to run"
+            id="cmd"
+            [nzAutosize]="{ minRows: 2, maxRows: 4 }"
+          ></textarea>
         </div>
         <div class="single-field-group">
           <label for="image">
             <span class="red-star">*</span>
             Image
           </label>
-          <input nz-input name="image" formControlName="image" placeholder="Image to use" />
+          <nz-select
+            nzShowSearch
+            [nzDropdownRender]="renderTemplate"
+            formControlName="image"
+            id="image"
+          >
+            <nz-option *ngFor="let image of imageList" [nzValue]="image" [nzLabel]="image"></nz-option>
+          </nz-select>
+          <ng-template #renderTemplate>
+            <nz-divider></nz-divider>
+            <div class="container">
+              <input type="text" nz-input #inputElement />
+              <a class="add-item" (click)="addItem(inputElement)">
+                <i nz-icon nzType="plus"></i>
+                Add item
+              </a>
+            </div>
+          </ng-template>
         </div>
-      </div>
-      <div *ngSwitchCase="1" id="page2">
-        <div>
-          <button nz-button id="env-btn" type="default" (click)="onCreateEnv()">
-            Add new env
+        <div style="margin-bottom: 10px">
+          <button
+            nz-button
+            style="display: block; margin: auto"
+            id="advancedBtn"
+            nzType="default"
+            (click)="ADVANCED = !ADVANCED"
+          >
+            Advanced
+            <i nz-icon [nzType]="ADVANCED ? 'up' : 'down'"></i>
           </button>
+        </div>
+        <div *ngIf="ADVANCED" class="single-field-group">
+          <label for="namespace">
+            <span class="red-star">*</span>
+            Namespace
+          </label>
+          <nz-select formControlName="namespace" id="namespace">
+            <nz-option *ngFor="let namespace of nameSpaceList" [nzValue]="namespace" [nzLabel]="namespace"></nz-option>
+          </nz-select>
+        </div>
+        <div *ngIf="ADVANCED">
           <ul formArrayName="envs" class="list-container">
             <ng-container *ngFor="let env of envs.controls; index as i">
-              <li *ngIf="i | indexInRange: currentEnvPage:PAGESIZE" [formGroupName]="i" class="input-group">
-                <div>
+              <li [formGroupName]="i" class="input-group">
+                <div style="margin-left: 30%">
                   <label for="key{{ i }}">Key</label>
-                  <input nz-input name="key{{ i }}" placeholder="Key" formControlName="key" />
+                  <input nz-input name="key{{ i }}" placeholder="Key" formControlName="key" id="key{{ i }}" />
                 </div>
-                <div>
+                <div style="margin-left: 0">
                   <label for="value{{ i }}">Value</label>
-                  <input nz-input name="value{{ i }}" placeholder="Value" formControlName="value" />
+                  <input nz-input name="value{{ i }}" placeholder="Value" formControlName="value" id="value{{ i }}" />
                 </div>
                 <i nz-icon nzType="close-circle" nzTheme="fill" (click)="deleteItem(envs, i)"></i>
               </li>
@@ -100,29 +132,37 @@
               </div>
             </ng-container>
           </ul>
-          <nz-pagination
-            [(nzPageIndex)]="currentEnvPage"
-            [nzPageSize]="PAGESIZE"
-            [nzTotal]="envs.controls.length"
-            nzSimple
-          ></nz-pagination>
+          <button nz-button id="env-btn" style="display: block; margin: auto" nzType="primary" (click)="onCreateEnv()">
+            Add new environment variable
+          </button>
         </div>
       </div>
-      <div *ngSwitchCase="2">
-        <nz-radio-group [(ngModel)]="jobTypes" [ngModelOptions]="{ standalone: true }" id="jobs-container">
-          <label nz-radio nzValue="Tensorflow">Distributed Tensorflow</label>
-          <label nz-radio nzValue="Pytorch">Distributed PyTorch</label>
+      <div *ngSwitchCase="1" id="secondStep">
+        <nz-radio-group [(ngModel)]="framework" [ngModelOptions]="{ standalone: true }" id="jobs-container">
+          <label nz-radio nzValue="Tensorflow" (click)="deleteAllItem(specs); jobTypes = 'Distributed Tensorflow'">
+            Distributed Tensorflow
+          </label>
+          <label nz-radio nzValue="Pytorch" (click)="deleteAllItem(specs); jobTypes = 'Distributed Pytorch'">
+            Distributed PyTorch
+          </label>
+          <label
+            nz-radio
+            nzValue="Standalone"
+            (click)="deleteAllItem(specs); onCreateSpec(); jobTypes = 'Standalone Script'"
+          >
+            Standalone Script
+          </label>
         </nz-radio-group>
         <label class="pg3-form-label"></label>
-        <button nz-button id="spec-btn" type="default" (click)="onCreateSpec()">
+        <button *ngIf="framework !== 'Standalone'" nz-button id="spec-btn" nzType="default" (click)="onCreateSpec()">
           Add new spec
         </button>
         <ul formArrayName="specs" class="list-container">
           <ng-container *ngFor="let spec of specs.controls; index as i">
             <li *ngIf="i | indexInRange: currentSpecPage:PAGESIZE" [formGroupName]="i" class="input-group">
-              <div id="spec{{ i }}">
+              <div id="spec{{ i }}" *ngIf="framework !== 'Standalone'">
                 <label>Spec name</label>
-                <nz-select formControlName="name" nzPlaceHolder="Spec name" [ngSwitch]="jobTypes">
+                <nz-select formControlName="name" nzPlaceHolder="Spec name" [ngSwitch]="framework">
                   <div *ngSwitchCase="'Tensorflow'">
                     <nz-option *ngFor="let spec of TF_SPECNAMES" [nzValue]="spec" [nzLabel]="spec"></nz-option>
                   </div>
@@ -131,7 +171,7 @@
                   </div>
                 </nz-select>
               </div>
-              <div>
+              <div *ngIf="framework !== 'Standalone'">
                 <label>Number of Replica</label>
                 <input
                   nz-input
@@ -145,10 +185,21 @@
                 <label>Number of cpu</label>
                 <input nz-input name="cpu{{ i }}" type="number" placeholder="number of cpu" formControlName="cpus" />
               </div>
+              <div>
+                <label>Number of gpu</label>
+                <input nz-input name="gpu{{ i }}" type="number" placeholder="number of gpu" formControlName="gpus" />
+              </div>
               <div id="memory{{ i }}">
                 <label>Memory</label>
                 <div formGroupName="memory" class="memory-input-group">
-                  <input nz-input name="memory{{ i }}" placeholder="Enter number" formControlName="num" />
+                  <input
+                    nz-input
+                    name="memory{{ i }}"
+                    type="number"
+                    step="1024"
+                    placeholder="Enter number"
+                    formControlName="num"
+                  />
                   <nz-select formControlName="unit">
                     <nz-option *ngFor="let unit of MEMORY_UNITS" [nzValue]="unit" [nzLabel]="unit"></nz-option>
                   </nz-select>
@@ -162,6 +213,7 @@
             >
               {{ spec.getError('specError') }}
             </div>
+
             <div
               class="alert-message"
               *ngIf="spec.get('name').dirty | logicalAnd: spec.get('name').hasError('duplicateError')"
@@ -183,6 +235,9 @@
             <div class="alert-message" *ngIf="spec.get('cpus').dirty | logicalAnd: spec.get('cpus').hasError('min')">
               cpus must be at least 1
             </div>
+            <div class="alert-message" *ngIf="spec.get('gpus').dirty | logicalAnd: spec.get('gpus').hasError('min')">
+              cpus must be at least 0
+            </div>
           </ng-container>
         </ul>
         <nz-pagination
@@ -192,6 +247,28 @@
           nzSimple
         ></nz-pagination>
       </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="Command" [nzSpan]="2">
+            {{ finialExperimentSpec.meta.cmd }}
+          </nz-descriptions-item>
+          <nz-descriptions-item nzTitle="Image" [nzSpan]="2">
+            {{ finialExperimentSpec.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">
+              {{ item.key }}={{ item.value }}{{ isLast ? '' : ', ' }}
+            </span>
+          </nz-descriptions-item>
+          <div *ngFor="let item of finialExperimentSpec.spec | keyvalue">
+            <nz-descriptions-item nzTitle="{{ item.key }}" [nzSpan]="2">
+              {{ item.value.resources }}
+            </nz-descriptions-item>
+          </div>
+        </nz-descriptions>
+      </div>
     </div>
   </form>
 </div>
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.scss b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.scss
index adf39b2..a158387 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.scss
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.scss
@@ -78,7 +78,7 @@ textarea.ng-invalid.ng-touched {
  }
 
  & > div:nth-child(1), & > div:nth-child(4) {
-    flex: 0 1 25%;
+    flex: 0 1 20%;
  }
 
  & i {
@@ -112,14 +112,12 @@ textarea.ng-invalid.ng-touched {
        margin-right: 1.5rem;
     }
  }
- & input {
+ & input, nz-select, textarea {
     flex: 0 0 48%;
  }
- & textarea {
-    flex: 0 0 55%;
- }
 }
 
+
 .alert-message {
  color: red;
  margin-top: .3rem;
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.ts b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.ts
index 1022e71..2a8b8a4 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.ts
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.ts
@@ -17,41 +17,56 @@
  * under the License.
  */
 
-import { Component, OnInit, Input, OnDestroy } from '@angular/core';
+import { Component, Input, OnDestroy, OnInit } from '@angular/core';
 import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
-import { ExperimentSpec, Specs, SpecEnviroment, SpecMeta } from '@submarine/interfaces/experiment-spec';
+import { EnvironmentSpec, ExperimentMeta, ExperimentSpec, Specs } from '@submarine/interfaces/experiment-spec';
+import { ExperimentFormService } from '@submarine/services/experiment.form.service';
 import { ExperimentService } from '@submarine/services/experiment.service';
 import { ExperimentValidatorService } from '@submarine/services/experiment.validator.service';
-import { NzMessageService } from 'ng-zorro-antd';
 import { nanoid } from 'nanoid';
+import { NzMessageService } from 'ng-zorro-antd';
 import { Subscription } from 'rxjs';
-import { ExperimentFormService } from '@submarine/services/experiment.form.service';
 
 @Component({
-  selector: 'experiment-customized-form',
+  selector: 'submarine-experiment-customized-form',
   templateUrl: './experiment-customized-form.component.html',
   styleUrls: ['./experiment-customized-form.component.scss']
 })
-export class ExperimentCustomizedForm implements OnInit, OnDestroy {
+export class ExperimentCustomizedFormComponent implements OnInit, OnDestroy {
   @Input() mode: 'create' | 'update' | 'clone';
 
   // About new experiment
   experiment: FormGroup;
+  finialExperimentSpec: ExperimentSpec;
   step: number = 0;
   subscriptions: Subscription[] = [];
 
+  // TODO: Fetch all namespaces from submarine server
+  defaultNameSpace = 'default';
+  nameSpaceList = [this.defaultNameSpace, 'submarine'];
+
+  // TODO: Fetch all images from submarine server
+  imageIndex = 0;
+  defaultImage = 'gcr.io/kubeflow-ci/tf-mnist-with-summaries:1.0'
+  imageList = [this.defaultImage];
+
   // Constants
   TF_SPECNAMES = ['Master', 'Worker', 'Ps'];
   PYTORCH_SPECNAMES = ['Master', 'Worker'];
+  defaultSpecName = 'worker';
   MEMORY_UNITS = ['M', 'G'];
-  TOTAL_STEPS = 2;
+
+  SECOND_STEP = 1;
+  PREVIEW_STEP = 2;
+  ADVANCED = false;
 
   // About env page
   currentEnvPage = 1;
   PAGESIZE = 5;
 
   // About spec
-  jobTypes = 'Tensorflow';
+  jobTypes = 'Distributed Tensorflow';
+  framework = 'Tensorflow';
   currentSpecPage = 1;
 
   // About update
@@ -69,9 +84,9 @@ export class ExperimentCustomizedForm implements OnInit, OnDestroy {
     this.experiment = new FormGroup({
       experimentName: new FormControl(null, Validators.required),
       description: new FormControl(null, [Validators.required]),
-      namespace: new FormControl('default', [Validators.required]),
+      namespace: new FormControl(this.defaultNameSpace, [Validators.required]),
       cmd: new FormControl('', [Validators.required]),
-      image: new FormControl('', [Validators.required]),
+      image: new FormControl(this.defaultImage, [Validators.required]),
       envs: new FormArray([], [this.experimentValidatorService.nameValidatorFactory('key')]),
       specs: new FormArray([], [this.experimentValidatorService.nameValidatorFactory('name')])
     });
@@ -90,8 +105,11 @@ export class ExperimentCustomizedForm implements OnInit, OnDestroy {
 
     const sub2 = this.experimentFormService.stepService.subscribe((n) => {
       if (n > 0) {
-        if (this.step === this.TOTAL_STEPS) {
+        if (this.step === this.PREVIEW_STEP) {
           this.handleSubmit();
+        } else if (this.step === this.SECOND_STEP) {
+          this.onPreview();
+          this.step += 1;
         } else {
           this.step += 1;
         }
@@ -100,7 +118,7 @@ export class ExperimentCustomizedForm implements OnInit, OnDestroy {
       }
       // Send the current step and okText back to parent
       this.experimentFormService.modalPropsChange({
-        okText: this.step !== this.TOTAL_STEPS ? 'Next step' : 'Submit',
+        okText: this.step !== this.PREVIEW_STEP ? 'Next step' : 'Submit',
         currentStep: this.step
       });
       // Run check after step is changed
@@ -117,6 +135,13 @@ export class ExperimentCustomizedForm implements OnInit, OnDestroy {
     });
   }
 
+  addItem(input: HTMLInputElement): void {
+    const value = input.value;
+    if (this.imageList.indexOf(value) === -1) {
+      this.imageList = [...this.imageList, input.value || `New item ${this.imageIndex++}`];
+    }
+  }
+
   // Getters of experiment request form
   get experimentName() {
     return this.experiment.get('experimentName');
@@ -141,7 +166,7 @@ export class ExperimentCustomizedForm implements OnInit, OnDestroy {
   }
 
   /**
-   * Reset properies in parent component when the form is about to closed
+   * Reset properties in parent component when the form is about to closed
    */
   closeModal() {
     this.experimentFormService.modalPropsClear();
@@ -153,22 +178,25 @@ export class ExperimentCustomizedForm implements OnInit, OnDestroy {
   checkStatus() {
     if (this.step === 0) {
       this.experimentFormService.btnStatusChange(
-        this.experimentName.invalid || this.namespace.invalid || this.cmd.invalid || this.image.invalid
+        this.experimentName.invalid ||
+          this.namespace.invalid ||
+          this.cmd.invalid ||
+          this.image.invalid ||
+          this.envs.invalid
       );
     } else if (this.step === 1) {
-      this.experimentFormService.btnStatusChange(this.envs.invalid);
-    } else if (this.step === this.TOTAL_STEPS) {
       this.experimentFormService.btnStatusChange(this.specs.invalid);
     }
   }
-
+  onPreview() {
+    this.finialExperimentSpec = this.constructSpec();
+  }
   /**
    * Event handler for Next step/Submit button
    */
   handleSubmit() {
     if (this.mode === 'create') {
-      const newSpec = this.constructSpec();
-      this.experimentService.createExperiment(newSpec).subscribe({
+      this.experimentService.createExperiment(this.finialExperimentSpec).subscribe({
         next: () => {},
         error: (msg) => {
           this.nzMessageService.error(`${msg}, please try again`, {
@@ -182,8 +210,7 @@ export class ExperimentCustomizedForm implements OnInit, OnDestroy {
         }
       });
     } else if (this.mode === 'update') {
-      const newSpec = this.constructSpec();
-      this.experimentService.updateExperiment(this.targetId, newSpec).subscribe(
+      this.experimentService.updateExperiment(this.targetId, this.finialExperimentSpec).subscribe(
         null,
         (msg) => {
           this.nzMessageService.error(`${msg}, please try again`, {
@@ -197,8 +224,7 @@ export class ExperimentCustomizedForm implements OnInit, OnDestroy {
         }
       );
     } else if (this.mode === 'clone') {
-      const newSpec = this.constructSpec();
-      this.experimentService.createExperiment(newSpec).subscribe(
+      this.experimentService.createExperiment(this.finialExperimentSpec).subscribe(
         null,
         (msg) => {
           this.nzMessageService.error(`${msg}, please try again`, {
@@ -231,10 +257,11 @@ export class ExperimentCustomizedForm implements OnInit, OnDestroy {
    * Create a new spec
    */
   createSpec(
-    defaultName: string = '',
+    defaultName: string = 'Worker',
     defaultReplica: number = 1,
     defaultCpu: number = 1,
-    defaultMemory: string = '',
+    defaultGpu: number = 0,
+    defaultMemory: number = 1024,
     defaultUnit: string = 'M'
   ): FormGroup {
     return new FormGroup(
@@ -242,6 +269,7 @@ export class ExperimentCustomizedForm implements OnInit, OnDestroy {
         name: new FormControl(defaultName, [Validators.required]),
         replicas: new FormControl(defaultReplica, [Validators.min(1), Validators.required]),
         cpus: new FormControl(defaultCpu, [Validators.min(1), Validators.required]),
+        gpus: new FormControl(defaultGpu, [Validators.min(0), Validators.required]),
         memory: new FormGroup(
           {
             num: new FormControl(defaultMemory, [Validators.required]),
@@ -283,10 +311,10 @@ export class ExperimentCustomizedForm implements OnInit, OnDestroy {
    */
   constructSpec(): ExperimentSpec {
     // Construct the spec
-    const meta: SpecMeta = {
+    const meta: ExperimentMeta = {
       name: this.experimentName.value,
       namespace: this.namespace.value,
-      framework: this.jobTypes,
+      framework: this.framework === 'Standalone' ? 'Tensorflow' : this.framework,
       cmd: this.cmd.value,
       envVars: {}
     };
@@ -301,14 +329,14 @@ export class ExperimentCustomizedForm implements OnInit, OnDestroy {
       if (spec.get('name').value) {
         specs[spec.get('name').value] = {
           replicas: spec.get('replicas').value,
-          resources: `cpu=${spec.get('cpus').value},memory=${spec.get('memory').get('num').value}${
-            spec.get('memory').get('unit').value
-          }`
+          resources: `cpu=${spec.get('cpus').value},nvidia.com/gpu=${spec.get('gpus').value},memory=${
+            spec.get('memory').get('num').value
+          }${spec.get('memory').get('unit').value}`
         };
       }
     }
 
-    const environment: SpecEnviroment = {
+    const environment: EnvironmentSpec = {
       image: this.image.value
     };
 
@@ -331,6 +359,10 @@ export class ExperimentCustomizedForm implements OnInit, OnDestroy {
     arr.removeAt(index);
   }
 
+  deleteAllItem(arr: FormArray) {
+    arr.clear();
+  }
+
   updateExperimentInit() {
     // Prevent user from modifying the name
     this.experimentName.disable();
@@ -349,6 +381,7 @@ export class ExperimentCustomizedForm implements OnInit, OnDestroy {
     const cloneExperimentName = spec.meta.name + '-' + id;
     this.experimentName.setValue(cloneExperimentName.toLocaleLowerCase());
     this.cloneExperiment(spec);
+    this.checkStatus();
   }
 
   cloneExperiment(spec: ExperimentSpec) {
@@ -361,8 +394,17 @@ export class ExperimentCustomizedForm implements OnInit, OnDestroy {
       this.envs.push(env);
     }
     for (const [specName, info] of Object.entries(spec.spec)) {
-      const [cpuCount, memory, unit] = info.resources.match(/\d+|[MG]/g);
-      const newSpec = this.createSpec(specName, parseInt(info.replicas, 10), parseInt(cpuCount, 10), memory, unit);
+      const cpuCount = info.resourceMap.cpu;
+      const gpuCount = info.resourceMap.gpu === undefined ? '0' : '1';
+      const [memory, unit] = info.resourceMap.memory.match(/\d+|[MG]/g);
+      const newSpec = this.createSpec(
+        specName,
+        parseInt(info.replicas, 10),
+        parseInt(cpuCount, 10),
+        parseInt(gpuCount, 10),
+        parseInt(memory, 10),
+        unit
+      );
       this.specs.push(newSpec);
     }
   }
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-info/experiment-info.component.html b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-info/experiment-info.component.html
index a076514..699523a 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-info/experiment-info.component.html
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-info/experiment-info.component.html
@@ -19,7 +19,7 @@
 
 <nz-table
   *ngIf="!isLoading"
-  style="margin-top: 10px;"
+  style="margin-top: 10px"
   id="experimentTable"
   #basicTable
   [nzData]="[experimentInfo]"
@@ -46,7 +46,9 @@
       <td>{{ experimentInfo.runningTime | date: 'M/d/yyyy, h:mm a' }}</td>
       <td>{{ experimentInfo.finishedTime | date: 'M/d/yyyy, h:mm a' }}</td>
       <td>{{ experimentInfo.duration }}</td>
-      <td>{{ experimentInfo.status }}</td>
+      <td>
+        <nz-tag [nzColor]="statusColor[experimentInfo.status]">{{ experimentInfo.status }}</nz-tag>
+      </td>
       <td class="td-action">
         <a (click)="startExperiment()">Start</a>
         <nz-divider nzType="vertical"></nz-divider>
@@ -75,7 +77,7 @@
   </tbody>
 </nz-table>
 <nz-spin *ngIf="isLoading"></nz-spin>
-<div style="background-color: white;">
+<div style="background-color: white">
   <nz-select id="nzSelect_selectPod" [(ngModel)]="selectedPod">
     <nz-option *ngFor="let pod of podNameArr" [nzValue]="pod" [nzLabel]="pod"></nz-option>
   </nz-select>
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-info/experiment-info.component.ts b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-info/experiment-info.component.ts
index a31ae34..c00b2ee 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-info/experiment-info.component.ts
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-info/experiment-info.component.ts
@@ -46,21 +46,28 @@ export class ExperimentInfoComponent implements OnInit {
     private nzMessageService: NzMessageService
   ) {}
 
+  statusColor: { [key: string]: string } = {
+    Accepted: 'gold',
+    Created: 'white',
+    Running: 'green',
+    Succeeded: 'blue'
+  };
+
   ngOnInit() {
     this.experimentID = this.route.snapshot.params.id;
     this.experimentService.querySpecificExperiment(this.experimentID).subscribe(
       (item) => {
         this.experimentInfo = item;
         this.isLoading = false;
-        if (this.experimentInfo.status == 'Succeeded') {
-          var finTime = new Date(this.experimentInfo.finishedTime);
-          var runTime = new Date(this.experimentInfo.runningTime);
-          var result = (finTime.getTime() - runTime.getTime()) / 1000;
+        if (this.experimentInfo.status === 'Succeeded') {
+          const finTime = new Date(this.experimentInfo.finishedTime);
+          const runTime = new Date(this.experimentInfo.runningTime);
+          const result = (finTime.getTime() - runTime.getTime()) / 1000;
           this.experimentInfo.duration = this.experimentService.durationHandle(result);
         } else {
-          var currentTime = new Date();
-          var runTime = new Date(this.experimentInfo.runningTime);
-          var result = (currentTime.getTime() - runTime.getTime()) / 1000;
+          const currentTime = new Date();
+          const runTime = new Date(this.experimentInfo.runningTime);
+          const result = (currentTime.getTime() - runTime.getTime()) / 1000;
           this.experimentInfo.duration = this.experimentService.durationHandle(result);
         }
       },
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-info/outputs/outputs.component.html b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-info/outputs/outputs.component.html
index 4ee4d33..81889d8 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-info/outputs/outputs.component.html
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-info/outputs/outputs.component.html
@@ -18,5 +18,5 @@
   -->
 
 <div id="showLogDiv">
-  <p style="white-space: nowrap;" *ngFor="let log of podLog; let j = index">[{{ j }}] {{ log }}</p>
+  <p style="white-space: nowrap" *ngFor="let log of podLog; let j = index">[{{ j }}] {{ log }}</p>
 </div>
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.html b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.html
index 30a750a..b23e2c4 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.html
+++ b/submarine-workbench/workbench-web-ng/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>
@@ -49,7 +49,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" />
@@ -62,7 +62,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>
@@ -71,7 +71,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"
@@ -170,15 +170,15 @@
       >
         {{ 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>
-    <experiment-customized-form
+    <submarine-experiment-customized-form
       [mode]="mode"
       [targetId]="targetId"
       [targetSpec]="targetSpec"
       *ngIf="this.modalProps.formType === 'customized'"
-    ></experiment-customized-form>
+    ></submarine-experiment-customized-form>
   </nz-modal>
 </nz-layout>
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.ts b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.ts
index 4732450..b81741f 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.ts
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.ts
@@ -20,11 +20,11 @@
 import { Component, OnInit } from '@angular/core';
 import { ActivatedRoute, NavigationStart, Router } from '@angular/router';
 import { ExperimentInfo } from '@submarine/interfaces/experiment-info';
-import { ExperimentService } from '@submarine/services/experiment.service';
-import { NzMessageService } from 'ng-zorro-antd';
 import { ExperimentSpec } from '@submarine/interfaces/experiment-spec';
-import { ExperimentFormService } from '@submarine/services/experiment.form.service';
 import { ModalProps } from '@submarine/interfaces/modal-props';
+import { ExperimentFormService } from '@submarine/services/experiment.form.service';
+import { ExperimentService } from '@submarine/services/experiment.service';
+import { NzMessageService } from 'ng-zorro-antd';
 
 @Component({
   selector: 'submarine-experiment',
@@ -114,7 +114,7 @@ export class ExperimentComponent implements OnInit {
     this.modalProps.isVisible = true;
     this.modalProps.formType = initFormType;
 
-    if (initMode === 'update') {
+    if (initMode === 'update' || initMode === 'clone') {
       // Keep id for later request
       this.targetId = id;
       this.targetSpec = spec;
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.module.ts b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.module.ts
index c5563d2..6db316a 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.module.ts
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.module.ts
@@ -1,17 +1,17 @@
 import { CommonModule } from '@angular/common';
 import { NgModule } from '@angular/core';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { RouterModule } from '@angular/router';
+import { PipeSharedModule } from '@submarine/pipe/pipe-shared.module';
 import { NgxChartsModule } from '@swimlane/ngx-charts';
 import { NgZorroAntdModule } from 'ng-zorro-antd';
+import { ExperimentCustomizedFormComponent } from './experiment-customized-form/experiment-customized-form.component';
 import { ChartsComponent } from './experiment-info/charts/charts.component';
 import { ExperimentInfoComponent } from './experiment-info/experiment-info.component';
 import { HyperParamsComponent } from './experiment-info/hyper-params/hyper-params.component';
 import { MetricsComponent } from './experiment-info/metrics/metrics.component';
 import { OutputsComponent } from './experiment-info/outputs/outputs.component';
-import { ExperimentCustomizedForm } from './experiment-customized-form/experiment-customized-form.component';
-import { PipeSharedModule } from '@submarine/pipe/pipe-shared.module';
 import { ExperimentComponent } from './experiment.component';
-import { RouterModule } from '@angular/router';
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
@@ -49,7 +49,7 @@ import { RouterModule } from '@angular/router';
     MetricsComponent,
     ChartsComponent,
     OutputsComponent,
-    ExperimentCustomizedForm
+    ExperimentCustomizedFormComponent
   ]
 })
 export class ExperimentModule {}
diff --git a/submarine-workbench/workbench-web-ng/src/app/services/environment.service.ts b/submarine-workbench/workbench-web-ng/src/app/services/environment.service.ts
index 9a14d3d..07001fd 100644
--- a/submarine-workbench/workbench-web-ng/src/app/services/environment.service.ts
+++ b/submarine-workbench/workbench-web-ng/src/app/services/environment.service.ts
@@ -22,9 +22,8 @@ import { Injectable } from '@angular/core';
 import { Rest } from '@submarine/interfaces';
 import { Environment } from '@submarine/interfaces/environment-info';
 import { BaseApiService } from '@submarine/services/base-api.service';
-import { of, throwError, Observable } from 'rxjs';
-import { catchError, map, switchMap } from 'rxjs/operators';
-import { EnvironmentSpec } from '@submarine/interfaces/environment-spec';
+import { of, Observable } from 'rxjs';
+import { switchMap } from 'rxjs/operators';
 
 @Injectable({
   providedIn: 'root'
diff --git a/submarine-workbench/workbench-web-ng/src/app/services/experiment.validator.service.ts b/submarine-workbench/workbench-web-ng/src/app/services/experiment.validator.service.ts
index d15ca2a..a2716fb 100644
--- a/submarine-workbench/workbench-web-ng/src/app/services/experiment.validator.service.ts
+++ b/submarine-workbench/workbench-web-ng/src/app/services/experiment.validator.service.ts
@@ -70,7 +70,9 @@ export class ExperimentValidatorService {
       for (let i = 0; i < arr.length; i++) {
         const nameControl = arr.controls[i].get(fieldName);
         // We don't consider empty string
-        if (!nameControl.value) continue;
+        if (!nameControl.value) {
+          continue;
+        }
 
         if (duplicateSet.has(nameControl.value)) {
           // Found duplicates, manually set errors on FormControl level


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@submarine.apache.org
For additional commands, e-mail: dev-help@submarine.apache.org