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/09/05 06:45:18 UTC

[submarine] branch master updated: SUBMARINE-610. [WEB] Refactor experiment.component

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 6dec6e1  SUBMARINE-610. [WEB] Refactor experiment.component
6dec6e1 is described below

commit 6dec6e13f229c6bc216c785bbaed827c10cefd30
Author: wang0630 <j2...@gmail.com>
AuthorDate: Fri Sep 4 13:43:49 2020 +0800

    SUBMARINE-610. [WEB] Refactor experiment.component
    
    ### What is this PR for?
    Separate form in `experiment.component` to another file, and using RXJS `Subject` and `Observable` to communicate between components.
    
    ### What type of PR is it?
    [Refactoring]
    
    ### Todos
    
    ### What is the Jira issue?
    https://issues.apache.org/jira/browse/SUBMARINE-610?filter=-1
    
    ### How should this be tested?
    https://github.com/wang0630/submarine/runs/1046727907
    
    ### Screenshots (if appropriate)
    ![Aug-30-2020 11-44-15](https://user-images.githubusercontent.com/26138982/91650772-2ee61700-eab6-11ea-91c5-f92307df8126.gif)
    
    ### Questions:
    * Does the licenses files need update? No
    * Is there breaking changes for older versions?No
    * Does this needs documentation? No
    
    Author: wang0630 <j2...@gmail.com>
    
    Closes #388 from wang0630/SUBMARINE-610 and squashes the following commits:
    
    f446696 [wang0630] Fix typo
    71b850d [wang0630] Add todo in experimentIT.java
    5692537 [wang0630] Fix clone btn
    fd430c2 [wang0630] Add styling
    f3ad296 [wang0630] Remove unnecessary style
    7ccc16b [wang0630] Add license
    0c53590 [wang0630] Add corresponding tests
    ead507a [wang0630] Refactor form to individual component
    2a56a29 [wang0630] Refactor form finished, check manually
    982f9e9 [wang0630] Before using ViewChild
    e2ce531 [wang0630] Refactor services
    d011029 [wang0630] Refactor experiment page
    b151a42 [wang0630] Files are organized, before launching
    ce12d3c [wang0630] Before refactor component
    e6962fb [wang0630] Add radio button for job types
---
 .../apache/submarine/integration/experimentIT.java |  30 +-
 .../integration/pages/ExperimentPage.java          |   7 +-
 .../src/app/interfaces/modal-props.ts              |  25 ++
 .../experiment-customized-form.component.html      | 197 +++++++++++
 .../experiment-customized-form.component.scss      | 134 ++++++++
 .../experiment-customized-form.component.ts        | 369 +++++++++++++++++++++
 .../workbench/experiment/experiment.component.html | 257 ++------------
 .../workbench/experiment/experiment.component.scss |  98 +-----
 .../workbench/experiment/experiment.component.ts   | 366 +++-----------------
 .../workbench/experiment/experiment.module.ts      |  27 +-
 .../src/app/pages/workbench/workbench.module.ts    |   1 -
 .../src/app/services/experiment.form.service.ts    |  60 ++++
 .../app/services/experiment.validator.service.ts   |   7 +-
 13 files changed, 925 insertions(+), 653 deletions(-)

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 356f481..477277e 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
@@ -27,13 +27,6 @@ import org.testng.Assert;
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
-import org.testng.Assert;
-import org.openqa.selenium.support.ui.WebDriverWait;
-import org.openqa.selenium.support.ui.ExpectedConditions;
-import sun.rmi.runtime.Log;
-import org.apache.submarine.CommandExecutor;
-import org.apache.submarine.ProcessData;
-import java.io.File;
 
 public class experimentIT extends AbstractSubmarineIT {
 
@@ -41,7 +34,7 @@ public class experimentIT extends AbstractSubmarineIT {
 
   @BeforeClass
   public static void startUp(){
-    LOG.info("[Testcase]: experimentNew");
+    LOG.info("[Test case]: experimentNew");
     driver = WebDriverManager.getWebDriver();
   }
 
@@ -52,7 +45,7 @@ public class experimentIT extends AbstractSubmarineIT {
 
   @Test
   public void experimentNavigation() throws Exception {
-    LOG.info("[Testacse: experimentNavigation]");
+    LOG.info("[Test case]: experimentNavigation]");
     // Init the page object
     ExperimentPage experimentPage = new ExperimentPage(driver);
     // Login
@@ -69,8 +62,9 @@ public class experimentIT extends AbstractSubmarineIT {
 
     // Test create new experiment
     LOG.info("new experiment");
-    String experimentName = "experiment-e2e-test";
     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();
@@ -90,15 +84,17 @@ 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());
-    /*
+
+  }
+
+  /*
       TODO: Launch submarine server and K8s in e2e-test
       Comment out because of Experiment creation failure on Travis
 
-      experimentPage.goButtonClick();
+      @Test
+      public void updateExperiment() {
+        ....
+      }
+  */
 
-      // Patch request
-      LOG.info("In spec patch");
-      experimentPage.editTfSpec(experimentName);
-    */
-  }
 }
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 6e70db0..eefe45d 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
@@ -34,7 +34,6 @@ import java.util.List;
 
 public class ExperimentPage {
 
-
   @FindBy(id = "experimentData")
   private WebElement dataSection;
 
@@ -44,6 +43,8 @@ public class ExperimentPage {
   @FindBy(id = "openExperiment")
   private WebElement newExperimentButton;
 
+  @FindBy(id = "customized")
+  private WebElement customizedBtn;
   /*
   * For svg/path/g element tag, we must use //*[name = 'svg'] to select
   * //svg will fail
@@ -127,6 +128,10 @@ public class ExperimentPage {
     wait.until(ExpectedConditions.elementToBeClickable(newExperimentButton)).click();
   }
 
+  public void customizedBtnClick() {
+    wait.until(ExpectedConditions.elementToBeClickable(customizedBtn)).click();
+  }
+
   public void envBtnClick() {
     wait.until(ExpectedConditions.elementToBeClickable(envBtn)).click();
   }
diff --git a/submarine-workbench/workbench-web-ng/src/app/interfaces/modal-props.ts b/submarine-workbench/workbench-web-ng/src/app/interfaces/modal-props.ts
new file mode 100644
index 0000000..6202e97
--- /dev/null
+++ b/submarine-workbench/workbench-web-ng/src/app/interfaces/modal-props.ts
@@ -0,0 +1,25 @@
+/*!
+ * 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 ModalProps {
+  okText?: string;
+  isVisible?: boolean;
+  currentStep?: number;
+  formType?: 'customized' | 'predefined';
+}
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
new file mode 100644
index 0000000..493f274
--- /dev/null
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.html
@@ -0,0 +1,197 @@
+<!--
+  ~ 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.
+-->
+
+<div>
+  <nz-steps [nzCurrent]="step">
+    <nz-step nzTitle="Meta"></nz-step>
+    <nz-step nzTitle="Env"></nz-step>
+    <nz-step nzTitle="Spec"></nz-step>
+  </nz-steps>
+</div>
+<div>
+  <form [formGroup]="experiment">
+    <div [ngSwitch]="step" style="margin-top: 30px;">
+      <div *ngSwitchCase="0">
+        <div class="single-field-group">
+          <label for="experimentName">
+            <span class="red-star">*</span>
+            Experiment Name
+          </label>
+          <input nz-input type="text" name="experimentName" id="experimentName" formControlName="experimentName" />
+        </div>
+        <div class="single-field-group">
+          <label for="description">
+            Description
+          </label>
+          <textarea
+            nz-input
+            [nzAutosize]="{ minRows: 3, maxRows: 6 }"
+            name="description"
+            formControlName="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" />
+        </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" />
+        </div>
+      </div>
+      <div *ngSwitchCase="1" id="page2">
+        <div>
+          <button nz-button id="env-btn" type="default" (click)="onCreateEnv()">
+            Add new env
+          </button>
+          <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>
+                  <label for="key{{ i }}">Key</label>
+                  <input nz-input name="key{{ i }}" placeholder="Key" formControlName="key" />
+                </div>
+                <div>
+                  <label for="value{{ i }}">Value</label>
+                  <input nz-input name="value{{ i }}" placeholder="Value" formControlName="value" />
+                </div>
+                <i nz-icon nzType="close-circle" nzTheme="fill" (click)="deleteItem(envs, i)"></i>
+              </li>
+              <div
+                class="alert-message"
+                *ngIf="env.get('key').dirty | logicalAnd: env.get('value').dirty:env.hasError('envMissing')"
+              >
+                {{ env.getError('envMissing') }}
+              </div>
+              <div
+                class="alert-message"
+                *ngIf="env.get('key').dirty | logicalAnd: env.get('key').hasError('duplicateError')"
+              >
+                {{ env.get('key').getError('duplicateError') }}
+              </div>
+            </ng-container>
+          </ul>
+          <nz-pagination
+            [(nzPageIndex)]="currentEnvPage"
+            [nzPageSize]="PAGESIZE"
+            [nzTotal]="envs.controls.length"
+            nzSimple
+          ></nz-pagination>
+        </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>
+        </nz-radio-group>
+        <label class="pg3-form-label"></label>
+        <button nz-button id="spec-btn" type="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 }}">
+                <label>Spec name</label>
+                <nz-select formControlName="name" nzPlaceHolder="Spec name" [ngSwitch]="jobTypes">
+                  <div *ngSwitchCase="'Tensorflow'">
+                    <nz-option *ngFor="let spec of TF_SPECNAMES" [nzValue]="spec" [nzLabel]="spec"></nz-option>
+                  </div>
+                  <div *ngSwitchCase="'Pytorch'">
+                    <nz-option *ngFor="let spec of PYTORCH_SPECNAMES" [nzValue]="spec" [nzLabel]="spec"></nz-option>
+                  </div>
+                </nz-select>
+              </div>
+              <div>
+                <label>Number of Replica</label>
+                <input
+                  nz-input
+                  name="replica{{ i }}"
+                  type="number"
+                  placeholder="number of replica"
+                  formControlName="replicas"
+                />
+              </div>
+              <div>
+                <label>Number of cpu</label>
+                <input nz-input name="cpu{{ i }}" type="number" placeholder="number of cpu" formControlName="cpus" />
+              </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" />
+                  <nz-select formControlName="unit">
+                    <nz-option *ngFor="let unit of MEMORY_UNITS" [nzValue]="unit" [nzLabel]="unit"></nz-option>
+                  </nz-select>
+                </div>
+              </div>
+              <i nz-icon nzType="close-circle" nzTheme="fill" class="delete-icon" (click)="deleteItem(specs, i)"></i>
+            </li>
+            <div
+              class="alert-message"
+              *ngIf="spec.get('name').dirty | logicalAnd: spec.get('memory').dirty:spec.hasError('specError')"
+            >
+              {{ spec.getError('specError') }}
+            </div>
+            <div
+              class="alert-message"
+              *ngIf="spec.get('name').dirty | logicalAnd: spec.get('name').hasError('duplicateError')"
+            >
+              {{ spec.get('name').getError('duplicateError') }}
+            </div>
+            <div
+              class="alert-message"
+              *ngIf="spec.get('memory').dirty | logicalAnd: spec.get('memory').hasError('memoryPatternError')"
+            >
+              {{ spec.get('memory').getError('memoryPatternError') }}
+            </div>
+            <div
+              class="alert-message"
+              *ngIf="spec.get('replicas').dirty | logicalAnd: spec.get('replicas').hasError('min')"
+            >
+              replicas must be at least 1
+            </div>
+            <div class="alert-message" *ngIf="spec.get('cpus').dirty | logicalAnd: spec.get('cpus').hasError('min')">
+              cpus must be at least 1
+            </div>
+          </ng-container>
+        </ul>
+        <nz-pagination
+          [(nzPageIndex)]="currentSpecPage"
+          [nzPageSize]="PAGESIZE"
+          [nzTotal]="specs.controls.length"
+          nzSimple
+        ></nz-pagination>
+      </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
new file mode 100644
index 0000000..adf39b2
--- /dev/null
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.scss
@@ -0,0 +1,134 @@
+/*!
+ * 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.
+ */
+
+ #experimentOuter{
+  background-color: white;
+  padding-left: 30px;
+  padding-top: 20px;
+}
+
+#experimentData{
+  margin-top: 16px;
+  margin-left: 25px;
+  margin-right: 25px;
+  background-color:white;
+  padding-left: 10px;
+  padding-right: 10px;
+}
+
+input.ng-invalid.ng-touched {
+ border: 1px solid red;
+}
+
+textarea.ng-invalid.ng-touched {
+ border: 1px solid red;
+}
+
+.red-star {
+ margin-top: 20px;
+ color: red;
+}
+
+.list-container {
+ padding: 0;
+ margin: 1rem 0;
+}
+
+/* For env section */
+.env-page-container {
+ display: flex;
+ flex-direction: column;
+}
+
+.env-input-container {
+ padding: 0;
+ margin: 1rem 0;
+}
+
+.input-group {
+ display: flex;
+ align-items: center;
+ font-size: .8rem;
+ &:not(:first-child) {
+    margin-top: 1rem;
+ }
+
+ & > *:not(:last-child) {
+    margin-right: .8rem;
+ }
+
+ & > div:nth-child(2), & > div:nth-child(3) {
+    flex: 0 1 20%;
+ }
+
+ & > div:nth-child(1), & > div:nth-child(4) {
+    flex: 0 1 25%;
+ }
+
+ & i {
+    cursor: pointer;
+    font-size: 20px;
+    margin-top: 20px;
+ }
+}
+
+.memory-input-group {
+ display: flex;
+ & input {
+    width: 70%;
+ }
+
+ & > * {
+    width: 30%;
+ }
+}
+
+/* utility */
+.single-field-group {
+ display: flex;
+ align-items: center;
+ margin-bottom: 1.5rem;
+ & label {
+    flex: 0 0 25%;
+    text-align: right;
+    font-weight: 500;
+    &:not(:last-child) {
+       margin-right: 1.5rem;
+    }
+ }
+ & input {
+    flex: 0 0 48%;
+ }
+ & textarea {
+    flex: 0 0 55%;
+ }
+}
+
+.alert-message {
+ color: red;
+ margin-top: .3rem;
+ margin-left: .5rem;
+}
+
+/* For jobs */
+#jobs-container {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
+ margin-bottom: 2rem;
+}
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
new file mode 100644
index 0000000..1022e71
--- /dev/null
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.ts
@@ -0,0 +1,369 @@
+/*
+ * 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, Input, OnDestroy } from '@angular/core';
+import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
+import { ExperimentSpec, Specs, SpecEnviroment, SpecMeta } from '@submarine/interfaces/experiment-spec';
+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 { Subscription } from 'rxjs';
+import { ExperimentFormService } from '@submarine/services/experiment.form.service';
+
+@Component({
+  selector: 'experiment-customized-form',
+  templateUrl: './experiment-customized-form.component.html',
+  styleUrls: ['./experiment-customized-form.component.scss']
+})
+export class ExperimentCustomizedForm implements OnInit, OnDestroy {
+  @Input() mode: 'create' | 'update' | 'clone';
+
+  // About new experiment
+  experiment: FormGroup;
+  step: number = 0;
+  subscriptions: Subscription[] = [];
+
+  // Constants
+  TF_SPECNAMES = ['Master', 'Worker', 'Ps'];
+  PYTORCH_SPECNAMES = ['Master', 'Worker'];
+  MEMORY_UNITS = ['M', 'G'];
+  TOTAL_STEPS = 2;
+
+  // About env page
+  currentEnvPage = 1;
+  PAGESIZE = 5;
+
+  // About spec
+  jobTypes = 'Tensorflow';
+  currentSpecPage = 1;
+
+  // About update
+  @Input() targetId: string = null;
+  @Input() targetSpec: ExperimentSpec = null;
+
+  constructor(
+    private experimentService: ExperimentService,
+    private experimentValidatorService: ExperimentValidatorService,
+    private experimentFormService: ExperimentFormService,
+    private nzMessageService: NzMessageService
+  ) {}
+
+  ngOnInit() {
+    this.experiment = new FormGroup({
+      experimentName: new FormControl(null, Validators.required),
+      description: new FormControl(null, [Validators.required]),
+      namespace: new FormControl('default', [Validators.required]),
+      cmd: new FormControl('', [Validators.required]),
+      image: new FormControl('', [Validators.required]),
+      envs: new FormArray([], [this.experimentValidatorService.nameValidatorFactory('key')]),
+      specs: new FormArray([], [this.experimentValidatorService.nameValidatorFactory('name')])
+    });
+
+    // Bind the component method for callback
+    this.checkStatus = this.checkStatus.bind(this);
+
+    if (this.mode === 'update') {
+      this.updateExperimentInit();
+    } else if (this.mode === 'clone') {
+      this.cloneExperimentInit(this.targetSpec);
+    }
+
+    // Fire status to parent when form value has changed
+    const sub1 = this.experiment.valueChanges.subscribe(this.checkStatus);
+
+    const sub2 = this.experimentFormService.stepService.subscribe((n) => {
+      if (n > 0) {
+        if (this.step === this.TOTAL_STEPS) {
+          this.handleSubmit();
+        } else {
+          this.step += 1;
+        }
+      } else {
+        this.step -= 1;
+      }
+      // Send the current step and okText back to parent
+      this.experimentFormService.modalPropsChange({
+        okText: this.step !== this.TOTAL_STEPS ? 'Next step' : 'Submit',
+        currentStep: this.step
+      });
+      // Run check after step is changed
+      this.checkStatus();
+    });
+
+    this.subscriptions.push(sub1, sub2);
+  }
+
+  ngOnDestroy() {
+    // Clean up the subscriptions
+    this.subscriptions.forEach((sub) => {
+      sub.unsubscribe();
+    });
+  }
+
+  // Getters of experiment request form
+  get experimentName() {
+    return this.experiment.get('experimentName');
+  }
+  get description() {
+    return this.experiment.get('description');
+  }
+  get namespace() {
+    return this.experiment.get('namespace');
+  }
+  get cmd() {
+    return this.experiment.get('cmd');
+  }
+  get envs() {
+    return this.experiment.get('envs') as FormArray;
+  }
+  get image() {
+    return this.experiment.get('image');
+  }
+  get specs() {
+    return this.experiment.get('specs') as FormArray;
+  }
+
+  /**
+   * Reset properies in parent component when the form is about to closed
+   */
+  closeModal() {
+    this.experimentFormService.modalPropsClear();
+  }
+
+  /**
+   * Check the validity of the experiment page
+   */
+  checkStatus() {
+    if (this.step === 0) {
+      this.experimentFormService.btnStatusChange(
+        this.experimentName.invalid || this.namespace.invalid || this.cmd.invalid || this.image.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);
+    }
+  }
+
+  /**
+   * Event handler for Next step/Submit button
+   */
+  handleSubmit() {
+    if (this.mode === 'create') {
+      const newSpec = this.constructSpec();
+      this.experimentService.createExperiment(newSpec).subscribe({
+        next: () => {},
+        error: (msg) => {
+          this.nzMessageService.error(`${msg}, please try again`, {
+            nzPauseOnHover: true
+          });
+        },
+        complete: () => {
+          this.nzMessageService.success('Experiment creation succeeds');
+          this.experimentFormService.fetchList();
+          this.closeModal();
+        }
+      });
+    } else if (this.mode === 'update') {
+      const newSpec = this.constructSpec();
+      this.experimentService.updateExperiment(this.targetId, newSpec).subscribe(
+        null,
+        (msg) => {
+          this.nzMessageService.error(`${msg}, please try again`, {
+            nzPauseOnHover: true
+          });
+        },
+        () => {
+          this.nzMessageService.success('Modification succeeds!');
+          this.experimentFormService.fetchList();
+          this.closeModal();
+        }
+      );
+    } else if (this.mode === 'clone') {
+      const newSpec = this.constructSpec();
+      this.experimentService.createExperiment(newSpec).subscribe(
+        null,
+        (msg) => {
+          this.nzMessageService.error(`${msg}, please try again`, {
+            nzPauseOnHover: true
+          });
+        },
+        () => {
+          this.nzMessageService.success('Create a new experiment !');
+          this.experimentFormService.fetchList();
+          this.closeModal();
+        }
+      );
+    }
+  }
+
+  /**
+   * Create a new env variable input
+   */
+  createEnv(defaultKey: string = '', defaultValue: string = '') {
+    // Create a new FormGroup
+    return new FormGroup(
+      {
+        key: new FormControl(defaultKey, [Validators.required]),
+        value: new FormControl(defaultValue, [Validators.required])
+      },
+      [this.experimentValidatorService.envValidator]
+    );
+  }
+  /**
+   * Create a new spec
+   */
+  createSpec(
+    defaultName: string = '',
+    defaultReplica: number = 1,
+    defaultCpu: number = 1,
+    defaultMemory: string = '',
+    defaultUnit: string = 'M'
+  ): FormGroup {
+    return new FormGroup(
+      {
+        name: new FormControl(defaultName, [Validators.required]),
+        replicas: new FormControl(defaultReplica, [Validators.min(1), Validators.required]),
+        cpus: new FormControl(defaultCpu, [Validators.min(1), Validators.required]),
+        memory: new FormGroup(
+          {
+            num: new FormControl(defaultMemory, [Validators.required]),
+            unit: new FormControl(defaultUnit, [Validators.required])
+          },
+          [this.experimentValidatorService.memoryValidator]
+        )
+      },
+      [this.experimentValidatorService.specValidator]
+    );
+  }
+
+  /**
+   * Handler for the create env button
+   */
+  onCreateEnv() {
+    const env = this.createEnv();
+    this.envs.push(env);
+    // If the new page is created, jump to that page
+    if (this.envs.controls.length > 1 && this.envs.controls.length % this.PAGESIZE === 1) {
+      this.currentEnvPage += 1;
+    }
+  }
+
+  /**
+   * Handler for the create spec button
+   */
+  onCreateSpec() {
+    const spec = this.createSpec();
+    this.specs.push(spec);
+    // If the new page is created, jump to that page
+    if (this.specs.controls.length > 1 && this.specs.controls.length % this.PAGESIZE === 1) {
+      this.currentSpecPage += 1;
+    }
+  }
+
+  /**
+   * Construct spec for new experiment creation
+   */
+  constructSpec(): ExperimentSpec {
+    // Construct the spec
+    const meta: SpecMeta = {
+      name: this.experimentName.value,
+      namespace: this.namespace.value,
+      framework: this.jobTypes,
+      cmd: this.cmd.value,
+      envVars: {}
+    };
+    for (const env of this.envs.controls) {
+      if (env.get('key').value) {
+        meta.envVars[env.get('key').value] = env.get('value').value;
+      }
+    }
+
+    const specs: Specs = {};
+    for (const spec of this.specs.controls) {
+      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
+          }`
+        };
+      }
+    }
+
+    const environment: SpecEnviroment = {
+      image: this.image.value
+    };
+
+    const newExperimentSpec: ExperimentSpec = {
+      meta: meta,
+      environment: environment,
+      spec: specs
+    };
+
+    return newExperimentSpec;
+  }
+
+  /**
+   * Delete list items(envs or specs)
+   *
+   * @param arr - The FormArray containing the item
+   * @param index - The index of the item
+   */
+  deleteItem(arr: FormArray, index: number) {
+    arr.removeAt(index);
+  }
+
+  updateExperimentInit() {
+    // Prevent user from modifying the name
+    this.experimentName.disable();
+    // Put value back
+    this.experimentName.setValue(this.targetSpec.meta.name);
+    this.cloneExperiment(this.targetSpec);
+    // Check status to enable next btn
+    this.checkStatus();
+  }
+
+  cloneExperimentInit(spec: ExperimentSpec) {
+    // Enable user from modifying the name
+    this.experimentName.enable();
+    // Put value back
+    const id: string = nanoid(8);
+    const cloneExperimentName = spec.meta.name + '-' + id;
+    this.experimentName.setValue(cloneExperimentName.toLocaleLowerCase());
+    this.cloneExperiment(spec);
+  }
+
+  cloneExperiment(spec: ExperimentSpec) {
+    this.description.setValue(spec.meta.description);
+    this.namespace.setValue(spec.meta.namespace);
+    this.cmd.setValue(spec.meta.cmd);
+    this.image.setValue(spec.environment.image);
+    for (const [key, value] of Object.entries(spec.meta.envVars)) {
+      const env = this.createEnv(key, value);
+      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);
+      this.specs.push(newSpec);
+    }
+  }
+}
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 6d6b76f..30a750a 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
@@ -63,7 +63,7 @@
           id="openExperiment"
           nzType="primary"
           style="margin-right: 5px; margin-bottom: 15px; margin-top: 15px;"
-          (click)="initExperimentStatus('create')"
+          (click)="initModal('create')"
         >
           <i nz-icon nzType="plus"></i>
           New Experiment
@@ -123,9 +123,9 @@
             <td>{{ data.runningTime | date: 'M/d/yyyy, h:mm a' }}</td>
             <td>{{ data.duration }}</td>
             <td class="td-action">
-              <a (click)="onCloneExperiment(data.spec)">Clone</a>
+              <a (click)="initModal('clone', 'customized', null, data.spec)">Clone</a>
               <nz-divider nzType="vertical"></nz-divider>
-              <a (click)="onUpdateExperiment(data.experimentId, data.spec)">Update</a>
+              <a (click)="initModal('update', 'customized', data.experimentId, data.spec)">Update</a>
               <nz-divider nzType="vertical"></nz-divider>
               <a
                 nz-popconfirm
@@ -145,229 +145,40 @@
     <router-outlet (experimentInfoChange)="isInfo = true"></router-outlet>
   </div>
   <nz-modal
-    [(nzVisible)]="isVisible"
+    [(nzVisible)]="modalProps.isVisible"
     nzTitle="Create Experiment"
-    [(nzOkText)]="okText"
-    (nzOnCancel)="isVisible = false"
+    (nzOnCancel)="closeModal()"
     [nzWidth]="1000"
   >
-    <div>
-      <nz-steps [nzCurrent]="current">
-        <nz-step nzTitle="Meta"></nz-step>
-        <nz-step nzTitle="Env"></nz-step>
-        <nz-step nzTitle="Spec"></nz-step>
-      </nz-steps>
-    </div>
-    <div>
-      <form [formGroup]="experiment">
-        <div *nzModalFooter>
-          <button nz-button nzType="default" (click)="isVisible = false">Cancel</button>
-          <button id="go" nz-button nzType="primary" [disabled]="checkStatus()" (click)="handleOk()">
-            {{ okText }}
-          </button>
-          <button
-            *ngIf="current > 0"
-            nz-button
-            nzType="default"
-            style="float: left;"
-            (click)="current = current - 1; okText = 'Next Step'"
-          >
-            Prev Step
-          </button>
-        </div>
-        <div [ngSwitch]="current" style="margin-top: 30px;">
-          <div *ngSwitchCase="0">
-            <div class="single-field-group">
-              <label for="experimentName">
-                <span class="red-star">*</span>
-                Experiment Name
-              </label>
-              <input nz-input type="text" name="experimentName" id="experimentName" formControlName="experimentName" />
-            </div>
-            <div class="single-field-group">
-              <label for="description">
-                Description
-              </label>
-              <textarea
-                nz-input
-                [nzAutosize]="{ minRows: 3, maxRows: 6 }"
-                name="description"
-                formControlName="description"
-              ></textarea>
-            </div>
-
-            <div class="single-field-group">
-              <label for="frameworks">
-                <span class="red-star">*</span>
-                Frameworks
-              </label>
-              <nz-select formControlName="frameworks" nzPlaceHolder="Choose" style="width: 48%;">
-                <nz-option
-                  *ngFor="let framework of FRAMEWORK_NAMES"
-                  [nzValue]="framework"
-                  [nzLabel]="framework"
-                ></nz-option>
-              </nz-select>
-            </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" />
-            </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" />
-            </div>
-          </div>
-          <div *ngSwitchCase="1" id="page2">
-            <div>
-              <button nz-button id="env-btn" type="default" (click)="onCreateEnv()">
-                Add new env
-              </button>
-              <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>
-                      <label for="key{{ i }}">Key</label>
-                      <input nz-input name="key{{ i }}" placeholder="Key" formControlName="key" />
-                    </div>
-                    <div>
-                      <label for="value{{ i }}">Value</label>
-                      <input nz-input name="value{{ i }}" placeholder="Value" formControlName="value" />
-                    </div>
-                    <i nz-icon nzType="close-circle" nzTheme="fill" (click)="deleteItem(envs, i)"></i>
-                  </li>
-                  <div
-                    class="alert-message"
-                    *ngIf="env.get('key').dirty | logicalAnd: env.get('value').dirty:env.hasError('envMissing')"
-                  >
-                    {{ env.getError('envMissing') }}
-                  </div>
-                  <div
-                    class="alert-message"
-                    *ngIf="env.get('key').dirty | logicalAnd: env.get('key').hasError('duplicateError')"
-                  >
-                    {{ env.get('key').getError('duplicateError') }}
-                  </div>
-                </ng-container>
-              </ul>
-              <nz-pagination
-                [(nzPageIndex)]="currentEnvPage"
-                [nzPageSize]="PAGESIZE"
-                [nzTotal]="envs.controls.length"
-                nzSimple
-              ></nz-pagination>
-            </div>
-          </div>
-          <div *ngSwitchCase="2">
-            <label class="pg3-form-label"></label>
-            <button nz-button id="spec-btn" type="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 }}">
-                    <label>Spec name</label>
-                    <nz-select formControlName="name" nzPlaceHolder="Spec name" [ngSwitch]="frameworks.value">
-                      <div *ngSwitchCase="'TensorFlow'">
-                        <nz-option *ngFor="let spec of TF_SPECNAMES" [nzValue]="spec" [nzLabel]="spec"></nz-option>
-                      </div>
-                      <div *ngSwitchCase="'PyTorch'">
-                        <nz-option *ngFor="let spec of PYTORCH_SPECNAMES" [nzValue]="spec" [nzLabel]="spec"></nz-option>
-                      </div>
-                    </nz-select>
-                  </div>
-                  <div>
-                    <label>Number of Replica</label>
-                    <input
-                      nz-input
-                      name="replica{{ i }}"
-                      type="number"
-                      placeholder="number of replica"
-                      formControlName="replicas"
-                    />
-                  </div>
-                  <div>
-                    <label>Number of cpu</label>
-                    <input
-                      nz-input
-                      name="cpu{{ i }}"
-                      type="number"
-                      placeholder="number of cpu"
-                      formControlName="cpus"
-                    />
-                  </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" />
-                      <nz-select formControlName="unit">
-                        <nz-option *ngFor="let unit of MEMORY_UNITS" [nzValue]="unit" [nzLabel]="unit"></nz-option>
-                      </nz-select>
-                    </div>
-                  </div>
-                  <i
-                    nz-icon
-                    nzType="close-circle"
-                    nzTheme="fill"
-                    class="delete-icon"
-                    (click)="deleteItem(specs, i)"
-                  ></i>
-                </li>
-                <div
-                  class="alert-message"
-                  *ngIf="spec.get('name').dirty | logicalAnd: spec.get('memory').dirty:spec.hasError('specError')"
-                >
-                  {{ spec.getError('specError') }}
-                </div>
-                <div
-                  class="alert-message"
-                  *ngIf="spec.get('name').dirty | logicalAnd: spec.get('name').hasError('duplicateError')"
-                >
-                  {{ spec.get('name').getError('duplicateError') }}
-                </div>
-                <div
-                  class="alert-message"
-                  *ngIf="spec.get('memory').dirty | logicalAnd: spec.get('memory').hasError('memoryPatternError')"
-                >
-                  {{ spec.get('memory').getError('memoryPatternError') }}
-                </div>
-                <div
-                  class="alert-message"
-                  *ngIf="spec.get('replicas').dirty | logicalAnd: spec.get('replicas').hasError('min')"
-                >
-                  replicas must be at least 1
-                </div>
-                <div
-                  class="alert-message"
-                  *ngIf="spec.get('cpus').dirty | logicalAnd: spec.get('cpus').hasError('min')"
-                >
-                  cpus must be at least 1
-                </div>
-              </ng-container>
-            </ul>
-            <nz-pagination
-              [(nzPageIndex)]="currentSpecPage"
-              [nzPageSize]="PAGESIZE"
-              [nzTotal]="specs.controls.length"
-              nzSimple
-            ></nz-pagination>
-          </div>
-        </div>
-      </form>
+    <nz-button-group id="form-type-container" *ngIf="!modalProps.formType">
+      <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">
+        From predefined experiment library
+      </button>
+    </nz-button-group>
+    <div *nzModalFooter>
+      <button nz-button nzType="default" (click)="closeModal()">Cancel</button>
+      <button
+        id="go"
+        nz-button
+        nzType="primary"
+        *ngIf="modalProps.formType"
+        [disabled]="nextBtnDisable"
+        (click)="proceedForm()"
+      >
+        {{ modalProps.okText }}
+      </button>
+      <button *ngIf="modalProps.currentStep > 0" nz-button nzType="default" style="float: left;" (click)="prevForm()">
+        Prev Step
+      </button>
     </div>
+    <experiment-customized-form
+      [mode]="mode"
+      [targetId]="targetId"
+      [targetSpec]="targetSpec"
+      *ngIf="this.modalProps.formType === 'customized'"
+    ></experiment-customized-form>
   </nz-modal>
 </nz-layout>
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.scss b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.scss
index ac1dc47..ba2f168 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.scss
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.scss
@@ -32,6 +32,16 @@
     padding-right: 10px;
  }
 
+ #form-type-container {
+    display: grid;
+    grid-auto-rows: 5rem;
+    grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
+    column-gap: 3rem;
+    & button {
+      height: 100%;
+    }
+ }
+
  input.ng-invalid.ng-touched {
    border: 1px solid red;
  }
@@ -39,91 +49,3 @@
  textarea.ng-invalid.ng-touched {
    border: 1px solid red;
 }
-
-.red-star {
-   margin-top: 20px;
-   color: red;
-}
-
-.list-container {
-   padding: 0;
-   margin: 1rem 0;
-}
-
-/* For env section */
-.env-page-container {
-   display: flex;
-   flex-direction: column;
-}
-
-.env-input-container {
-   padding: 0;
-   margin: 1rem 0;
-}
-
-.input-group {
-   display: flex;
-   align-items: center;
-   font-size: .8rem;
-   &:not(:first-child) {
-      margin-top: 1rem;
-   }
-
-   & > *:not(:last-child) {
-      margin-right: .8rem;
-   }
-
-   & > div:nth-child(2), & > div:nth-child(3) {
-      flex: 0 1 20%;
-   }
-
-   & > div:nth-child(1), & > div:nth-child(4) {
-      flex: 0 1 25%;
-   }
-
-   & i {
-      cursor: pointer;
-      font-size: 20px;
-      margin-top: 20px;
-   }
-}
-
-.memory-input-group {
-   display: flex;
-   & input {
-      width: 70%;
-   }
-
-   & > * {
-      width: 30%;
-   }
-}
-
-/* utility */
-.single-field-group {
-   display: flex;
-   align-items: center;
-   margin-bottom: 1.5rem;
-   & label {
-      flex: 0 0 25%;
-      text-align: right;
-      font-weight: 500;
-      &:not(:last-child) {
-         margin-right: 1.5rem;
-      }
-   }
-   & input {
-      flex: 0 0 48%;
-   }
-   & textarea {
-      flex: 0 0 55%;
-   }
-}
-
-.alert-message {
-   color: red;
-   margin-top: .3rem;
-   margin-left: .5rem;
-}
-
-/* For memory  */
\ No newline at end of file
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 f22ee4a..fde4738 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
@@ -18,24 +18,25 @@
  */
 
 import { Component, OnInit } from '@angular/core';
-import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
 import { ActivatedRoute, NavigationStart, Router } from '@angular/router';
 import { ExperimentInfo } from '@submarine/interfaces/experiment-info';
-import { ExperimentSpec, Specs, SpecEnviroment, SpecMeta } from '@submarine/interfaces/experiment-spec';
 import { ExperimentService } from '@submarine/services/experiment.service';
-import { ExperimentFormService } from '@submarine/services/experiment.validator.service';
-import { nanoid } from 'nanoid';
 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';
 
 @Component({
   selector: 'submarine-experiment',
   templateUrl: './experiment.component.html',
-  styleUrls: ['./experiment.component.scss']
+  styleUrls: ['./experiment.component.scss'],
+  providers: [ExperimentFormService]
 })
 export class ExperimentComponent implements OnInit {
   experimentList: ExperimentInfo[] = [];
   checkedList: boolean[] = [];
   selectAllChecked: boolean = false;
+
   // About experiment information
   isInfo = false;
   experimentID: string;
@@ -44,27 +45,19 @@ export class ExperimentComponent implements OnInit {
   showExperiment = 'All';
   searchText = '';
 
-  // About new experiment
-  experiment: FormGroup;
-  current = 0;
-  okText = 'Next Step';
-  isVisible = false;
+  // About form management
+  modalProps: ModalProps = {
+    okText: 'Next step',
+    isVisible: false,
+    currentStep: 0,
+    formType: null
+  };
+  nextBtnDisable: boolean = true;
 
-  // About update
+  // About update and clone
   mode: 'create' | 'update' | 'clone' = 'create';
-  updateId: string = null;
-
-  FRAMEWORK_NAMES = ['TensorFlow', 'PyTorch'];
-  TF_SPECNAMES = ['Master', 'Worker', 'Ps'];
-  PYTORCH_SPECNAMES = ['Master', 'Worker'];
-  MEMORY_UNITS = ['M', 'G'];
-
-  // About env page
-  currentEnvPage = 1;
-  PAGESIZE = 5;
-
-  // About spec
-  currentSpecPage = 1;
+  targetId: string = null;
+  targetSpec: ExperimentSpec = null;
 
   statusColor: { [key: string]: string } = {
     Accepted: 'gold',
@@ -74,24 +67,14 @@ export class ExperimentComponent implements OnInit {
   };
 
   constructor(
-    private experimentService: ExperimentService,
-    private experimentFormService: ExperimentFormService,
     private nzMessageService: NzMessageService,
     private route: ActivatedRoute,
-    private router: Router
+    private router: Router,
+    private experimentService: ExperimentService,
+    private experimentFormService: ExperimentFormService
   ) {}
 
   ngOnInit() {
-    this.experiment = new FormGroup({
-      experimentName: new FormControl(null, Validators.required),
-      description: new FormControl(null, [Validators.required]),
-      frameworks: new FormControl('TensorFlow', [Validators.required]),
-      namespace: new FormControl('default', [Validators.required]),
-      cmd: new FormControl('', [Validators.required]),
-      envs: new FormArray([], [this.experimentFormService.nameValidatorFactory('key')]),
-      image: new FormControl('', [Validators.required]),
-      specs: new FormArray([], [this.experimentFormService.nameValidatorFactory('name')])
-    });
     this.fetchExperimentList();
     this.isInfo = this.router.url !== '/workbench/experiment';
     this.experimentID = this.route.snapshot.params.id;
@@ -107,250 +90,47 @@ export class ExperimentComponent implements OnInit {
       }
     });
 
-    this.reloadCheck();
-  }
-
-  // Getters of experiment request form
-  get experimentName() {
-    return this.experiment.get('experimentName');
-  }
-  get description() {
-    return this.experiment.get('description');
-  }
-  get frameworks() {
-    return this.experiment.get('frameworks');
-  }
-  get namespace() {
-    return this.experiment.get('namespace');
-  }
-  get cmd() {
-    return this.experiment.get('cmd');
-  }
-  get envs() {
-    return this.experiment.get('envs') as FormArray;
-  }
-  get image() {
-    return this.experiment.get('image');
-  }
-  get specs() {
-    return this.experiment.get('specs') as FormArray;
-  }
-  /**
-   * Check the validity of the experiment page
-   *
-   */
-  checkStatus() {
-    if (this.current === 0) {
-      return (
-        this.experimentName.invalid ||
-        this.frameworks.invalid ||
-        this.namespace.invalid ||
-        this.cmd.invalid ||
-        this.image.invalid
-      );
-    } else if (this.current === 1) {
-      return this.envs.invalid;
-    } else if (this.current === 2) {
-      return this.specs.invalid;
-    }
-  }
-
-  /**
-   * Init a new experiment form, clear all status, clear all form controls and open the form in the mode specified in the argument
-   *
-   * @param mode - The mode which the form should open in
-   */
-  initExperimentStatus(mode: 'create' | 'update' | 'clone') {
-    this.mode = mode;
-    this.current = 0;
-    this.okText = 'Next step';
-    this.isVisible = true;
-    this.updateId = null;
-    // Reset the form
-    this.experimentName.enable();
-    this.envs.clear();
-    this.specs.clear();
-    this.experiment.reset({ frameworks: 'TensorFlow', namespace: 'default' });
-  }
-
-  /**
-   * Event handler for Next step/Submit button
-   */
-  handleOk() {
-    if (this.current === 1) {
-      this.okText = 'Submit';
-    } else if (this.current === 2) {
-      if (this.mode === 'create') {
-        const newSpec = this.constructSpec();
-        this.experimentService.createExperiment(newSpec).subscribe({
-          next: (result) => {
-            this.fetchExperimentList();
-          },
-          error: (msg) => {
-            this.nzMessageService.error(`${msg}, please try again`, {
-              nzPauseOnHover: true
-            });
-          },
-          complete: () => {
-            this.nzMessageService.success('Experiment creation succeeds');
-            this.isVisible = false;
-          }
-        });
-      } else if (this.mode === 'update') {
-        const newSpec = this.constructSpec();
-        this.experimentService.updateExperiment(this.updateId, newSpec).subscribe(
-          () => {
-            this.fetchExperimentList();
-          },
-          (msg) => {
-            this.nzMessageService.error(`${msg}, please try again`, {
-              nzPauseOnHover: true
-            });
-          },
-          () => {
-            this.nzMessageService.success('Modification succeeds!');
-            this.isVisible = false;
-          }
-        );
-      } else if (this.mode === 'clone') {
-        const newSpec = this.constructSpec();
-        this.experimentService.createExperiment(newSpec).subscribe(
-          () => {
-            this.fetchExperimentList();
-          },
-          (msg) => {
-            this.nzMessageService.error(`${msg}, please try again`, {
-              nzPauseOnHover: true
-            });
-          },
-          () => {
-            this.nzMessageService.success('Create a new experiment !');
-            this.isVisible = false;
-          }
-        );
-      }
-    }
-
-    if (this.current < 2) {
-      this.current++;
-    }
-  }
+    // Subscriptions to experimentFormService
+    this.experimentFormService.fetchListService.subscribe(() => {
+      this.fetchExperimentList();
+    });
+    this.experimentFormService.btnStatusService.subscribe((status) => {
+      this.nextBtnDisable = status;
+    });
+    this.experimentFormService.modalPropsService.subscribe((props) => {
+      this.modalProps = { ...this.modalProps, ...props };
+    });
 
-  /**
-   * Create a new env variable input
-   */
-  createEnv(defaultKey: string = '', defaultValue: string = '') {
-    // Create a new FormGroup
-    return new FormGroup(
-      {
-        key: new FormControl(defaultKey, [Validators.required]),
-        value: new FormControl(defaultValue, [Validators.required])
-      },
-      [this.experimentFormService.envValidator]
-    );
-  }
-  /**
-   * Create a new spec
-   */
-  createSpec(
-    defaultName: string = '',
-    defaultReplica: number = 1,
-    defaultCpu: number = 1,
-    defaultMemory: string = '',
-    defaultUnit: string = 'M'
-  ): FormGroup {
-    return new FormGroup(
-      {
-        name: new FormControl(defaultName, [Validators.required]),
-        replicas: new FormControl(defaultReplica, [Validators.min(1), Validators.required]),
-        cpus: new FormControl(defaultCpu, [Validators.min(1), Validators.required]),
-        memory: new FormGroup(
-          {
-            num: new FormControl(defaultMemory, [Validators.required]),
-            unit: new FormControl(defaultUnit, [Validators.required])
-          },
-          [this.experimentFormService.memoryValidator]
-        )
-      },
-      [this.experimentFormService.specValidator]
-    );
+    this.reloadCheck();
   }
 
-  /**
-   * Handler for the create env button
-   */
-  onCreateEnv() {
-    const env = this.createEnv();
-    this.envs.push(env);
-    // If the new page is created, jump to that page
-    if (this.envs.controls.length > 1 && this.envs.controls.length % this.PAGESIZE === 1) {
-      this.currentEnvPage += 1;
+  initModal(
+    initMode: 'create' | 'update' | 'clone',
+    initFormType = null,
+    id: string = null,
+    spec: ExperimentSpec = null
+  ) {
+    this.mode = initMode;
+    this.modalProps.isVisible = true;
+    this.modalProps.formType = initFormType;
+
+    if (initMode === 'update') {
+      // Keep id for later request
+      this.targetId = id;
+      this.targetSpec = spec;
     }
   }
 
-  /**
-   * Handler for the create spec button
-   */
-  onCreateSpec() {
-    const spec = this.createSpec();
-    this.specs.push(spec);
-    // If the new page is created, jump to that page
-    if (this.specs.controls.length > 1 && this.specs.controls.length % this.PAGESIZE === 1) {
-      this.currentSpecPage += 1;
-    }
+  closeModal() {
+    this.experimentFormService.modalPropsClear();
   }
 
-  /**
-   * Construct spec for new experiment creation
-   */
-  constructSpec(): ExperimentSpec {
-    // Construct the spec
-    const meta: SpecMeta = {
-      name: this.experimentName.value,
-      namespace: this.namespace.value,
-      framework: this.frameworks.value,
-      cmd: this.cmd.value,
-      envVars: {}
-    };
-    for (const env of this.envs.controls) {
-      if (env.get('key').value) {
-        meta.envVars[env.get('key').value] = env.get('value').value;
-      }
-    }
-
-    const specs: Specs = {};
-    for (const spec of this.specs.controls) {
-      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
-          }`
-        };
-      }
-    }
-
-    const environment: SpecEnviroment = {
-      image: this.image.value
-    };
-
-    const newExperimentSpec: ExperimentSpec = {
-      meta: meta,
-      environment: environment,
-      spec: specs
-    };
-
-    return newExperimentSpec;
+  proceedForm() {
+    this.experimentFormService.stepChange(1);
   }
 
