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 b0ea958  SUBMARINE-722. [WEB] Add template page on workbench
b0ea958 is described below

commit b0ea9583d24418653ef341681d68fedb40219dc6
Author: kobe860219 <[email protected]>
AuthorDate: Tue Apr 13 00:12:50 2021 +0800

    SUBMARINE-722. [WEB] Add template page on workbench
    
    ### What is this PR for?
    Complete template page on workbench. This pr is a UI for users to register 
new pre-defined experiment template. Now user could register a pre-defined 
experiment template, then run experiments quickly with different 
hyper-parameters.
    
    ### What type of PR is it?
    [Feature]
    
    ### Todos
    * [ ] - templateIT
    
    ### What is the Jira issue?
    https://issues.apache.org/jira/browse/SUBMARINE-722
    
    ### How should this be tested?
    https://github.com/kobe860219/submarine/runs/2325676029
    
    ### Screenshots (if appropriate)
    <img width="1440" alt="截圖 2021-04-13 上午12 18 14" 
src="https://user-images.githubusercontent.com/48027290/114427696-eebf4900-9bed-11eb-8790-0ec75a0e347f.png";>
    <img width="1440" alt="截圖 2021-04-13 上午12 18 23" 
src="https://user-images.githubusercontent.com/48027290/114427715-f252d000-9bed-11eb-9fec-51b5ffaf0d4d.png";>
    <img width="1440" alt="截圖 2021-04-13 上午12 18 38" 
src="https://user-images.githubusercontent.com/48027290/114427720-f383fd00-9bed-11eb-83af-e144914674da.png";>
    <img width="1440" alt="截圖 2021-04-13 上午12 18 47" 
src="https://user-images.githubusercontent.com/48027290/114427728-f41c9380-9bed-11eb-9d4d-0d9b82da4bc0.png";>
    <img width="1440" alt="截圖 2021-04-13 上午12 19 03" 
src="https://user-images.githubusercontent.com/48027290/114427732-f54dc080-9bed-11eb-82bc-09e7d24f02e3.png";>
    <img width="1440" alt="截圖 2021-04-13 上午12 19 13" 
src="https://user-images.githubusercontent.com/48027290/114427736-f5e65700-9bed-11eb-80c2-8700aa0458bb.png";>
    
    ### Questions:
    * Does the licenses files need update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    Author: kobe860219 <[email protected]>
    
    Signed-off-by: Liu Xun <[email protected]>
    
    Closes #555 from kobe860219/SUBMARINE-722 and squashes the following 
commits:
    
    48da7b6 [kobe860219] Add image column
    fc9e695 [kobe860219] Template page done
    20569a4 [kobe860219] Init form
    6536789 [kobe860219] Delete done
    a4cf16d [kobe860219] Implement template info page.
    ce0b758 [kobe860219] Init template page
---
 .../src/app/interfaces/experiment-template.ts      |   4 +-
 .../environment-form/environment-form.component.ts |   5 +-
 .../template-form/template-form.component.html     | 300 +++++++++++++++++++++
 .../template-form/template-form.component.scss}    |  56 ++--
 .../template-form/template-form.component.ts       | 290 ++++++++++++++++++++
 .../template-home/template-home.component.html     |  35 +++
 .../template-home/template-home.component.scss}    |  25 +-
 .../template-home/template-home.component.ts       |  50 ++++
 .../template-list/template-list.component.html     |  41 +++
 .../template-list/template-list.component.scss}    |  25 +-
 .../template-list/template-list.component.ts}      |  28 +-
 .../template-info/template-info.component.html     |  96 +++++++
 .../template-info/template-info.component.scss}    |  25 +-
 .../template-info/template-info.component.ts       |  81 ++++++
 .../workbench/template/template-routing.module.ts} |  46 ++--
 .../workbench/template/template.component.html     |  41 +++
 .../workbench/template/template.component.scss}    |  25 +-
 .../workbench/template/template.component.ts}      |  31 +--
 .../template.module.ts}                            |  45 ++--
 .../pages/workbench/workbench-routing.module.ts    |   5 +
 .../src/app/pages/workbench/workbench.component.ts |  36 +--
 .../src/app/pages/workbench/workbench.module.ts    |   2 +
 .../src/app/services/experiment.service.ts         |  41 ++-
 .../app/services/experiment.validator.service.ts   |  10 +-
 24 files changed, 1133 insertions(+), 210 deletions(-)

diff --git 
a/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts 
b/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts
index d88cbf2..498536f 100644
--- 
a/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts
+++ 
b/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts
@@ -19,14 +19,14 @@
 
 import { ExperimentSpec } from '@submarine/interfaces/experiment-spec';
 
