You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@submarine.apache.org by li...@apache.org on 2020/05/13 04:55:10 UTC

[submarine] branch master updated: SUBMARINE-495. [WEB] Submarine experiment list/delete in workbench with Angular

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 b6aeed0  SUBMARINE-495. [WEB] Submarine experiment list/delete in workbench with Angular
b6aeed0 is described below

commit b6aeed05f235b44aca33d094ae3476ccc2f4842d
Author: pingsutw <pi...@gmail.com>
AuthorDate: Mon May 11 20:50:49 2020 +0800

    SUBMARINE-495. [WEB] Submarine experiment list/delete in workbench with Angular
    
    ### What is this PR for?
    Implement list/delete experiment in workbench with Angular.
    will start working on submit/edit, after submarine experiment spec is refactored.
    
    ### What type of PR is it?
    [Improvement]
    
    ### Todos
    * [ ] - Task
    
    ### What is the Jira issue?
    https://issues.apache.org/jira/browse/SUBMARINE-495
    
    ### How should this be tested?
    https://travis-ci.org/github/pingsutw/hadoop-submarine/builds/685730718
    
    ### Screenshots (if appropriate)
    ![Untitled_-May-11_-2020-10_39-PM_1](https://user-images.githubusercontent.com/37936015/81586972-f4624c80-93e8-11ea-8a41-41e00cffe2a9.gif)
    
    ### Questions:
    * Does the licenses files need update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    Author: pingsutw <pi...@gmail.com>
    
    Closes #283 from pingsutw/SUBMARINE-495 and squashes the following commits:
    
    90bde6f [pingsutw] SUBMARINE-495. [WEB] Submarine experiment list/delete in workbench with Angular
---
 .../submarine/server/rest/JobManagerRestApi.java   |  18 ++--
 .../integration/{jobIT.java => experimentIT.java}  |  21 ++--
 .../apache/submarine/integration/sidebarIT.java    |   6 +-
 .../experiment-info.ts}                            |  20 ++--
 .../experiment-spec.ts}                            |  10 +-
 .../experiment.component.html}                     |  89 ++++++++++------
 .../experiment.component.scss}                     |   4 +-
 .../workbench/experiment/experiment.component.ts   | 116 +++++++++++++++++++++
 .../experiment.module.ts}                          |   2 +-
 .../src/app/pages/workbench/job/job.component.ts   | 103 ------------------
 .../pages/workbench/workbench-routing.module.ts    |   6 +-
 .../src/app/pages/workbench/workbench.component.ts |   4 +-
 .../src/app/pages/workbench/workbench.module.ts    |   9 +-
 .../src/app/services/experiment.service.ts         |  87 ++++++++++++++++
 14 files changed, 310 insertions(+), 185 deletions(-)