-  /**
-   * Delete list items(envs or specs)
-   *
-   * @param arr - The FormArray containing the item
-   * @param index - The index of the item
-   */
-  deleteItem(arr: FormArray, index: number) {
-    arr.removeAt(index);
+  prevForm() {
+    this.experimentFormService.stepChange(-1);
   }
 
   fetchExperimentList() {
@@ -376,50 +156,6 @@ export class ExperimentComponent implements OnInit {
     });
   }
 
-  onUpdateExperiment(id: string, spec: ExperimentSpec) {
-    // Open Modal in update mode
-    this.initExperimentStatus('update');
-    // Keep id for later request
-    this.updateId = id;
-
-    // Prevent user from modifying the name
-    this.experimentName.disable();
-
-    // Put value back
-    this.experimentName.setValue(spec.meta.name);
-    this.cloneExperiment(spec);
-  }
-
-  onCloneExperiment(spec: ExperimentSpec) {
-    // Open Modal in update mode
-    this.initExperimentStatus('clone');
-    // Prevent user from modifying the name
-    this.experimentName.enable();
-    // Put value back
-    const id: string = nanoid(8);
-    const cloneExperimentName = spec.meta.name + '-' + id;
-    this.experimentName.setValue(cloneExperimentName.toLocaleLowerCase());
-    this.cloneExperiment(spec);
-  }
-
-  cloneExperiment(spec: ExperimentSpec) {
-    this.description.setValue(spec.meta.description);
-    this.namespace.setValue(spec.meta.namespace);
-    this.cmd.setValue(spec.meta.cmd);
-    this.image.setValue(spec.environment.image);
-
-    for (const [key, value] of Object.entries(spec.meta.envVars)) {
-      const env = this.createEnv(key, value);
-      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);
-      this.specs.push(newSpec);
-    }
-  }
-
   onDeleteExperiment(id: string, onMessage: boolean) {
     this.experimentService.deleteExperiment(id).subscribe(
       () => {
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 966f647..c5563d2 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
@@ -8,7 +8,10 @@ import { ExperimentInfoComponent } from './experiment-info/experiment-info.compo
 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
@@ -29,8 +32,24 @@ import { OutputsComponent } from './experiment-info/outputs/outputs.component';
  */
 
 @NgModule({
-  exports: [ReactiveFormsModule],
-  imports: [NgZorroAntdModule, CommonModule, FormsModule, NgxChartsModule],
-  declarations: [ExperimentInfoComponent, HyperParamsComponent, MetricsComponent, ChartsComponent, OutputsComponent]
+  exports: [ReactiveFormsModule, ExperimentComponent],
+  imports: [
+    ReactiveFormsModule,
+    NgZorroAntdModule,
+    CommonModule,
+    FormsModule,
+    NgxChartsModule,
+    RouterModule,
+    PipeSharedModule
+  ],
+  declarations: [
+    ExperimentComponent,
+    ExperimentInfoComponent,
+    HyperParamsComponent,
+    MetricsComponent,
+    ChartsComponent,
+    OutputsComponent,
+    ExperimentCustomizedForm
+  ]
 })
 export class ExperimentModule {}
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/workbench.module.ts b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/workbench.module.ts
index a652add..af8b64d 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/workbench.module.ts
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/workbench.module.ts
@@ -41,7 +41,6 @@ import { EnvironmentComponent } from './environment/environment.component';
     WorkbenchComponent,
     HomeComponent,
     WorkspaceComponent,
-    ExperimentComponent,
     DataComponent,
     ModelComponent,
     EnvironmentComponent
diff --git a/submarine-workbench/workbench-web-ng/src/app/services/experiment.form.service.ts b/submarine-workbench/workbench-web-ng/src/app/services/experiment.form.service.ts
new file mode 100644
index 0000000..f937357
--- /dev/null
+++ b/submarine-workbench/workbench-web-ng/src/app/services/experiment.form.service.ts
@@ -0,0 +1,60 @@
+/*!
+ * 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 { Injectable } from '@angular/core';
+import { Subject } from 'rxjs';
+import { ModalProps } from '@submarine/interfaces/modal-props';
+
+@Injectable()
+export class ExperimentFormService {
+  // Subject(observable source)
+  private stepServiceSource = new Subject<number>();
+  private fetchListServiceSource = new Subject<boolean>();
+  private btnStatusServiceSource = new Subject<boolean>();
+  private modalPropsServiceSource = new Subject<ModalProps>();
+
+  // Observable streams
+  stepService = this.stepServiceSource.asObservable();
+  fetchListService = this.fetchListServiceSource.asObservable();
+  btnStatusService = this.btnStatusServiceSource.asObservable();
+  modalPropsService = this.modalPropsServiceSource.asObservable();
+
+  // Event emitter
+  stepChange(step: number) {
+    this.stepServiceSource.next(step);
+  }
+  btnStatusChange(status: boolean) {
+    this.btnStatusServiceSource.next(status);
+  }
+  modalPropsChange(props: ModalProps) {
+    this.modalPropsServiceSource.next(props);
+  }
+  // Syntax sugar for reset modal
+  modalPropsClear() {
+    this.modalPropsChange({
+      okText: 'Next step',
+      isVisible: false,
+      currentStep: 0,
+      formType: null
+    });
+  }
+  fetchList() {
+    this.fetchListServiceSource.next(true);
+  }
+}
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 df09c9c..d15ca2a 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
@@ -17,14 +17,13 @@
  * under the License.
  */
 
-import { FormGroup, ValidatorFn, ValidationErrors, FormControl, FormArray } from '@angular/forms';
+import { FormGroup, ValidatorFn, ValidationErrors, FormArray } from '@angular/forms';
 import { Injectable } from '@angular/core';
-import { ExperimentModule } from '@submarine/pages/workbench/experiment/experiment.module';
 
 @Injectable({
-  providedIn: ExperimentModule
+  providedIn: 'root'
 })
-export class ExperimentFormService {
+export class ExperimentValidatorService {
   /**
    * The validator for env key/value pair
    * @param envGroup A FormGroup resides in `envs` FromArray in createExperiment


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