-interface ExperimentTemplateParamSpec {
+export interface ExperimentTemplateParamSpec {
   name: string;
   required: string;
   description: string;
   value: string;
 }
 
-interface ExperimentTemplateSpec {
+export interface ExperimentTemplateSpec {
   name: string;
   author: string;
   description: string;
diff --git 
a/submarine-workbench/workbench-web/src/app/pages/workbench/environment/environment-home/environment-form/environment-form.component.ts
 
b/submarine-workbench/workbench-web/src/app/pages/workbench/environment/environment-home/environment-form/environment-form.component.ts
index ac0ed04..91a408d 100644
--- 
a/submarine-workbench/workbench-web/src/app/pages/workbench/environment/environment-home/environment-form/environment-form.component.ts
+++ 
b/submarine-workbench/workbench-web/src/app/pages/workbench/environment/environment-home/environment-form/environment-form.component.ts
@@ -20,10 +20,8 @@
 import { Component, EventEmitter, OnInit, Output } from '@angular/core';
 import { FormArray, FormBuilder, Validators } from '@angular/forms';
 import { EnvironmentService } from 
'@submarine/services/environment-services/environment.service';
-import { ExperimentValidatorService } from 
'@submarine/services/experiment.validator.service';
 import { NzMessageService } from 'ng-zorro-antd';
-import { UploadChangeParam, UploadFile, UploadListType } from 
'ng-zorro-antd/upload';
-import { BaseApiService } from '@submarine/services/base-api.service';
+import { UploadChangeParam, UploadFile } from 'ng-zorro-antd/upload';
 
 @Component({
   selector: 'submarine-environment-form',
@@ -41,7 +39,6 @@ export class EnvironmentFormComponent implements OnInit {
 
   constructor(
     private fb: FormBuilder,
-    private experimentValidatorService: ExperimentValidatorService,
     private environmentService: EnvironmentService,
     private nzMessageService: NzMessageService
   ) {}
diff --git 
a/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-form/template-form.component.html
 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-form/template-form.component.html
new file mode 100644
index 0000000..c954dfd
--- /dev/null
+++ 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-form/template-form.component.html
@@ -0,0 +1,300 @@
+<!--
+  ~ 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.
+  -->
+
+<nz-modal [(nzVisible)]="isVisible" (nzOnCancel)="isVisible = false" 
nzTitle="Create Template" [nzWidth]="1000">
+  <div [ngSwitch]="step">
+    <div *nzModalFooter>
+      <button
+        nz-button
+        id="btn-tp-form-preStep"
+        style="float: left"
+        nzType="defult"
+        (click)="step = step - 1"
+        *ngIf="step !== 0"
+      >
+        Pre Step
+      </button>
+      <button nz-button id="btn-tp-form-cancel" nzType="default" 
(click)="onCancel()">Cancel</button>
+      <button
+        nz-button
+        id="btn-tp-form-page0"
+        nzType="primary"
+        [disabled]="checkTemplateInfo()"
+        (click)="step = step + 1"
+        *ngIf="step === 0"
+      >
+        Next to experiment spec
+      </button>
+      <button
+        nz-button
+        id="btn-tp-form-page1"
+        nzType="primary"
+        [disabled]="checkExperimentInfo()"
+        (click)="step = step + 1"
+        *ngIf="step === 1"
+      >
+        Next to resource spec
+      </button>
+      <button
+        nz-button
+        id="btn-tp-form-creat"
+        nzType="primary"
+        [disabled]="checkResourceSpec()"
+        (click)="createTemplate()"
+        *ngIf="step === 2"
+      >
+        Creat
+      </button>
+    </div>
+    <form nz-form [formGroup]="templateForm" nzLayout="horizontal">
+      <div *ngSwitchCase="0" style="margin-top: 10px">
+        <nz-form-item>
+          <nz-form-label [nzSm]="6" [nzXs]="24" nzRequired 
nzFor="templateName">Template Name</nz-form-label>
+          <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="Please input 
template name!">
+            <input
+              required
+              nz-input
+              type="text"
+              name="templateName"
+              id="templateName"
+              formControlName="templateName"
+              placeholder="Name of template."
+            />
+          </nz-form-control>
+        </nz-form-item>
+        <nz-form-item>
+          <nz-form-label [nzSm]="6" [nzXs]="24" nzRequired 
nzFor="description">Description</nz-form-label>
+          <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="Please input 
description for this template!">
+            <textarea
+              nz-input
+              [nzAutosize]="{ minRows: 1, maxRows: 4 }"
+              name="description"
+              formControlName="description"
+              id="description"
+              placeholder="Description for this template."
+            ></textarea>
+          </nz-form-control>
+        </nz-form-item>
+        <div formArrayName="parameters">
+          <ng-container *ngFor="let param of parameters.controls; index as i">
+            <nz-form-item>
+              <nz-form-label nzRequired [nzSm]="6" [nzXs]="24">Param{{ i + 1 
}}</nz-form-label>
+              <div [formGroupName]="i">
+                <div nz-col nzSpan="12">
+                  <input
+                    style="width: 50%"
+                    nz-input
+                    required
+                    id="name{{ i }}"
+                    name="name{{ i }}"
+                    placeholder="Name"
+                    formControlName="name"
+                  />
+                  <input
+                    style="width: 40%; margin-left: 10px"
+                    nz-input
+                    required
+                    id="value{{ i }}"
+                    name="value{{ i }}"
+                    placeholder="Value"
+                    formControlName="value"
+                  />
+                  <i
+                    nz-icon
+                    style="margin-left: 5px"
+                    nzType="close-circle"
+                    nzTheme="fill"
+                    (click)="deleteItem(parameters, i)"
+                  ></i>
+                  <br />
+                  <input
+                    style="margin-top: 5px"
+                    nz-input
+                    required
+                    id="description{{ i }}"
+                    name="description{{ i }}"
+                    placeholder="Description"
+                    formControlName="description"
+                  />
+                </div>
+              </div>
+            </nz-form-item>
+          </ng-container>
+        </div>
+        <button
+          nz-button
+          style="display: block; margin: auto"
+          id="btn-addParam"
+          type="default"
+          (click)="onCreateParam()"
+        >
+          Add Param
+        </button>
+      </div>
+      <div *ngSwitchCase="1" style="margin-top: 10px">
+        <nz-form-item>
+          <nz-form-label [nzSm]="6" [nzXs]="24" nzRequired 
nzFor="image">Image</nz-form-label>
+          <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="Please input 
image!">
+            <input
+              required
+              nz-input
+              type="text"
+              name="image"
+              id="image"
+              formControlName="image"
+              placeholder="Image for experiment."
+            />
+          </nz-form-control>
+        </nz-form-item>
+        <nz-form-item>
+          <nz-form-label [nzSm]="6" [nzXs]="24" nzRequired 
nzFor="cmd">Command</nz-form-label>
+          <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="Please input 
command for this template!">
+            <textarea
+              nz-input
+              [nzAutosize]="{ minRows: 1, maxRows: 4 }"
+              name="cmd"
+              formControlName="cmd"
+              id="cmd"
+              placeholder="Command for this template."
+            ></textarea>
+          </nz-form-control>
+        </nz-form-item>
+        <div formArrayName="envVars">
+          <ng-container *ngFor="let env of envVars.controls; index as i">
+            <nz-form-item>
+              <nz-form-label nzRequired [nzSm]="6" [nzXs]="24">Env Var{{ i + 1 
}}</nz-form-label>
+              <div [formGroupName]="i">
+                <div nz-col nzSpan="12">
+                  <input
+                    style="width: 30%"
+                    nz-input
+                    required
+                    id="key{{ i }}"
+                    name="key{{ i }}"
+                    placeholder="key"
+                    formControlName="key"
+                  />
+                  <input
+                    style="width: 60%; margin-left: 10px"
+                    nz-input
+                    required
+                    id="value{{ i }}"
+                    name="value{{ i }}"
+                    placeholder="Value"
+                    formControlName="value"
+                  />
+                  <i
+                    nz-icon
+                    style="margin-left: 5px"
+                    nzType="close-circle"
+                    nzTheme="fill"
+                    (click)="deleteItem(envVars, i)"
+                  ></i>
+                </div>
+              </div>
+            </nz-form-item>
+          </ng-container>
+        </div>
+        <button nz-button style="display: block; margin: auto" id="btn-addEnv" 
type="default" (click)="onCreateEnv()">
+          Add New Environment Variable
+        </button>
+      </div>
+      <div *ngSwitchCase="2" style="margin-top: 10px">
+        <nz-radio-group [(ngModel)]="framework" [ngModelOptions]="{ 
standalone: true }">
+          <label nz-radio nzValue="Tensorflow" (click)="deleteAllItem(specs); 
jobTypes = 'Distributed Tensorflow'">
+            Distributed Tensorflow
+          </label>
+          <label nz-radio nzValue="Pytorch" (click)="deleteAllItem(specs); 
jobType = 'Distributed Pytorch'">
+            Distributed PyTorch
+          </label>
+          <label
+            nz-radio
+            nzValue="Standalone"
+            (click)="deleteAllItem(specs); onCreateSpec(); jobType = 
'Standalone Script'"
+          >
+            Standalone Script
+          </label>
+        </nz-radio-group>
+        <br />
+        <button
+          nz-button
+          *ngIf="framework !== 'Standalone'"
+          id="spec-btn"
+          nzType="default"
+          style="margin-top: 10px"
+          (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 }}" *ngIf="framework !== 'Standalone'">
+                <label>Spec name</label>
+                <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>
+                  <div *ngSwitchCase="'Pytorch'">
+                    <nz-option *ngFor="let spec of PYTORCH_SPECNAMES" 
[nzValue]="spec" [nzLabel]="spec"></nz-option>
+                  </div>
+                </nz-select>
+              </div>
+              <div *ngIf="framework !== 'Standalone'">
+                <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>
+                <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 }}"
+                    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>
+                </div>
+              </div>
+              <i nz-icon nzType="close-circle" nzTheme="fill" 
class="delete-icon" (click)="deleteItem(specs, i)"></i>
+            </li>
+          </ng-container>
+        </ul>
+      </div>
+    </form>
+  </div>
+</nz-modal>
diff --git 
a/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-form/template-form.component.scss
similarity index 60%
copy from 
submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts
copy to 
submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-form/template-form.component.scss
index d88cbf2..5e9e45b 100644
--- 
a/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts
+++ 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-form/template-form.component.scss
@@ -1,4 +1,4 @@
-/*
+/*!
  * 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
@@ -16,25 +16,47 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+ 
+.list-container {
+ padding: 0;
+ margin: 1rem 0;
+}
 
-import { ExperimentSpec } from '@submarine/interfaces/experiment-spec';
+.input-group {
+ display: flex;
+ align-items: center;
+ font-size: .8rem;
+ &:not(:first-child) {
+    margin-top: 1rem;
+ }
 
-interface ExperimentTemplateParamSpec {
-  name: string;
-  required: string;
-  description: string;
-  value: string;
-}
+ & > *:not(:last-child) {
+    margin-right: .8rem;
+ }
+
+ & > div:nth-child(2), & > div:nth-child(3) {
+    flex: 0 1 20%;
+ }
 
-interface ExperimentTemplateSpec {
-  name: string;
-  author: string;
-  description: string;
-  parameters: ExperimentTemplateParamSpec[];
-  experimentSpec: ExperimentSpec;
+ & > div:nth-child(1), & > div:nth-child(4) {
+    flex: 0 1 20%;
+ }
+
+ & i {
+    cursor: pointer;
+    font-size: 20px;
+    margin-top: 20px;
+ }
 }
 
-export interface ExperimentTemplate {
-  experimentTemplateId: string;
-  experimentTemplateSpec: ExperimentTemplateSpec;
+.memory-input-group {
+ display: flex;
+ & input {
+    width: 70%;
+ }
+
+ & > * {
+    width: 30%;
+ }
 }
+
diff --git 
a/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-form/template-form.component.ts
 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-form/template-form.component.ts
new file mode 100644
index 0000000..da0585b
--- /dev/null
+++ 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-form/template-form.component.ts
@@ -0,0 +1,290 @@
+/*
+ * 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, Output, EventEmitter } from '@angular/core';
+import { FormArray, FormControl, FormGroup, Validators, FormBuilder } from 
'@angular/forms';
+import { ExperimentService } from '@submarine/services/experiment.service';
+import { ExperimentValidatorService } from 
'@submarine/services/experiment.validator.service';
+import { Specs } from '@submarine/interfaces/experiment-spec';
+import { ExperimentTemplateSpec } from 
'@submarine/interfaces/experiment-template';
+import { NzMessageService } from 'ng-zorro-antd';
+
+@Component({
+  selector: 'submarine-template-form',
+  templateUrl: './template-form.component.html',
+  styleUrls: ['./template-form.component.scss'],
+})
+export class TemplateFormComponent implements OnInit {
+  @Output() private updater = new EventEmitter<string>();
+
+  step: number = 0;
+
+  defaultExperimentName = '{{experiment_name}}';
+
+  isVisible: boolean;
+
+  templateForm: FormGroup;
+  finaleTemplate;
+
+  jobType = 'Distributed Tensorflow';
+  framework = 'Tensorflow';
+  currentSpecPage = 1;
+  PAGESIZE = 5;
+
+  // Constants
+  TF_SPECNAMES = ['Master', 'Worker', 'Ps'];
+  PYTORCH_SPECNAMES = ['Master', 'Worker'];
+  defaultSpecName = 'worker';
+  MEMORY_UNITS = ['M', 'G'];
+
+  AUTHOR = 'admin';
+  NAMESPACE = 'default';
+
+  constructor(
+    private experimentValidatorService: ExperimentValidatorService,
+    private fb: FormBuilder,
+    private experimentService: ExperimentService,
+    private nzMessageService: NzMessageService
+  ) {}
+
+  ngOnInit() {
+    this.templateForm = this.fb.group({
+      templateName: [null, Validators.required],
+      description: [null, Validators.required],
+      parameters: this.fb.array([], 
[this.experimentValidatorService.nameValidatorFactory('name')]),
+      code: [null],
+      specs: this.fb.array([], 
[this.experimentValidatorService.nameValidatorFactory('name')]),
+      cmd: [null, Validators.required],
+      envVars: this.fb.array([], 
[this.experimentValidatorService.nameValidatorFactory('key')]),
+      image: [null, Validators.required],
+    });
+  }
+
+  get templateName() {
+    return this.templateForm.get('templateName');
+  }
+
+  get description() {
+    return this.templateForm.get('description');
+  }
+
+  get parameters() {
+    return this.templateForm.get('parameters') as FormArray;
+  }
+
+  get code() {
+    return this.templateForm.get('code');
+  }
+
+  get specs() {
+    return this.templateForm.get('specs') as FormArray;
+  }
+
+  get cmd() {
+    return this.templateForm.get('cmd');
+  }
+
+  get envVars() {
+    return this.templateForm.get('envVars') as FormArray;
+  }
+
+  get image() {
+    return this.templateForm.get('image');
+  }
+
+  initModal() {
+    this.isVisible = true;
+    this.initForm();
+  }
+
+  initForm() {
+    this.templateName.reset();
+    this.description.reset();
+    this.parameters.clear();
+    this.specs.clear();
+    this.cmd.reset();
+    this.envVars.clear();
+    this.image.reset();
+  }
+
+  checkTemplateInfo() {
+    return this.templateName.invalid || this.description.invalid || 
this.parameters.invalid;
+  }
+
+  checkExperimentInfo() {
+    return this.image.invalid || this.cmd.invalid || this.envVars.invalid;
+  }
+
+  checkResourceSpec() {
+    return this.specs.invalid || this.specs.length < 1;
+  }
+
+  onCancel() {
+    this.isVisible = false;
+    this.step = 0;
+  }
+
+  createParam(defaultName: string = '', defaultValue: string = '') {
+    return new FormGroup(
+      {
+        name: new FormControl(defaultName, [Validators.required]),
+        value: new FormControl(defaultValue, [Validators.required]),
+        required: new FormControl(true, [Validators.required]),
+        description: new FormControl('', [Validators.required]),
+      },
+      [this.experimentValidatorService.paramValidator]
+    );
+  }
+
+  onCreateParam() {
+    const param = this.createParam();
+    this.parameters.push(param);
+  }
+
+  createEnv(defaultKey: string = '', defaultValue: string = '') {
+    return new FormGroup(
+      {
+        key: new FormControl(defaultKey, [Validators.required]),
+        value: new FormControl(defaultValue, [Validators.required]),
+      },
+      [this.experimentValidatorService.envValidator]
+    );
+  }
+
+  onCreateEnv() {
+    const env = this.createEnv();
+    this.envVars.push(env);
+  }
+
+  createSpec(
+    defaultName: string = 'Worker',
+    defaultReplica: number = 1,
+    defaultCpu: number = 1,
+    defaultGpu: number = 0,
+    defaultMemory: number = 1024,
+    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]),
+        gpus: new FormControl(defaultGpu, [Validators.min(0), 
Validators.required]),
+        memory: new FormGroup(
+          {
+            num: new FormControl(defaultMemory, [Validators.required]),
+            unit: new FormControl(defaultUnit, [Validators.required]),
+          },
+          [this.experimentValidatorService.memoryValidator]
+        ),
+      },
+      [this.experimentValidatorService.specValidator]
+    );
+  }
+
+  onCreateSpec() {
+    const spec = this.createSpec();
+    this.specs.push(spec);
+  }
+
+  deleteItem(arr: FormArray, index: number) {
+    arr.removeAt(index);
+  }
+
+  deleteAllItem(arr: FormArray) {
+    arr.clear();
+  }
+
+  createDefaultParameter() {
+    return new FormGroup({
+      name: new FormControl('experiment_name'),
+      value: new FormControl(null),
+      required: new FormControl(true),
+      description: new FormControl('The name of experiment.'),
+    });
+  }
+
+  constructTemplateSpec() {
+    const defaultParameter = this.createDefaultParameter();
+    this.parameters.push(defaultParameter);
+
+    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},nvidia.com/gpu=${spec.get('gpus').value},memory=${
+            spec.get('memory').get('num').value
+          }${spec.get('memory').get('unit').value}`,
+        };
+      }
+    }
+
+    const envVars = {};
+    for (const envVar of this.envVars.controls) {
+      if (envVar.get('key').value) {
+        envVars[envVar.get('key').value] = envVar.get('value').value;
+      }
+    }
+
+    const newTemplateSpec: ExperimentTemplateSpec = {
+      name: this.templateForm.get('templateName').value,
+      author: this.AUTHOR,
+      description: this.templateForm.get('description').value,
+      parameters: this.templateForm.get('parameters').value,
+      experimentSpec: {
+        meta: {
+          cmd: this.templateForm.get('cmd').value,
+          name: this.defaultExperimentName,
+          envVars: envVars,
+          framework: this.framework,
+          namespace: this.NAMESPACE,
+        },
+        spec: specs,
+        environment: {
+          image: this.templateForm.get('image').value,
+        },
+      },
+    };
+
+    console.log(newTemplateSpec);
+    return newTemplateSpec;
+  }
+
+  createTemplate() {
+    const templateSpec = this.constructTemplateSpec();
+    this.experimentService.createTemplate(templateSpec).subscribe({
+      next: () => {},
+      error: (msg) => {
+        this.nzMessageService.error(`${msg}, please try again`, {
+          nzPauseOnHover: true,
+        });
+      },
+      complete: () => {
+        this.nzMessageService.success('Template creation succeeds');
+        this.isVisible = false;
+        this.sendUpdate('Update Template List');
+      },
+    });
+  }
+
+  sendUpdate(updateInfo: string) {
+    this.updater.emit(updateInfo);
+  }
+}
diff --git 
a/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-home.component.html
 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-home.component.html
new file mode 100644
index 0000000..6d46957
--- /dev/null
+++ 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-home.component.html
@@ -0,0 +1,35 @@
+<!--
+  ~ 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 style="margin: 15px; padding: 15px; background-color: white">
+  <div align="right">
+    <button
+      nz-button
+      id="btn-newTemplate"
+      nzType="primary"
+      style="margin: 10px 4px 10px 4px"
+      (click)="form.initModal()"
+    >
+      <i nz-icon nzType="plus"></i>
+      New Template
+    </button>
+  </div>
+  <submarine-template-list 
[templateList]="templateList"></submarine-template-list>
+  <submarine-template-form #form 
(updater)="updateTemplateList($event)"></submarine-template-form>
+</div>
diff --git 
a/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-home.component.scss
similarity index 62%
copy from 
submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts
copy to 
submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-home.component.scss
index d88cbf2..61d2115 100644
--- 
a/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts
+++ 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-home.component.scss
@@ -1,4 +1,4 @@
-/*
+/*!
  * 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
@@ -16,25 +16,4 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
-import { ExperimentSpec } from '@submarine/interfaces/experiment-spec';
-
-interface ExperimentTemplateParamSpec {
-  name: string;
-  required: string;
-  description: string;
-  value: string;
-}
-
-interface ExperimentTemplateSpec {
-  name: string;
-  author: string;
-  description: string;
-  parameters: ExperimentTemplateParamSpec[];
-  experimentSpec: ExperimentSpec;
-}
-
-export interface ExperimentTemplate {
-  experimentTemplateId: string;
-  experimentTemplateSpec: ExperimentTemplateSpec;
-}
+ 
\ No newline at end of file
diff --git 
a/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-home.component.ts
 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-home.component.ts
new file mode 100644
index 0000000..694f2e3
--- /dev/null
+++ 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-home.component.ts
@@ -0,0 +1,50 @@
+/*
+ * 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, ViewChild } from '@angular/core';
+import { ExperimentTemplate } from '@submarine/interfaces/experiment-template';
+import { ExperimentService } from '@submarine/services/experiment.service';
+import { TemplateFormComponent } from 
'./template-form/template-form.component';
+
+@Component({
+  selector: 'submarine-template-home',
+  templateUrl: './template-home.component.html',
+  styleUrls: ['./template-home.component.scss'],
+})
+export class TemplateHomeComponent implements OnInit {
+  constructor(private experimentService: ExperimentService) {}
+
+  templateList: ExperimentTemplate[];
+
+  @ViewChild('form', { static: true }) form: TemplateFormComponent;
+
+  ngOnInit() {
+    this.fetchTemplateList();
+  }
+
+  fetchTemplateList() {
+    this.experimentService.fetchExperimentTemplateList().subscribe((res) => {
+      this.templateList = res;
+    });
+  }
+
+  updateTemplateList(msg: string) {
+    this.fetchTemplateList();
+  }
+}
diff --git 
a/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-list/template-list.component.html
 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-list/template-list.component.html
new file mode 100644
index 0000000..d20d8cc
--- /dev/null
+++ 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-list/template-list.component.html
@@ -0,0 +1,41 @@
+<!--
+  ~ 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.
+  -->
+
+<nz-table id="templateTable" nzBordered #basicTable [nzData]="templateList" 
[nzNoResult]="'No data'">
+  <thead>
+    <tr>
+      <th>Template Name</th>
+      <th>Framework</th>
+      <th>Description</th>
+      <th>Image</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr *ngFor="let data of basicTable.data; let i = index">
+      <td>
+        <a [routerLink]="['info', data.experimentTemplateSpec.name]">
+          {{ data.experimentTemplateSpec.name }}
+        </a>
+      </td>
+      <td>{{ data.experimentTemplateSpec.experimentSpec.meta.framework }}</td>
+      <td>{{ data.experimentTemplateSpec.description }}</td>
+      <td>{{ data.experimentTemplateSpec.experimentSpec.environment.image 
}}</td>
+    </tr>
+  </tbody>
+</nz-table>
diff --git 
a/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-list/template-list.component.scss
similarity index 62%
copy from 
submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts
copy to 
submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-list/template-list.component.scss
index d88cbf2..61d2115 100644
--- 
a/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts
+++ 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-list/template-list.component.scss
@@ -1,4 +1,4 @@
-/*
+/*!
  * 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
@@ -16,25 +16,4 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
-import { ExperimentSpec } from '@submarine/interfaces/experiment-spec';
-
-interface ExperimentTemplateParamSpec {
-  name: string;
-  required: string;
-  description: string;
-  value: string;
-}
-
-interface ExperimentTemplateSpec {
-  name: string;
-  author: string;
-  description: string;
-  parameters: ExperimentTemplateParamSpec[];
-  experimentSpec: ExperimentSpec;
-}
-
-export interface ExperimentTemplate {
-  experimentTemplateId: string;
-  experimentTemplateSpec: ExperimentTemplateSpec;
-}
+ 
\ No newline at end of file
diff --git 
a/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-list/template-list.component.ts
similarity index 62%
copy from 
submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts
copy to 
submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-list/template-list.component.ts
index d88cbf2..e3393e1 100644
--- 
a/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts
+++ 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-home/template-list/template-list.component.ts
@@ -17,24 +17,18 @@
  * under the License.
  */
 
-import { ExperimentSpec } from '@submarine/interfaces/experiment-spec';
+import { Component, Input, OnInit } from '@angular/core';
+import { ExperimentTemplate } from '@submarine/interfaces/experiment-template';
 
-interface ExperimentTemplateParamSpec {
-  name: string;
-  required: string;
-  description: string;
-  value: string;
-}
+@Component({
+  selector: 'submarine-template-list',
+  templateUrl: './template-list.component.html',
+  styleUrls: ['./template-list.component.scss'],
+})
+export class TemplateListComponent implements OnInit {
+  @Input() templateList: ExperimentTemplate[];
 
-interface ExperimentTemplateSpec {
-  name: string;
-  author: string;
-  description: string;
-  parameters: ExperimentTemplateParamSpec[];
-  experimentSpec: ExperimentSpec;
-}
+  constructor() {}
 
-export interface ExperimentTemplate {
-  experimentTemplateId: string;
-  experimentTemplateSpec: ExperimentTemplateSpec;
+  ngOnInit() {}
 }
diff --git 
a/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-info/template-info.component.html
 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-info/template-info.component.html
new file mode 100644
index 0000000..6ecfb50
--- /dev/null
+++ 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-info/template-info.component.html
@@ -0,0 +1,96 @@
+<!--
+  ~ 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 *ngIf="isLoading == false" style="margin: 15px; padding: 15px; 
background-color: white">
+  <div align="right">
+    <button nz-button id="btn-backHome" nzType="primary" style="margin: 10px 
4px 10px 4px" (click)="backHome()">
+      <i nz-icon nzType="caret-left"></i>
+      Back
+    </button>
+    <button
+      nz-button
+      id="btn-delTemplate"
+      nzType="primary"
+      style="margin: 10px 4px 10px 4px"
+      nz-popconfirm
+      nzPlacement="left"
+      nzTitle="Are you sure you want to delete?"
+      nzCancelText="Cancel"
+      nzOkText="Ok"
+      (nzOnConfirm)="deleteTemplate()"
+    >
+      <i nz-icon nzType="delete"></i>
+      Delete Template
+    </button>
+  </div>
+  <nz-descriptions nzTitle="Template Info" nzBordered [nzColumn]="{ xxl: 4, 
xl: 3, lg: 3, md: 3, sm: 2, xs: 1 }">
+    <nz-descriptions-item nzTitle="Template Name">
+      {{ templateInfo.experimentTemplateSpec.name }}
+    </nz-descriptions-item>
+    <nz-descriptions-item nzTitle="Framework">
+      {{ templateInfo.experimentTemplateSpec.experimentSpec.meta.framework }}
+    </nz-descriptions-item>
+    <nz-descriptions-item nzTitle="Author">
+      {{ templateInfo.experimentTemplateSpec.author }}
+    </nz-descriptions-item>
+    <nz-descriptions-item nzTitle="Description">
+      {{ templateInfo.experimentTemplateSpec.description }}
+    </nz-descriptions-item>
+  </nz-descriptions>
+  <nz-tabset>
+    <nz-tab nzTitle="ExperimentSpec">
+      <nz-descriptions nzTitle="Experiment Spec" nzBordered [nzColumn]="{ xxl: 
4, xl: 3, lg: 3, md: 3, sm: 2, xs: 1 }">
+        <nz-descriptions-item nzTitle="Namespace">
+          {{ templateInfo.experimentTemplateSpec.experimentSpec.meta.namespace 
}}
+        </nz-descriptions-item>
+        <nz-descriptions-item nzTitle="Image">
+          {{ 
templateInfo.experimentTemplateSpec.experimentSpec.environment.image }}
+        </nz-descriptions-item>
+        <nz-descriptions-item nzTitle="Command">
+          {{ templateInfo.experimentTemplateSpec.experimentSpec.meta.cmd }}
+        </nz-descriptions-item>
+        <nz-descriptions-item nzTitle="Environment Varibles">
+          {{ templateVars }}
+        </nz-descriptions-item>
+      </nz-descriptions>
+    </nz-tab>
+    <nz-tab nzTitle="Parameters">
+      <nz-table #basicTable 
[nzData]="templateInfo.experimentTemplateSpec.parameters">
+        <thead>
+          <tr>
+            <th>Name</th>
+            <th>Value</th>
+            <th>Description</th>
+            <th>Required</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr *ngFor="let data of basicTable.data">
+            <td>{{ data.name }}</td>
+            <td>{{ data.value }}</td>
+            <td>{{ data.description }}</td>
+            <td>
+              {{ data.required }}
+            </td>
+          </tr>
+        </tbody>
+      </nz-table>
+    </nz-tab>
+  </nz-tabset>
+</div>
diff --git 
a/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-info/template-info.component.scss
similarity index 62%
copy from 
submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts
copy to 
submarine-workbench/workbench-web/src/app/pages/workbench/template/template-info/template-info.component.scss
index d88cbf2..61d2115 100644
--- 
a/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts
+++ 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-info/template-info.component.scss
@@ -1,4 +1,4 @@
-/*
+/*!
  * 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
@@ -16,25 +16,4 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
-import { ExperimentSpec } from '@submarine/interfaces/experiment-spec';
-
-interface ExperimentTemplateParamSpec {
-  name: string;
-  required: string;
-  description: string;
-  value: string;
-}
-
-interface ExperimentTemplateSpec {
-  name: string;
-  author: string;
-  description: string;
-  parameters: ExperimentTemplateParamSpec[];
-  experimentSpec: ExperimentSpec;
-}
-
-export interface ExperimentTemplate {
-  experimentTemplateId: string;
-  experimentTemplateSpec: ExperimentTemplateSpec;
-}
+ 
\ No newline at end of file
diff --git 
a/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-info/template-info.component.ts
 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-info/template-info.component.ts
new file mode 100644
index 0000000..b4da987
--- /dev/null
+++ 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-info/template-info.component.ts
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { ExperimentTemplate } from '@submarine/interfaces/experiment-template';
+import { ExperimentService } from '@submarine/services/experiment.service';
+import { NzMessageService } from 'ng-zorro-antd';
+
+@Component({
+  selector: 'submarine-template-info',
+  templateUrl: './template-info.component.html',
+  styleUrls: ['./template-info.component.scss'],
+})
+export class TemplateInfoComponent implements OnInit {
+  isLoading = true;
+  templateName;
+  templateInfo: ExperimentTemplate;
+  templateVars: string;
+
+  constructor(
+    private router: Router,
+    private route: ActivatedRoute,
+    private experimentService: ExperimentService,
+    private nzMessageService: NzMessageService
+  ) {}
+
+  ngOnInit() {
+    this.templateName = this.route.snapshot.params.name;
+    this.getTemplateInfo(this.templateName);
+    this.experimentService.emitInfo(this.templateName);
+  }
+
+  getTemplateInfo(name: string) {
+    this.experimentService.querySpecificTemplate(name).subscribe(
+      (item) => {
+        this.templateInfo = item;
+        this.templateVars = 
JSON.stringify(this.templateInfo.experimentTemplateSpec.experimentSpec.meta.envVars);
+        console.log(this.templateInfo.experimentTemplateSpec);
+        this.isLoading = false;
+      },
+      (err) => {
+        this.nzMessageService.error('Cannot load ' + name);
+        this.router.navigate(['/workbench/template']);
+      }
+    );
+  }
+
+  deleteTemplate() {
+    this.experimentService.deleteTemplate(this.templateName).subscribe(
+      () => {
+        this.router.navigate(['/workbench/template']);
+      },
+      (err) => {
+        this.nzMessageService.error(err);
+      }
+    );
+  }
+
+  backHome() {
+    this.router.navigate(['/workbench/template']);
+    this.templateName = null;
+    this.experimentService.emitInfo(this.templateName);
+  }
+}
diff --git 
a/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-routing.module.ts
similarity index 52%
copy from 
submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts
copy to 
submarine-workbench/workbench-web/src/app/pages/workbench/template/template-routing.module.ts
index d88cbf2..59b240e 100644
--- 
a/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts
+++ 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template-routing.module.ts
@@ -17,24 +17,32 @@
  * under the License.
  */
 
-import { ExperimentSpec } from '@submarine/interfaces/experiment-spec';
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { TemplateComponent } from './template.component';
+import { TemplateHomeComponent } from 
'./template-home/template-home.component';
+import { TemplateInfoComponent } from 
'./template-info/template-info.component';
 
-interface ExperimentTemplateParamSpec {
-  name: string;
-  required: string;
-  description: string;
-  value: string;
-}
+const routes: Routes = [
+  {
+    path: '',
+    component: TemplateComponent,
+    children: [
+      {
+        path: '',
+        pathMatch: 'full',
+        component: TemplateHomeComponent,
+      },
+      {
+        path: 'info/:name',
+        component: TemplateInfoComponent,
+      },
+    ],
+  },
+];
 
-interface ExperimentTemplateSpec {
-  name: string;
-  author: string;
-  description: string;
-  parameters: ExperimentTemplateParamSpec[];
-  experimentSpec: ExperimentSpec;
-}
-
-export interface ExperimentTemplate {
-  experimentTemplateId: string;
-  experimentTemplateSpec: ExperimentTemplateSpec;
-}
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule],
+})
+export class TemplateRoutingModule {}
diff --git 
a/submarine-workbench/workbench-web/src/app/pages/workbench/template/template.component.html
 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template.component.html