diff --git a/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/JobManagerRestApi.java b/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/JobManagerRestApi.java
index f468717..ef26e40 100644
--- a/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/JobManagerRestApi.java
+++ b/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/JobManagerRestApi.java
@@ -68,7 +68,7 @@ public class JobManagerRestApi {
   public Response createJob(JobSpec spec) {
     try {
       Job job = jobManager.createJob(spec);
-      return new JsonResponse.Builder<Job>(Response.Status.OK).result(job).build();
+      return new JsonResponse.Builder<Job>(Response.Status.OK).success(true).result(job).build();
     } catch (SubmarineRuntimeException e) {
       return parseJobServiceException(e);
     }
@@ -82,7 +82,7 @@ public class JobManagerRestApi {
   public Response listJob(@QueryParam("status") String status) {
     try {
       List<Job> jobList = jobManager.listJobsByStatus(status);
-      return new JsonResponse.Builder<List<Job>>(Response.Status.OK).result(jobList).build();
+      return new JsonResponse.Builder<List<Job>>(Response.Status.OK).success(true).result(jobList).build();
     } catch (SubmarineRuntimeException e) {
       return parseJobServiceException(e);
     }
@@ -98,7 +98,7 @@ public class JobManagerRestApi {
   public Response getJob(@PathParam(RestConstants.JOB_ID) String id) {
     try {
       Job job = jobManager.getJob(id);
-      return new JsonResponse.Builder<Job>(Response.Status.OK).result(job).build();
+      return new JsonResponse.Builder<Job>(Response.Status.OK).success(true).result(job).build();
     } catch (SubmarineRuntimeException e) {
       return parseJobServiceException(e);
     }
@@ -127,20 +127,20 @@ public class JobManagerRestApi {
   public Response deleteJob(@PathParam(RestConstants.JOB_ID) String id) {
     try {
       Job job = jobManager.deleteJob(id);
-      return new JsonResponse.Builder<Job>(Response.Status.OK)
+      return new JsonResponse.Builder<Job>(Response.Status.OK).success(true)
           .result(job).build();
     } catch (SubmarineRuntimeException e) {
       return parseJobServiceException(e);
     }
   }
-  
+
   @GET
   @Path("/logs")
   public Response listLog(@QueryParam("status") String status) {
     try {
       List<JobLog> jobLogList = jobManager.listJobLogsByStatus(status);
-      return new JsonResponse.Builder<List<JobLog>>(Response.Status.OK).
-          result(jobLogList).build();
+      return new JsonResponse.Builder<List<JobLog>>(Response.Status.OK).success(true)
+          .result(jobLogList).build();
 
     } catch (SubmarineRuntimeException e) {
       return parseJobServiceException(e);
@@ -152,8 +152,8 @@ public class JobManagerRestApi {
   public Response getLog(@PathParam(RestConstants.JOB_ID) String id) {
     try {
       JobLog jobLog = jobManager.getJobLog(id);
-      return new JsonResponse.Builder<JobLog>(Response.Status.OK).
-          result(jobLog).build();
+      return new JsonResponse.Builder<JobLog>(Response.Status.OK).success(true)
+          .result(jobLog).build();
 
     } catch (SubmarineRuntimeException e) {
       return parseJobServiceException(e);
diff --git a/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/jobIT.java b/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/experimentIT.java
similarity index 73%
rename from submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/jobIT.java
rename to submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/experimentIT.java
index aebe245..c7d1509 100644
--- a/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/jobIT.java
+++ b/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/experimentIT.java
@@ -19,7 +19,6 @@ package org.apache.submarine.integration;
 
 import org.apache.submarine.AbstractSubmarineIT;
 import org.apache.submarine.WebDriverManager;
-import org.apache.submarine.SubmarineITUtils;
 import org.openqa.selenium.By;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -28,9 +27,9 @@ import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 import org.testng.Assert;
 
-public class jobIT extends AbstractSubmarineIT {
+public class experimentIT extends AbstractSubmarineIT {
 
-  public final static Logger LOG = LoggerFactory.getLogger(jobIT.class);
+  public final static Logger LOG = LoggerFactory.getLogger(experimentIT.class);
 
   @BeforeClass
   public static void startUp(){
@@ -53,19 +52,19 @@ public class jobIT extends AbstractSubmarineIT {
     pollingWait(By.cssSelector("a[routerlink='/workbench/dashboard']"), MAX_BROWSER_TIMEOUT_SEC);
 
     // Routing to workspace
-    pollingWait(By.xpath("//span[contains(text(), \"Job\")]"), MAX_BROWSER_TIMEOUT_SEC).click();
-    Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8080/workbench/job");
+    pollingWait(By.xpath("//span[contains(text(), \"Experiment\")]"), MAX_BROWSER_TIMEOUT_SEC).click();
+    Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8080/workbench/experiment");
 
-    // Test create new job
-    pollingWait(By.xpath("//button[@id='openJob']"), MAX_BROWSER_TIMEOUT_SEC).click();
-    Assert.assertEquals(pollingWait(By.xpath("//form"), MAX_BROWSER_TIMEOUT_SEC).isDisplayed(), true);
-    pollingWait(By.xpath("//input[@id='jobname']"), MAX_BROWSER_TIMEOUT_SEC).sendKeys("e2e test Job");
+    // Test create new experiment
+    pollingWait(By.xpath("//button[@id='openExperiment']"), MAX_BROWSER_TIMEOUT_SEC).click();
+    Assert.assertTrue(pollingWait(By.xpath("//form"), MAX_BROWSER_TIMEOUT_SEC).isDisplayed());
+    pollingWait(By.xpath("//input[@id='experimentName']"), MAX_BROWSER_TIMEOUT_SEC).sendKeys("e2e test Experiment");
     pollingWait(By.xpath("//textarea"), MAX_BROWSER_TIMEOUT_SEC).sendKeys("e2e test Project description");
     pollingWait(By.xpath("//button[@id='go']"), MAX_BROWSER_TIMEOUT_SEC).click();
     //Next step
-    Assert.assertEquals(pollingWait(By.xpath("//div[@id='page2']"), MAX_BROWSER_TIMEOUT_SEC).isDisplayed(), true);
+    Assert.assertTrue(pollingWait(By.xpath("//div[@id='page2']"), MAX_BROWSER_TIMEOUT_SEC).isDisplayed());
     pollingWait(By.xpath("//button[@id='go']"), MAX_BROWSER_TIMEOUT_SEC).click();
-    Assert.assertEquals(pollingWait(By.xpath("//label[@class='pg3-form-label']"), MAX_BROWSER_TIMEOUT_SEC).isDisplayed(), true);
+    Assert.assertTrue(pollingWait(By.xpath("//label[@class='pg3-form-label']"), MAX_BROWSER_TIMEOUT_SEC).isDisplayed());
     pollingWait(By.xpath("//button[@id='go']"), MAX_BROWSER_TIMEOUT_SEC).click();
   }
 }
diff --git a/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/sidebarIT.java b/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/sidebarIT.java
index cfee7a2..796ab78 100644
--- a/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/sidebarIT.java
+++ b/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/sidebarIT.java
@@ -60,8 +60,8 @@ public class sidebarIT extends AbstractSubmarineIT {
     Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8080/workbench/workspace");
     pollingWait(By.xpath("//span[contains(text(), \"Interpreter\")]"), MAX_BROWSER_TIMEOUT_SEC).click();
     Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8080/workbench/interpreter");
-    pollingWait(By.xpath("//span[contains(text(), \"Job\")]"), MAX_BROWSER_TIMEOUT_SEC).click();
-    Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8080/workbench/job");
+    pollingWait(By.xpath("//span[contains(text(), \"Experiment\")]"), MAX_BROWSER_TIMEOUT_SEC).click();
+    Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8080/workbench/experiment");
     pollingWait(By.xpath("//span[contains(text(), \"Data\")]"), MAX_BROWSER_TIMEOUT_SEC).click();
     Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8080/workbench/data");
     pollingWait(By.xpath("//span[contains(text(), \"Model\")]"), MAX_BROWSER_TIMEOUT_SEC).click();
@@ -78,6 +78,6 @@ public class sidebarIT extends AbstractSubmarineIT {
     pollingWait(By.xpath("//a[@href='/workbench/manager/dataDict']"), MAX_BROWSER_TIMEOUT_SEC).click();
     Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8080/workbench/manager/dataDict");
     pollingWait(By.xpath("//span[contains(text(), \"Home\")]"), MAX_BROWSER_TIMEOUT_SEC).click();
-    Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8080/workbench/home"); 
+    Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8080/workbench/home");
   }
 }
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/job/job.module.ts b/submarine-workbench/workbench-web-ng/src/app/interfaces/experiment-info.ts
similarity index 73%
copy from submarine-workbench/workbench-web-ng/src/app/pages/workbench/job/job.module.ts
copy to submarine-workbench/workbench-web-ng/src/app/interfaces/experiment-info.ts
index 256726e..8f7227c 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/job/job.module.ts
+++ b/submarine-workbench/workbench-web-ng/src/app/interfaces/experiment-info.ts
@@ -1,6 +1,3 @@
-import { NgModule } from "@angular/core";
-import { ReactiveFormsModule } from '@angular/forms';
-
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
@@ -20,7 +17,16 @@ import { ReactiveFormsModule } from '@angular/forms';
  * under the License.
  */
 
-@NgModule({
-   exports: [ReactiveFormsModule]
-})
-export class JobModule {}
+import { ExperimentSpec } from '@submarine/interfaces/experiment-spec';
+
+export class ExperimentInfo {
+  jobId: string;
+  name: string;
+  uid: string;
+  status: string;
+  acceptedTime: string;
+  createdTime: string;
+  runningTime: string;
+  finishedTime: string;
+  spec: ExperimentSpec;
+}
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/job/job.module.ts b/submarine-workbench/workbench-web-ng/src/app/interfaces/experiment-spec.ts
similarity index 82%
copy from submarine-workbench/workbench-web-ng/src/app/pages/workbench/job/job.module.ts
copy to submarine-workbench/workbench-web-ng/src/app/interfaces/experiment-spec.ts
index 256726e..1f239da 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/job/job.module.ts
+++ b/submarine-workbench/workbench-web-ng/src/app/interfaces/experiment-spec.ts
@@ -1,6 +1,3 @@
-import { NgModule } from "@angular/core";
-import { ReactiveFormsModule } from '@angular/forms';
-
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
@@ -20,7 +17,6 @@ import { ReactiveFormsModule } from '@angular/forms';
  * under the License.
  */
 
-@NgModule({
-   exports: [ReactiveFormsModule]
-})
-export class JobModule {}
+export class ExperimentSpec {
+   // TODO(pingsutw): After refactor submarine experiment spec, we could start implementing it.
+}
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/job/job.component.html b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.html
similarity index 64%
rename from submarine-workbench/workbench-web-ng/src/app/pages/workbench/job/job.component.html
rename to submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.html
index cf3594f..82fc8d1 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/job/job.component.html
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.html
@@ -19,22 +19,22 @@
 
 <nz-layout style="margin: -24px -24px 16px;">
   <nz-layout class="inner-layout">
-    <div id="jobOuter">
+    <div id="experimentOuter">
       <nz-breadcrumb>
         <nz-breadcrumb-item>
           <a [routerLink]="['/', 'workbench', 'home']">Home</a>
         </nz-breadcrumb-item>
-        <nz-breadcrumb-item>Job</nz-breadcrumb-item>
+        <nz-breadcrumb-item>experiment</nz-breadcrumb-item>
       </nz-breadcrumb>
       <br/>
-      <h2>Job</h2>
-      <nz-content>A job is a way of running a notebook or on a scheduled basis.</nz-content>
+      <h2>Experiment</h2>
+      <nz-content>A experiment is a way of running a adhoc, pre-defined or pipeline on a scheduled basis.</nz-content>
       <br/>
     </div>
   </nz-layout>
-  <div id="jobData">
+  <div id="experimentData">
     <div align="right">
-      <nz-radio-group [(ngModel)]="showJob" (ngModelChange)="showChange()">
+      <nz-radio-group [(ngModel)]="showExperiment" (ngModelChange)="showChange()">
         <label nz-radio-button nzValue="All">All</label>
         <label nz-radio-button nzValue="Own">Owned By Me</label>
         <label nz-radio-button nzValue="Access">Accessable By Me</label>
@@ -45,45 +45,68 @@
       <ng-template #suffixIconButton>
         <button nz-button nzType="primary" nzSearch><i nz-icon nzType="search"></i></button>
       </ng-template>
-        
-      <button nz-button id="openJob" nzType="primary" style="margin-right: 30px;margin-bottom: 15px;margin-top: 15px;" (click)="isVisible=true;"><i nz-icon nzType="plus"></i>New Job</button>
-    </div>  
-    <nz-table id="releaseTable" nzBordered #basicTable [nzData]="joblist" [nzNoResult]="'No data'">
+
+      <button nz-button id="openExperiment" nzType="primary" style="margin-right: 30px;margin-bottom: 15px;margin-top: 15px;" (click)="isVisible=true;"><i nz-icon nzType="plus"></i>New Experiment</button>
+    </div>
+    <nz-table id="releaseTable" nzBordered #basicTable [nzData]="experimentList" [nzNoResult]="'No data'">
       <thead>
         <tr>
-          <th nzWidth="60px">Job Name</th>
-          <th nzWidth="20px">Job ID</th>
-          <th nzWidth="40px">Owner</th>
-          <th nzWidth="60px">Actuator</th>
-          <th nzWidth="60px">Status</th>
-          <th nzWidth="60px">Progress</th>
-          <th nzWidth="60px">Last Run</th>
-          <th nzWidth="60px">Action</th>
+          <th>Experiment Name</th>
+          <th>Experiment ID</th>
+          <th>Created Time</th>
+          <th>Running Time</th>
+          <th>Finished Time</th>
+          <th>Status</th>
+          <th>Progress</th>
+          <th nzMinWidth="40px">Action</th>
         </tr>
       </thead>
       <tbody>
         <tr *ngFor="let data of basicTable.data">
           <td>{{ data.name }}</td>
-          <td>{{ data.id }}</td>
-          <td>{{ data.owner }}</td>
-          <td>{{ data.actuator }}</td>
+          <td>{{ data.jobId }}</td>
+          <td>{{ data.createdTime }}</td>
+          <td>{{ data.runningTime }}</td>
+          <td>{{ data.finishedTime }}</td>
           <td>{{ data.status }}</td>
-          <td><nz-progress nzStrokeLinecap="round" [nzPercent]="data.progress"></nz-progress></td>
-          <td>{{ data.lastRun }}</td>
-          <td><button nz-button nzType="link" (click)="startJob(data)" style="padding-left: 2px;padding-right: 5px;">Start</button>|<button nz-button nzType="link" (click)="editJob(data)" style="padding-left: 2px;padding-right: 5px;">Edit</button></td>
+          <td *ngIf="data.status === 'Succeeded'"><nz-progress nzStrokeLinecap="round" [nzPercent]="100"></nz-progress></td>
+          <td *ngIf="data.status !== 'Succeeded'"><nz-progress nzStrokeLinecap="round" [nzPercent]="50"></nz-progress></td>
+          <td class="td-action" nzRight="0px">
+            <a (click)="startExperiment(data)">Start</a>
+            <a nz-dropdown [nzDropdownMenu]="more">
+              More
+              <i nz-icon nzType="down"></i>
+            </a>
+            <nz-dropdown-menu #more="nzDropdownMenu">
+              <ul nz-menu nzSelectable>
+                <li nz-menu-item (click)="editExperiment(data)">Edit</li>
+                <li
+                  nz-menu-item
+                  nz-popconfirm
+                  nzPlacement="left"
+                  nzTitle="Confirm to delete?"
+                  nzCancelText="Cancel"
+                  nzOkText="Ok"
+                  (nzOnConfirm)="onDeleteExperiment(data)"
+                >
+                  Delete
+                </li>
+              </ul>
+            </nz-dropdown-menu>
+          </td>
         </tr>
       </tbody>
     </nz-table>
   </div>
   <nz-modal
       [(nzVisible)]="isVisible"
-      nzTitle="Create Job"
+      nzTitle="Create Experiment"
       [(nzOkText)]="okText"
       [nzOkLoading]="isOkLoading"
       (nzOnCancel)="isVisible=false;"
       [nzWidth]="740"
   >
-    <div >
+    <div>
       <nz-steps [nzCurrent]="current">
         <nz-step nzTitle="Basic Information"></nz-step>
         <nz-step nzTitle="Configuration"></nz-step>
@@ -91,17 +114,17 @@
       </nz-steps>
     </div>
     <div>
-      <form [formGroup]="createJob">
+      <form [formGroup]="createExperiment">
         <div *nzModalFooter>
           <button nz-button nzType="default" (click)="isVisible=false;"> Cancel </button>
-          <button id="go" nz-button nzType="primary" [disabled]="!(createJob.get('jobName').valid && createJob.get('description').valid)" (click)="handleOk()">{{okText}}</button>
+          <button id="go" nz-button nzType="primary" [disabled]="!(createExperiment.get('experimentName').valid && createExperiment.get('description').valid)" (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>
-              <label class="form-label"><span class="red-star">* </span> Job Name:</label>
-              <input type="text" id="jobname" style="margin-top: 32px;width: 350px;" class="form-control" formControlName="jobName">
+              <label class="form-label"><span class="red-star">* </span> Experiment Name:</label>
+              <input type="text" id="experimentName" style="margin-top: 32px;width: 350px;" class="form-control" formControlName="experimentName">
             </div>
             <div>
               <label class="form-label"><span class="red-star">* </span>Description:</label>
@@ -110,9 +133,9 @@
           </div>
           <div *ngSwitchCase="1" id="page2">
             <div>
-              <label class="form-label"><span class="red-star">* </span> Monitor Object:</label>
-              <nz-select formControlName="monitorObject" nzPlaceHolder="Choose" style="width: 350px;margin-top: 30px;">
-                <nz-option *ngFor="let monitorObject of monitorObjects" [nzValue]="monitorObject" [nzLabel]="monitorObject"></nz-option>
+              <label class="form-label"><span class="red-star">* </span> Experiment Spec:</label>
+              <nz-select formControlName="experimentSpec" nzPlaceHolder="Choose" style="width: 350px;margin-top: 30px;">
+                <nz-option *ngFor="let experimentSpec of ExperimentSpecs" [nzValue]="experimentSpec" [nzLabel]="experimentSpec"></nz-option>
               </nz-select>
             </div>
             <div>
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/job/job.component.scss b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.scss
similarity index 97%
rename from submarine-workbench/workbench-web-ng/src/app/pages/workbench/job/job.component.scss
rename to submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.scss
index 64d6f27..3838a1b 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/job/job.component.scss
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.scss
@@ -17,13 +17,13 @@
  * under the License.
  */
 
- #jobOuter{
+ #experimentOuter{
     background-color: white;
     padding-left: 30px;
     padding-top: 20px;
  }
 
- #jobData{
+ #experimentData{
     margin-top: 16px;
     margin-left: 25px;
     margin-right: 25px;
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
new file mode 100644
index 0000000..3935628
--- /dev/null
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.ts
@@ -0,0 +1,116 @@
+/*!
+ * 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 { FormControl, FormGroup, Validators } from '@angular/forms';
+import { ExperimentInfo } from '@submarine/interfaces/experiment-info';
+import { ExperimentService } from '@submarine/services/experiment.service';
+import { NzMessageService } from 'ng-zorro-antd';
+
+@Component({
+  selector: 'submarine-experiment',
+  templateUrl: './experiment.component.html',
+  styleUrls: ['./experiment.component.scss']
+})
+export class ExperimentComponent implements OnInit {
+
+  experimentList: ExperimentInfo[] = [];
+  // About show existing experiments
+  showExperiment = 'All';
+  searchText = '';
+
+  // About new experiment
+  createExperiment: FormGroup;
+  current = 0;
+  okText = 'Next Step';
+  isVisible = false;
+
+  ExperimentSpecs = ['Adhoc', 'Predefined'];
+  ruleTemplates = ['Template1', 'Template2'];
+  ruleTypes = ['Strong', 'Weak'];
+  scheduleCycles = ['Month', 'Week'];
+
+  constructor(
+    private experimentService: ExperimentService,
+    private nzMessageService: NzMessageService
+  ) { }
+
+  ngOnInit() {
+    this.createExperiment =  new FormGroup({
+      'experimentName': new FormControl(null, Validators.required),
+      'description': new FormControl(null, [Validators.required]),
+      'experimentSpec': new FormControl('Adhoc'),
+      'ruleTemplate': new FormControl('Template1'),
+      'ruleType': new FormControl('Strong'),
+      'startDate': new FormControl(new Date()),
+      'scheduleCycle': new FormControl('Month')
+    });
+    this.fetchExperimentList();
+  }
+
+  handleOk() {
+    if (this.current === 1) {
+      this.okText = 'Submit';
+      this.current++;
+    } else if (this.current === 2) {
+      this.okText = 'Next Step';
+      this.current = 0;
+      this.isVisible = false;
+      // TODO(jasoonn): Create Real experiment
+      console.log(this.createExperiment);
+    } else {
+      this.current++;
+    }
+  }
+
+  fetchExperimentList() {
+    this.experimentService
+      .fetchExperimentList()
+      .subscribe((  list ) => {
+        this.experimentList = list;
+      });
+  }
+  onDeleteExperiment(data: ExperimentInfo) {
+    this.experimentService.deleteExperiment(data.jobId).subscribe(
+      () => {
+        this.nzMessageService.success('Delete user success!');
+        this.fetchExperimentList();
+      }, err => {
+        this.nzMessageService.success(err.message);
+      }
+    );
+  }
+
+  // TODO(jasoonn): Filter experiment list
+  filter(event) {
+    console.log(this.searchText + event.key);
+  }
+  // TODO(jasoonn): Perform part of list
+  showChange() {
+    console.log("Change to " + this.showExperiment);
+  }
+  // TODO(jasoonn): Start experiment
+  startExperiment(Experiment) {
+    console.log(Experiment);
+  }
+  // TODO(jasoonn): Edit experiment
+  editExperiment(Experiment) {
+    console.log(Experiment);
+  }
+}
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/job/job.module.ts b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.module.ts
similarity index 96%
rename from submarine-workbench/workbench-web-ng/src/app/pages/workbench/job/job.module.ts
rename to submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.module.ts
index 256726e..042d3c9 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/job/job.module.ts
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.module.ts
@@ -23,4 +23,4 @@ import { ReactiveFormsModule } from '@angular/forms';
 @NgModule({
    exports: [ReactiveFormsModule]
 })
-export class JobModule {}
+export class ExperimentModule {}
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/job/job.component.ts b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/job/job.component.ts
deleted file mode 100644
index 0c2e1b4..0000000
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/job/job.component.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-/*!
- * 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 { FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
-
-@Component({
-  selector: 'submarine-job',
-  templateUrl: './job.component.html',
-  styleUrls: ['./job.component.scss']
-})
-export class JobComponent implements OnInit {
-
-  //About show existing jobs
-  showJob = 'All';
-  searchText = '';
-  joblist=[
-    {
-      name: 'Spark actuator',
-      id: 1,
-      owner: 'Frank',
-      actuator: 'Spark Actuator',
-      status: 'Running',
-      progress: 85,
-      lastRun: '2009-09-24 20:38:24'
-    }
-  ]
-  //About new Job
-  createJob: FormGroup;
-  current = 0;
-  okText = 'Next Step';
-  isVisible = false;
-
-  monitorObjects = ['Table1', 'Table2'];
-  ruleTemplates = ['Template1', 'Template2'];
-  ruleTypes = ['Strong', 'Weak'];
-
-  scheduleCycles = ['Month', 'Week'];
-
-  constructor() { }
-
-  ngOnInit() {
-    this.createJob =  new FormGroup({
-      'jobName': new FormControl(null, Validators.required),
-      'description': new FormControl(null, [Validators.required]),
-      'monitorObject': new FormControl('Table1'),
-      'ruleTemplate': new FormControl('Template1'),
-      'ruleType': new FormControl('Strong'),
-      'startDate': new FormControl(new Date()),
-      'scheduleCycle': new FormControl('Month')
-    });
-  }
-
-  handleOk(){
-    if (this.current === 1){
-      this.okText = 'Complete';
-      this.current++;
-    }
-    else if (this.current === 2){
-      this.okText = 'Next Step';
-      this.current = 0;
-      this.isVisible = false;
-      //TODO(jasoonn): Create Real Job
-      console.log(this.createJob);
-    }
-    else {
-      this.current++;
-    }
-  }
-
-  //TODO(jasoonn): Filter Job list
-  filter(event){
-    console.log(this.searchText+event.key);
-  }
-  //TODO(jasoonn): Perfrom part of list
-  showChange(){
-    console.log("Change to " + this.showJob);
-  }
-  //TODO(jasoonn): Start Job
-  startJob(job){
-    console.log(job);
-  }
-  //TODO(jasoonn): Edit job
-  editJob(job){
-    console.log(job);
-  }
-}
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/workbench-routing.module.ts b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/workbench-routing.module.ts
index c9a5579..a4d6326 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/workbench-routing.module.ts
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/workbench-routing.module.ts
@@ -19,11 +19,11 @@
 
 import { NgModule } from '@angular/core';
 import { RouterModule, Routes } from '@angular/router';
+import { ExperimentComponent } from '@submarine/pages/workbench/experiment/experiment.component';
 import { WorkbenchComponent } from '@submarine/pages/workbench/workbench.component';
 import { DataComponent } from './data/data.component';
 import { HomeComponent } from './home/home.component';
 import { InterpreterComponent } from './interpreter/interpreter.component';
-import { JobComponent } from './job/job.component';
 import { ModelComponent } from './model/model.component';
 import { WorkspaceComponent } from './workspace/workspace.component';
 
@@ -50,8 +50,8 @@ const routes: Routes = [
         component: InterpreterComponent
       },
       {
-        path: 'job',
-        component: JobComponent
+        path: 'experiment',
+        component: ExperimentComponent
       },
       {
         path: 'data',
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/workbench.component.ts b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/workbench.component.ts
index fd452ab..0f5ab4b 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/workbench.component.ts
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/workbench.component.ts
@@ -60,9 +60,9 @@ export class WorkbenchComponent implements OnInit {
       routerLink: '/workbench/interpreter'
     },
     {
-      title: 'Job',
+      title: 'Experiment',
       iconType: 'cluster',
-      routerLink: '/workbench/job'
+      routerLink: '/workbench/experiment'
     },
     {
       title: 'Data',
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 3d85b23..2573f12 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
@@ -24,10 +24,11 @@ import { RouterModule } from '@angular/router';
 import { WorkbenchRoutingModule } from '@submarine/pages/workbench/workbench-routing.module';
 import { NgZorroAntdModule } from 'ng-zorro-antd';
 import { DataComponent } from './data/data.component';
+import { ExperimentComponent } from './experiment/experiment.component';
+import { ExperimentModule } from './experiment/experiment.module';
+
 import { HomeComponent } from './home/home.component';
 import { InterpreterModule } from './interpreter/interpreter.module';
-import { JobComponent } from './job/job.component';
-import { JobModule } from './job/job.module';
 import { ModelComponent } from './model/model.component';
 import { WorkbenchComponent } from './workbench.component';
 import { WorkspaceComponent } from './workspace/workspace.component';
@@ -38,7 +39,7 @@ import { WorkspaceModule } from './workspace/workspace.module';
     WorkbenchComponent,
     HomeComponent,
     WorkspaceComponent,
-    JobComponent,
+    ExperimentComponent,
     DataComponent,
     ModelComponent
   ],
@@ -49,7 +50,7 @@ import { WorkspaceModule } from './workspace/workspace.module';
     RouterModule,
     FormsModule,
     WorkspaceModule,
-    JobModule,
+    ExperimentModule,
     InterpreterModule
   ]
 })
diff --git a/submarine-workbench/workbench-web-ng/src/app/services/experiment.service.ts b/submarine-workbench/workbench-web-ng/src/app/services/experiment.service.ts
new file mode 100644
index 0000000..beb972f
--- /dev/null
+++ b/submarine-workbench/workbench-web-ng/src/app/services/experiment.service.ts
@@ -0,0 +1,87 @@
+/*
+ * 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 { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Rest } from '@submarine/interfaces';
+import { ExperimentInfo } from '@submarine/interfaces/experiment-info';
+import { BaseApiService } from '@submarine/services/base-api.service';
+import { of, Observable } from 'rxjs';
+import { switchMap } from 'rxjs/operators';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class ExperimentService {
+  constructor(private baseApi: BaseApiService, private httpClient: HttpClient) {
+  }
+
+  fetchExperimentList(): Observable<ExperimentInfo[]> {
+    const apiUrl = this.baseApi.getRestApi('/v1/jobs');
+    return this.httpClient.get<Rest<ExperimentInfo[]>>(apiUrl).pipe(
+      switchMap(res => {
+        if (res.success) {
+          console.log(res.result);
+          return of(res.result);
+        } else {
+          throw this.baseApi.createRequestError(res.message, res.code, apiUrl, 'get');
+        }
+      })
+    );
+  }
+
+  createExperiment(experimentSpec): Observable<ExperimentInfo> {
+    const apiUrl = this.baseApi.getRestApi('/v1/jobs');
+    return this.httpClient.post<Rest<ExperimentInfo>>(apiUrl, experimentSpec).pipe(
+      switchMap(res => {
+        if (res.success) {
+          return of(res.result);
+        } else {
+          throw this.baseApi.createRequestError(res.message, res.code, apiUrl, 'post', experimentSpec);
+        }
+      })
+    );
+  }
+
+  editExperiment(experimentSpec): Observable<ExperimentInfo> {
+    const apiUrl = this.baseApi.getRestApi('/v1/jobs');
+    return this.httpClient.patch<Rest<ExperimentInfo>>(apiUrl, experimentSpec).pipe(
+      switchMap(res => {
+        if (res.success) {
+          return of(res.result);
+        } else {
+          throw this.baseApi.createRequestError(res.message, res.code, apiUrl, 'patch', experimentSpec);
+        }
+      })
+    );
+  }
+
+  deleteExperiment(id: string): Observable<ExperimentInfo> {
+    const apiUrl = this.baseApi.getRestApi('/v1/jobs/' + id);
+    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', id);
+        }
+      })
+    );
+  }
+}


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