new file mode 100644
index 0000000..6c6011e
--- /dev/null
+++ 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template.component.html
@@ -0,0 +1,41 @@
+<!--
+  ~ 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.
+  -->
+
+<nz-layout style="margin: -24px -24px 16px">
+  <div style="background-color: white; padding-left: 30px; padding-top: 20px">
+    <nz-breadcrumb>
+      <nz-breadcrumb-item>
+        <a>Home</a>
+      </nz-breadcrumb-item>
+      <nz-breadcrumb-item>
+        <a [routerLink]="['/', 'workbench', 'template']" (click)="templateName 
= null">template</a>
+      </nz-breadcrumb-item>
+      <nz-breadcrumb-item *ngIf="templateName != null">
+        {{ templateName }}
+      </nz-breadcrumb-item>
+    </nz-breadcrumb>
+    <div *ngIf="templateName == null">
+      <br />
+      <h2>Template</h2>
+      <nz-content>Apache submarine support predefined-template for 
experiment.</nz-content>
+    </div>
+    <br />
+  </div>
+  <router-outlet></router-outlet>
+</nz-layout>
diff --git 
a/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template.component.scss
similarity index 62%
copy from 
submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts
copy to 
submarine-workbench/workbench-web/src/app/pages/workbench/template/template.component.scss
index d88cbf2..61d2115 100644
--- 
a/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts
+++ 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template.component.scss
@@ -1,4 +1,4 @@
-/*
+/*!
  * 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
@@ -16,25 +16,4 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
-import { ExperimentSpec } from '@submarine/interfaces/experiment-spec';
-
-interface ExperimentTemplateParamSpec {
-  name: string;
-  required: string;
-  description: string;
-  value: string;
-}
-
-interface ExperimentTemplateSpec {
-  name: string;
-  author: string;
-  description: string;
-  parameters: ExperimentTemplateParamSpec[];
-  experimentSpec: ExperimentSpec;
-}
-
-export interface ExperimentTemplate {
-  experimentTemplateId: string;
-  experimentTemplateSpec: ExperimentTemplateSpec;
-}
+ 
\ No newline at end of file
diff --git 
a/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template.component.ts
similarity index 58%
copy from 
submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts
copy to 
submarine-workbench/workbench-web/src/app/pages/workbench/template/template.component.ts
index d88cbf2..9eea347 100644
--- 
a/submarine-workbench/workbench-web/src/app/interfaces/experiment-template.ts
+++ 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template.component.ts
@@ -17,24 +17,21 @@
  * under the License.
  */
 
-import { ExperimentSpec } from '@submarine/interfaces/experiment-spec';
+import { Component, OnInit } from '@angular/core';
+import { ExperimentService } from '@submarine/services/experiment.service';
+import { delay } from 'rxjs/operators';
 
-interface ExperimentTemplateParamSpec {
-  name: string;
-  required: string;
-  description: string;
-  value: string;
-}
+@Component({
+  selector: 'submarine-template',
+  templateUrl: './template.component.html',
+  styleUrls: ['./template.component.scss'],
+})
+export class TemplateComponent implements OnInit {
+  templateName: string = null;
 
-interface ExperimentTemplateSpec {
-  name: string;
-  author: string;
-  description: string;
-  parameters: ExperimentTemplateParamSpec[];
-  experimentSpec: ExperimentSpec;
-}
+  constructor(private experimentService: ExperimentService) {}
 
-export interface ExperimentTemplate {
-  experimentTemplateId: string;
-  experimentTemplateSpec: ExperimentTemplateSpec;
+  ngOnInit() {
+    this.experimentService.infoEmitted$.pipe(delay(0)).subscribe((name) => 
(this.templateName = name));
+  }
 }
diff --git 
a/submarine-workbench/workbench-web/src/app/pages/workbench/workbench.module.ts 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template.module.ts
similarity index 56%
copy from 
submarine-workbench/workbench-web/src/app/pages/workbench/workbench.module.ts
copy to 
submarine-workbench/workbench-web/src/app/pages/workbench/template/template.module.ts
index b08bdc7..e47d696 100644
--- 
a/submarine-workbench/workbench-web/src/app/pages/workbench/workbench.module.ts
+++ 
b/submarine-workbench/workbench-web/src/app/pages/workbench/template/template.module.ts
@@ -17,40 +17,37 @@
  * under the License.
  */
 
-import { CommonModule } from '@angular/common';
 import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { NgZorroAntdModule } from 'ng-zorro-antd';
+import { TemplateRoutingModule } from './template-routing.module';
 import { RouterModule } from '@angular/router';
-import { WorkbenchRoutingModule } from 
'@submarine/pages/workbench/workbench-routing.module';
+import { TemplateHomeComponent } from 
'./template-home/template-home.component';
+import { TemplateFormComponent } from 
'./template-home/template-form/template-form.component';
+import { TemplateListComponent } from 
'./template-home/template-list/template-list.component';
+import { TemplateComponent } from './template.component';
+import { TemplateInfoComponent } from 
'./template-info/template-info.component';
 import { PipeSharedModule } from '@submarine/pipe/pipe-shared.module';
-import { NgZorroAntdModule } from 'ng-zorro-antd';
-import { WorkspaceModule } from './workspace/workspace.module';
-import { ExperimentModule } from './experiment/experiment.module';
-import { InterpreterModule } from './interpreter/interpreter.module';
-import { NotebookModule } from './notebook/notebook.module';
-
-import { HomeComponent } from './home/home.component';
-import { ModelComponent } from './model/model.component';
-import { WorkbenchComponent } from './workbench.component';
-import { WorkspaceComponent } from './workspace/workspace.component';
-import { DataComponent } from './data/data.component';
-import { EnvironmentModule } from './environment/environment.module';
 
 @NgModule({
-  declarations: [WorkbenchComponent, HomeComponent, WorkspaceComponent, 
DataComponent, ModelComponent],
+  declarations: [
+    TemplateComponent,
+    TemplateHomeComponent,
+    TemplateFormComponent,
+    TemplateListComponent,
+    TemplateInfoComponent,
+  ],
   imports: [
     CommonModule,
-    WorkbenchRoutingModule,
-    NgZorroAntdModule,
-    RouterModule,
     FormsModule,
     ReactiveFormsModule,
-    WorkspaceModule,
-    ExperimentModule,
-    InterpreterModule,
+    NgZorroAntdModule,
+    RouterModule,
+    TemplateRoutingModule,
     PipeSharedModule,
-    NotebookModule,
-    EnvironmentModule,
   ],
+  providers: [],
+  exports: [TemplateComponent],
 })
-export class WorkbenchModule {}
+export class TemplateModule {}
diff --git 
a/submarine-workbench/workbench-web/src/app/pages/workbench/workbench-routing.module.ts
 
b/submarine-workbench/workbench-web/src/app/pages/workbench/workbench-routing.module.ts
index 8232fed..393f75a 100644
--- 
a/submarine-workbench/workbench-web/src/app/pages/workbench/workbench-routing.module.ts
+++ 
b/submarine-workbench/workbench-web/src/app/pages/workbench/workbench-routing.module.ts
@@ -62,6 +62,11 @@ const routes: Routes = [
         canActivate: ['canActivatePage'],
       },
       {
+        path: 'template',
+        loadChildren: () => import('./template/template.module').then((m) => 
m.TemplateModule),
+        canActivate: ['canActivatePage'],
+      },
+      {
         path: 'data',
         component: DataComponent,
         canActivate: ['canActivatePage'],
diff --git 
a/submarine-workbench/workbench-web/src/app/pages/workbench/workbench.component.ts
 
b/submarine-workbench/workbench-web/src/app/pages/workbench/workbench.component.ts
index ec8c801..e778f7f 100644
--- 
a/submarine-workbench/workbench-web/src/app/pages/workbench/workbench.component.ts
+++ 
b/submarine-workbench/workbench-web/src/app/pages/workbench/workbench.component.ts
@@ -40,7 +40,7 @@ interface SidebarMenu {
 @Component({
   selector: 'submarine-workbench',
   templateUrl: './workbench.component.html',
-  styleUrls: ['./workbench.component.scss']
+  styleUrls: ['./workbench.component.scss'],
 })
 export class WorkbenchComponent implements OnInit {
   isCollapsed: boolean = false;
@@ -50,25 +50,31 @@ export class WorkbenchComponent implements OnInit {
       title: 'Home',
       iconType: 'home',
       routerLink: '/workbench/home',
-      disabled: true
+      disabled: true,
     },
     {
       title: 'Notebook',
       iconType: 'book',
       routerLink: '/workbench/notebook',
-      disabled: false
+      disabled: false,
     },
     {
       title: 'Experiment',
       iconType: 'cluster',
       routerLink: '/workbench/experiment',
-      disabled: false
+      disabled: false,
+    },
+    {
+      title: 'Template',
+      iconType: 'file',
+      routerLink: '/workbench/template',
+      disabled: false,
     },
     {
       title: 'Environment',
       iconType: 'codepen',
       routerLink: '/workbench/environment',
-      disabled: false
+      disabled: false,
     },
     {
       title: 'Manager',
@@ -78,44 +84,44 @@ export class WorkbenchComponent implements OnInit {
         {
           title: 'User',
           routerLink: '/workbench/manager/user',
-          disabled: false
+          disabled: false,
         },
         {
           title: 'Data dict',
           routerLink: '/workbench/manager/dataDict',
-          disabled: false
+          disabled: false,
         },
         {
           title: 'Department',
           routerLink: '/workbench/manager/department',
-          disabled: false
-        }
-      ]
+          disabled: false,
+        },
+      ],
     },
     {
       title: 'Data',
       iconType: 'bar-chart',
       routerLink: '/workbench/data',
-      disabled: true
+      disabled: true,
     },
     {
       title: 'Model',
       iconType: 'experiment',
       routerLink: '/workbench/model',
-      disabled: true
+      disabled: true,
     },
     {
       title: 'Workspace',
       iconType: 'desktop',
       routerLink: '/workbench/workspace',
-      disabled: true
+      disabled: true,
     },
     {
       title: 'Interpreter',
       iconType: 'api',
       routerLink: '/workbench/interpreter',
-      disabled: true
-    }
+      disabled: true,
+    },
   ];
   userInfo$: Observable<UserInfo>;
 
diff --git 
a/submarine-workbench/workbench-web/src/app/pages/workbench/workbench.module.ts 
b/submarine-workbench/workbench-web/src/app/pages/workbench/workbench.module.ts
index b08bdc7..7ed7e21 100644
--- 
a/submarine-workbench/workbench-web/src/app/pages/workbench/workbench.module.ts
+++ 
b/submarine-workbench/workbench-web/src/app/pages/workbench/workbench.module.ts
@@ -35,6 +35,7 @@ import { WorkbenchComponent } from './workbench.component';
 import { WorkspaceComponent } from './workspace/workspace.component';
 import { DataComponent } from './data/data.component';
 import { EnvironmentModule } from './environment/environment.module';
+import { TemplateModule } from './template/template.module';
 
 @NgModule({
   declarations: [WorkbenchComponent, HomeComponent, WorkspaceComponent, 
DataComponent, ModelComponent],
@@ -51,6 +52,7 @@ import { EnvironmentModule } from 
'./environment/environment.module';
     PipeSharedModule,
     NotebookModule,
     EnvironmentModule,
+    TemplateModule,
   ],
 })
 export class WorkbenchModule {}
diff --git 
a/submarine-workbench/workbench-web/src/app/services/experiment.service.ts 
b/submarine-workbench/workbench-web/src/app/services/experiment.service.ts
index bd3eaae..5916052 100644
--- a/submarine-workbench/workbench-web/src/app/services/experiment.service.ts
+++ b/submarine-workbench/workbench-web/src/app/services/experiment.service.ts
@@ -22,7 +22,7 @@ import { Injectable } from '@angular/core';
 import { Rest } from '@submarine/interfaces';
 import { ExperimentInfo } from '@submarine/interfaces/experiment-info';
 import { ExperimentSpec } from '@submarine/interfaces/experiment-spec';
-import { ExperimentTemplate } from '@submarine/interfaces/experiment-template';
+import { ExperimentTemplate, ExperimentTemplateSpec } from 
'@submarine/interfaces/experiment-template';
 import { ExperimentTemplateSubmit } from 
'@submarine/interfaces/experiment-template-submit';
 import { TensorboardInfo } from '@submarine/interfaces/tensorboard-info';
 import { MlflowInfo } from '@submarine/interfaces/mlflow-info';
@@ -186,6 +186,19 @@ export class ExperimentService {
     );
   }
 
+  querySpecificTemplate(name: string): Observable<ExperimentTemplate> {
+    const apiUrl = this.baseApi.getRestApi('/v1/template/' + name);
+    return this.httpClient.get<Rest<ExperimentTemplate>>(apiUrl).pipe(
+      switchMap((res) => {
+        if (res.success) {
+          return of(res.result);
+        } else {
+          throw this.baseApi.createRequestError(res.message, res.code, apiUrl, 
'get');
+        }
+      })
+    );
+  }
+
   createExperimentfromTemplate(
     experimentSpec: ExperimentTemplateSubmit,
     templateName: string
@@ -213,6 +226,32 @@ export class ExperimentService {
     );
   }
 
+  createTemplate(templateSpec: ExperimentTemplateSpec): 
Observable<ExperimentTemplate> {
+    const apiUrl = this.baseApi.getRestApi(`/v1/template`);
+    return this.httpClient.post<Rest<ExperimentTemplate>>(apiUrl, 
templateSpec).pipe(
+      switchMap((res) => {
+        if (res.success) {
+          return of(res.result);
+        } else {
+          throw this.baseApi.createRequestError(res.message, res.code, apiUrl, 
'post', templateSpec);
+        }
+      })
+    );
+  }
+
+  deleteTemplate(name: string): Observable<ExperimentTemplate> {
+    const apiUrl = this.baseApi.getRestApi(`/v1/template/${name}`);
+    return this.httpClient.delete<Rest<any>>(apiUrl).pipe(
+      switchMap((res) => {
+        if (res.success) {
+          return of(res.result);
+        } else {
+          throw this.baseApi.createRequestError(res.message, res.code, apiUrl, 
'delete', name);
+        }
+      })
+    );
+  }
+
   getTensorboardInfo(): Observable<TensorboardInfo> {
     const apiUrl = this.baseApi.getRestApi('/v1/experiment/tensorboard');
     return this.httpClient.get<Rest<TensorboardInfo>>(apiUrl).pipe(
diff --git 
a/submarine-workbench/workbench-web/src/app/services/experiment.validator.service.ts
 
b/submarine-workbench/workbench-web/src/app/services/experiment.validator.service.ts
index a2716fb..659490c 100644
--- 
a/submarine-workbench/workbench-web/src/app/services/experiment.validator.service.ts
+++ 
b/submarine-workbench/workbench-web/src/app/services/experiment.validator.service.ts
@@ -21,7 +21,7 @@ import { FormGroup, ValidatorFn, ValidationErrors, FormArray 
} from '@angular/fo
 import { Injectable } from '@angular/core';
 
 @Injectable({
-  providedIn: 'root'
+  providedIn: 'root',
 })
 export class ExperimentValidatorService {
   /**
@@ -34,6 +34,12 @@ export class ExperimentValidatorService {
     return !(key.invalid || keyValue.invalid) ? null : { envMissing: 'Missing 
key or value' };
   };
 
+  paramValidator: ValidatorFn = (paramGroup: FormGroup): ValidationErrors | 
null => {
+    const key = paramGroup.get('name');
+    const keyValue = paramGroup.get('value');
+    return !(key.invalid || keyValue.invalid) ? null : { envMissing: 'Missing 
key or value' };
+  };
+
   specValidator: ValidatorFn = (specGroup: FormGroup): ValidationErrors | null 
=> {
     const name = specGroup.get('name');
     const replicas = specGroup.get('replicas');
@@ -77,7 +83,7 @@ export class ExperimentValidatorService {
         if (duplicateSet.has(nameControl.value)) {
           // Found duplicates, manually set errors on FormControl level
           nameControl.setErrors({
-            duplicateError: 'Duplicate key or name'
+            duplicateError: 'Duplicate key or name',
           });
         } else {
           duplicateSet.add(nameControl.value);

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to