You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@submarine.apache.org by pi...@apache.org on 2020/07/28 12:12:29 UTC

[submarine] branch master updated: SUBMARINE-566. [WEB] Create a new experiment through UI

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

pingsutw pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/submarine.git


The following commit(s) were added to refs/heads/master by this push:
     new 5e8f0e9  SUBMARINE-566. [WEB] Create a new experiment through UI
5e8f0e9 is described below

commit 5e8f0e90823b927ce0785e545bb83da0323d18ad
Author: wang0630 <j2...@gmail.com>
AuthorDate: Thu Jul 23 15:59:40 2020 +0800

    SUBMARINE-566. [WEB] Create a new experiment through UI
    
    ### What is this PR for?
    New experiment creation through UI.
    
    ### What type of PR is it
    [Feature]
    
    ### Todos
    * More user-feedback should be added later
    
    ### What is the Jira issue?
    [SUBMARINE-566](https://issues.apache.org/jira/browse/SUBMARINE-566?filter=-1)
    
    ### How should this be tested?
    https://travis-ci.com/github/wang0630/submarine/jobs/364539196
    
    ### Screenshots (if appropriate)
    ![first](https://user-images.githubusercontent.com/26138982/88267743-d9268e00-cd03-11ea-95d1-a07856c5c0eb.gif)
    
    ### Questions:
    * Does the licenses files need update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    Author: wang0630 <j2...@gmail.com>
    
    Closes #354 from wang0630/SUBMARINE-566 and squashes the following commits:
    
    f25a54b [wang0630] SUBMARINE-566. [WEB] Create a new experiment through UI
    f94fa5b [wang0630] Before refactoring the e2e testing
    dff392a [wang0630] Error handling finished
    c64448b [wang0630] Fire the request succeed
---
 .../server/submitter/k8s/K8sSubmitter.java         |   2 +-
 .../apache/submarine/integration/experimentIT.java |  71 ++++-----
 .../integration/pages/ExperimentPage.java          | 172 +++++++++++++++++++++
 .../src/app/interfaces/experiment-spec.ts          |  29 +++-
 .../workbench/experiment/experiment.component.html |   2 +-
 .../workbench/experiment/experiment.component.scss |   4 -
 .../workbench/experiment/experiment.component.ts   |  91 +++++++++--
 .../src/app/services/base-api.service.ts           |   3 +-
 .../src/app/services/experiment.service.ts         |  23 ++-
 .../app/services/experiment.validator.service.ts   |   4 +-
 10 files changed, 332 insertions(+), 69 deletions(-)

diff --git a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/K8sSubmitter.java b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/K8sSubmitter.java
index 91db0b9..9b495bf 100644
--- a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/K8sSubmitter.java
+++ b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/K8sSubmitter.java
@@ -104,7 +104,7 @@ public class K8sSubmitter implements Submitter {
       experiment = parseResponseObject(object, ParseOp.PARSE_OP_RESULT);
     } catch (InvalidSpecException e) {
       LOG.error("K8s submitter: parse Job object failed by " + e.getMessage(), e);
-      throw new SubmarineRuntimeException(200, e.getMessage());
+      throw new SubmarineRuntimeException(400, e.getMessage());
     } catch (ApiException e) {
       LOG.error("K8s submitter: parse Job object failed by " + e.getMessage(), e);
       throw new SubmarineRuntimeException(e.getCode(), e.getMessage());
diff --git a/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/experimentIT.java b/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/experimentIT.java
index a525749..7dce204 100644
--- a/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/experimentIT.java
+++ b/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/experimentIT.java
@@ -17,25 +17,16 @@
 
 package org.apache.submarine.integration;
 
-import org.apache.commons.io.FileUtils;
 import org.apache.submarine.AbstractSubmarineIT;
 import org.apache.submarine.WebDriverManager;
+import org.apache.submarine.integration.pages.ExperimentPage;
 import org.openqa.selenium.By;
-import org.openqa.selenium.OutputType;
-import org.openqa.selenium.TakesScreenshot;
-import org.openqa.selenium.WebElement;
-import org.openqa.selenium.interactions.Actions;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.testng.Assert;
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
-import org.testng.Assert;
-import org.openqa.selenium.support.ui.WebDriverWait;
-import org.openqa.selenium.support.ui.ExpectedConditions;
-import sun.rmi.runtime.Log;
-
-import java.io.File;
 
 public class experimentIT extends AbstractSubmarineIT {
 
@@ -43,8 +34,8 @@ public class experimentIT extends AbstractSubmarineIT {
 
   @BeforeClass
   public static void startUp(){
-    LOG.info("[Testcase]: experimentIT");
-    driver =  WebDriverManager.getWebDriver();
+    LOG.info("[Testcase]: experimentNew");
+    driver = WebDriverManager.getWebDriver();
   }
 
   @AfterClass
@@ -54,6 +45,8 @@ public class experimentIT extends AbstractSubmarineIT {
 
   @Test
   public void experimentNavigation() throws Exception {
+    // Init the page object
+    ExperimentPage experimentPage = new ExperimentPage(driver);
     // Login
     LOG.info("Login");
     pollingWait(By.cssSelector("input[ng-reflect-name='userName']"), MAX_BROWSER_TIMEOUT_SEC).sendKeys("admin");
@@ -68,35 +61,29 @@ public class experimentIT extends AbstractSubmarineIT {
 
     // Test create new experiment
     LOG.info("new experiment");
-    pollingWait(By.xpath("//button[@id='openExperiment']"), MAX_BROWSER_TIMEOUT_SEC).click();
-    Assert.assertTrue(pollingWait(By.xpath("//form"), MAX_BROWSER_TIMEOUT_SEC).isDisplayed());
-    WebDriverWait wait = new WebDriverWait( driver, 15);
-    // Basic information section
-    pollingWait(By.name("experimentName"), MAX_BROWSER_TIMEOUT_SEC).sendKeys("e2e test Experiment");
-    pollingWait(By.name("description"), MAX_BROWSER_TIMEOUT_SEC).sendKeys("e2e test Project description");
-    pollingWait(By.name("namespace"), MAX_BROWSER_TIMEOUT_SEC).sendKeys("e2e namespace");
-    pollingWait(By.name("cmd"), MAX_BROWSER_TIMEOUT_SEC).sendKeys("python3 -m e2e cmd");
-    pollingWait(By.name("image"), MAX_BROWSER_TIMEOUT_SEC).sendKeys("e2e custom image");
-    pollingWait(By.xpath("//button[@id='go']"), MAX_BROWSER_TIMEOUT_SEC).click();
-    // env variables section
-    LOG.info("in env");
-    Assert.assertTrue(pollingWait(By.xpath("//button[@id='env-btn']"), MAX_BROWSER_TIMEOUT_SEC).isDisplayed());
-    WebElement envBtn = buttonCheck(By.id("env-btn"), MAX_BROWSER_TIMEOUT_SEC);
-    envBtn.click();
-    wait.until(ExpectedConditions.visibilityOfAllElementsLocatedBy(By.xpath("//input[@name='key0' or name='value0']")));
-    pollingWait(By.name("key0"), MAX_BROWSER_TIMEOUT_SEC).sendKeys("e2e key");
-    pollingWait(By.name("value0"), MAX_BROWSER_TIMEOUT_SEC).sendKeys("e2e value");
+    experimentPage.newExperimentButtonClick();
+    experimentPage.fillMeta("good-e2e-test", "e2e des", "default", "python /var/tf_mnist/mnist_with_summaries.py --log_dir=/train/log --learning_rate=0.01 --batch_size=150", "gcr.io/kubeflow-ci/tf-mnist-with-summaries:1.0");
+    Assert.assertTrue(experimentPage.getGoButton().isEnabled());
+    experimentPage.goButtonClick();
+
+    LOG.info("In env");
+    experimentPage.envBtnClick();
+    experimentPage.fillEnv("ENV_1", "ENV1");
+    Assert.assertTrue(experimentPage.getGoButton().isEnabled());
+    experimentPage.goButtonClick();
 
-    pollingWait(By.xpath("//button[@id='go']"), MAX_BROWSER_TIMEOUT_SEC).click();
-    // Spec section
-    LOG.info("in spec");
-    WebElement specBtn = wait.until(ExpectedConditions.elementToBeClickable(By.id("spec-btn")));
-    specBtn.click();
-    pollingWait(By.name("spec0"), MAX_BROWSER_TIMEOUT_SEC).sendKeys("e2e spec");
-    pollingWait(By.name("replica0"), MAX_BROWSER_TIMEOUT_SEC).sendKeys("1");
-    pollingWait(By.name("cpu0"), MAX_BROWSER_TIMEOUT_SEC).sendKeys("1");
-    pollingWait(By.name("memory0"), MAX_BROWSER_TIMEOUT_SEC).sendKeys("512M");
-    Assert.assertTrue(pollingWait(By.xpath("//button[@id='go']"), MAX_BROWSER_TIMEOUT_SEC).isEnabled());
-//    pollingWait(By.xpath("//button[@id='go']"), MAX_BROWSER_TIMEOUT_SEC).click();
+    // Fail due to incorrect spec name
+    LOG.info("In spec fail");
+    experimentPage.fillTfSpec(1, new String[]{"wrong name"}, new int[]{1}, new int[]{1}, new String[]{"512M"});
+    Assert.assertTrue(experimentPage.getGoButton().isEnabled());
+    experimentPage.goButtonClick();
+    Assert.assertTrue(experimentPage.getErrorNotification().isDisplayed());
+    // Successful request
+    LOG.info("In spec success");
+    experimentPage.deleteSpec();
+    Assert.assertEquals(experimentPage.getSpecs(), 0);
+    experimentPage.fillTfSpec(2, new String[]{"Ps", "Worker"}, new int[]{1, 1}, new int[]{1, 1}, new String[]{"1024M", "1024M"});
+    Assert.assertTrue(experimentPage.getGoButton().isEnabled());
+    experimentPage.goButtonClick();
   }
 }
diff --git a/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/pages/ExperimentPage.java b/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/pages/ExperimentPage.java
new file mode 100644
index 0000000..b173ffb
--- /dev/null
+++ b/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/pages/ExperimentPage.java
@@ -0,0 +1,172 @@
+/*
+ * 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.
+ */
+
+package org.apache.submarine.integration.pages;
+
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.PageFactory;
+import org.openqa.selenium.support.pagefactory.AjaxElementLocatorFactory;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+import org.openqa.selenium.support.ui.WebDriverWait;
+
+import java.util.List;
+
+public class ExperimentPage {
+
+  @FindBy(id = "go")
+  private WebElement goButton;
+
+  @FindBy(id = "openExperiment")
+  private WebElement newExperimentButton;
+
+  /*
+  * For svg/path/g element tag, we must use //*[name = 'svg'] to select
+  * //svg will fail
+  */
+  @FindBy(xpath = "//*[name() = 'svg' and @data-icon = 'close-circle']")
+  private List<WebElement> deleteBtns;
+
+  // Meta form
+  @FindBy(name = "experimentName")
+  private WebElement experimentName;
+
+  @FindBy(name = "description")
+  private WebElement description;
+
+  @FindBy(name = "namespace")
+  private WebElement namespace;
+
+  @FindBy(name = "cmd")
+  private WebElement cmd;
+
+  @FindBy(name = "image")
+  private WebElement image;
+
+  // Env form
+  @FindBy(id = "env-btn")
+  private WebElement envBtn;
+
+  @FindBy(xpath = "//input[contains(@name, 'key')]")
+  private WebElement envKey;
+
+  @FindBy(xpath = "//input[contains(@name, 'value')]")
+  private WebElement envValue;
+
+  // Spec
+  @FindBy(id = "spec-btn")
+  private WebElement specBtn;
+
+  @FindBy(xpath = "//input[contains(@name, 'spec')]")
+  private List<WebElement> specNames;
+
+  @FindBy(xpath = "//input[contains(@name, 'replica')]")
+  private List<WebElement> replicas;
+
+  @FindBy(xpath = "//input[contains(@name, 'cpu')]")
+  private List<WebElement> cpus;
+
+  @FindBy(xpath = "//input[contains(@name, 'memory')]")
+  private List<WebElement> memory;
+
+  // Notification
+  @FindBy(xpath = "//div[contains(@class, 'ant-message-error')]//span")
+  private WebElement errorNotification;
+
+  @FindBy(xpath = "//div[contains(@class, 'ant-message-success')]//span")
+  private WebElement successNotification;
+
+  private WebDriverWait wait;
+
+  public ExperimentPage(WebDriver driver) {
+    // NoSuchElementException will be thrown if the elements are not found
+    PageFactory.initElements(new AjaxElementLocatorFactory(driver, 5), this);
+    wait = new WebDriverWait(driver, 15);
+  }
+
+  // Getter
+  public WebElement getGoButton() {
+    return goButton;
+  }
+
+  public WebElement getErrorNotification() {
+    return errorNotification;
+  }
+
+
+  public int getSpecs() {
+    return specNames.size();
+  }
+
+  // button click actions
+  public void goButtonClick() {
+    wait.until(ExpectedConditions.elementToBeClickable(goButton)).click();
+  }
+
+  public void newExperimentButtonClick() {
+    wait.until(ExpectedConditions.elementToBeClickable(newExperimentButton)).click();
+  }
+
+  public void envBtnClick() {
+    wait.until(ExpectedConditions.elementToBeClickable(envBtn)).click();
+  }
+
+  public void specBtnClick() {
+    wait.until(ExpectedConditions.elementToBeClickable(specBtn)).click();
+  }
+
+
+  // Real actions
+  public void fillMeta(String name, String des, String namespaceStr, String cmdStr, String imageStr) {
+    experimentName.clear();
+    experimentName.sendKeys(name);
+    description.clear();
+    description.sendKeys(des);
+    namespace.clear();
+    namespace.sendKeys(namespaceStr);
+    cmd.clear();
+    cmd.sendKeys(cmdStr);
+    image.clear();
+    image.sendKeys(imageStr);
+  }
+
+  public void fillEnv(String key, String value) {
+    envKey.sendKeys(key);
+    envValue.sendKeys(value);
+  }
+
+  public void deleteSpec() {
+    for (WebElement d : deleteBtns) {
+      d.click();
+    }
+  }
+
+  public void fillTfSpec(int specCount, String[] inputNames, int[] replicaCount, int[] cpuCount, String[] inputMemory) {
+    for (int i = 0; i < specCount; i++) {
+      specBtnClick();
+    }
+
+    for (int i = 0; i < specCount; i++) {
+      specNames.get(i).sendKeys(inputNames[i]);
+      replicas.get(i).sendKeys(Integer.toString(replicaCount[i]));
+      cpus.get(i).sendKeys(Integer.toString(cpuCount[i]));
+      memory.get(i).sendKeys(inputMemory[i]);
+    }
+
+  }
+}
diff --git a/submarine-workbench/workbench-web-ng/src/app/interfaces/experiment-spec.ts b/submarine-workbench/workbench-web-ng/src/app/interfaces/experiment-spec.ts
index 98906bf..2ea061b 100644
--- a/submarine-workbench/workbench-web-ng/src/app/interfaces/experiment-spec.ts
+++ b/submarine-workbench/workbench-web-ng/src/app/interfaces/experiment-spec.ts
@@ -17,6 +17,31 @@
  * under the License.
  */
 
-export class ExperimentSpec {
-  // TODO(pingsutw): After refactor submarine experiment spec, we could start implementing it.
+export interface SpecMeta {
+  name: string;
+  namespace: string;
+  framework: string;
+  cmd: string;
+  envVars?: {
+    [key: string]: string;
+  };
+}
+
+export interface SpecEnviroment {
+  image: string;
+}
+
+export interface Specs {
+  [name: string]: {
+    replicas: string;
+    resources: string;
+  };
+}
+
+export interface ExperimentSpec {
+  meta: SpecMeta;
+  environment: {
+    image: string;
+  };
+  spec: Specs;
 }
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.html b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.html
index 39dacd9..480b32e 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.html
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.html
@@ -155,7 +155,7 @@
       </nz-steps>
     </div>
     <div>
-      <form [formGroup]="createExperiment">
+      <form [formGroup]="experiment">
         <div *nzModalFooter>
           <button nz-button nzType="default" (click)="isVisible = false">Cancel</button>
           <button id="go" nz-button nzType="primary" [disabled]="checkStatus()" (click)="handleOk()">
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.scss b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.scss
index 1cc8199..57998b8 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.scss
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.scss
@@ -113,7 +113,3 @@
    flex-direction: column;
    align-items: center;
 }
-
-.pg3-form-label {
-   
-}
\ No newline at end of file
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.ts b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.ts
index 8b07030..37af51c 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.ts
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.ts
@@ -24,6 +24,7 @@ import { ExperimentInfo } from '@submarine/interfaces/experiment-info';
 import { ExperimentService } from '@submarine/services/experiment.service';
 import { ExperimentFormService } from '@submarine/services/experiment.validator.service';
 import { NzMessageService } from 'ng-zorro-antd';
+import { SpecMeta, Specs, SpecEnviroment, ExperimentSpec } from '@submarine/interfaces/experiment-spec';
 
 @Component({
   selector: 'submarine-experiment',
@@ -41,7 +42,7 @@ export class ExperimentComponent implements OnInit {
   searchText = '';
 
   // About new experiment
-  createExperiment: FormGroup;
+  experiment: FormGroup;
   current = 0;
   okText = 'Next Step';
   isVisible = false;
@@ -65,7 +66,7 @@ export class ExperimentComponent implements OnInit {
   ) {}
 
   ngOnInit() {
-    this.createExperiment = new FormGroup({
+    this.experiment = new FormGroup({
       experimentName: new FormControl(null, Validators.required),
       description: new FormControl(null, [Validators.required]),
       // experimentSpec: new FormControl('Adhoc'),
@@ -97,28 +98,28 @@ export class ExperimentComponent implements OnInit {
 
   // Getters of experiment request form
   get experimentName() {
-    return this.createExperiment.get('experimentName');
+    return this.experiment.get('experimentName');
   }
   get description() {
-    return this.createExperiment.get('description');
+    return this.experiment.get('description');
   }
   get frameworks() {
-    return this.createExperiment.get('frameworks');
+    return this.experiment.get('frameworks');
   }
   get namespace() {
-    return this.createExperiment.get('namespace');
+    return this.experiment.get('namespace');
   }
   get cmd() {
-    return this.createExperiment.get('cmd');
+    return this.experiment.get('cmd');
   }
   get envs() {
-    return this.createExperiment.get('envs') as FormArray;
+    return this.experiment.get('envs') as FormArray;
   }
   get image() {
-    return this.createExperiment.get('image');
+    return this.experiment.get('image');
   }
   get specs() {
-    return this.createExperiment.get('specs') as FormArray;
+    return this.experiment.get('specs') as FormArray;
   }
   /**
    * Check the validity of the experiment page
@@ -141,9 +142,38 @@ export class ExperimentComponent implements OnInit {
     }
   }
 
+  /**
+   * Init a new experiment form, clear all status
+   */
+  initExperimentStatus() {
+    this.isVisible = false;
+    this.current = 0;
+    this.okText = 'Next step';
+  }
+
+  /**
+   * Event handler for Next step/Submit button
+   */
   handleOk() {
     if (this.current === 1) {
       this.okText = 'Submit';
+    } else if (this.current === 2) {
+      const newSpec = this.constructSpec();
+      this.experimentService.createExperiment(newSpec).subscribe({
+        next: (result) => {
+          // Must reconstruct a new array for re-rendering
+          this.experimentList = [...this.experimentList, result];
+        },
+        error: (msg) => {
+          this.nzMessageService.error(`${msg}, please try again`, {
+            nzPauseOnHover: true
+          });
+        },
+        complete: () => {
+          this.nzMessageService.success('Experiment creation succeeds');
+          this.initExperimentStatus();
+        }
+      });
     }
 
     if (this.current < 2) {
@@ -191,6 +221,47 @@ export class ExperimentComponent implements OnInit {
   }
 
   /**
+   * Construct spec for new experiment creation
+   */
+  constructSpec(): ExperimentSpec {
+    // Construct the spec
+    const meta: SpecMeta = {
+      name: this.experimentName.value,
+      namespace: this.namespace.value,
+      framework: this.frameworks.value,
+      cmd: this.cmd.value,
+      envVars: {}
+    };
+    for (const env of this.envs.controls) {
+      if (env.get('key').value) {
+        meta.envVars[env.get('key').value] = env.get('value').value;
+      }
+    }
+
+    const specs: Specs = {};
+    for (const spec of this.specs.controls) {
+      if (spec.get('name').value) {
+        specs[spec.get('name').value] = {
+          replicas: spec.get('replicas').value,
+          resources: `cpu=${spec.get('cpus').value},memory=${spec.get('memory').value}`
+        };
+      }
+    }
+
+    const enviroment: SpecEnviroment = {
+      image: this.image.value
+    };
+
+    const newExperimentSpec: ExperimentSpec = {
+      meta: meta,
+      environment: enviroment,
+      spec: specs
+    };
+
+    return newExperimentSpec;
+  }
+
+  /**
    * Delete list items(envs or specs)
    *
    * @param arr - The FormArray containing the item
diff --git a/submarine-workbench/workbench-web-ng/src/app/services/base-api.service.ts b/submarine-workbench/workbench-web-ng/src/app/services/base-api.service.ts
index 86b2710..b9506d8 100644
--- a/submarine-workbench/workbench-web-ng/src/app/services/base-api.service.ts
+++ b/submarine-workbench/workbench-web-ng/src/app/services/base-api.service.ts
@@ -35,7 +35,8 @@ class HttpError extends Error {
     this.params = params;
 
     if (!environment.production) {
-      this.logError();
+      // comment out because weird this behavior
+      // this.logError();
     }
   }
 
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
index 41876ae..18ab660 100644
--- a/submarine-workbench/workbench-web-ng/src/app/services/experiment.service.ts
+++ b/submarine-workbench/workbench-web-ng/src/app/services/experiment.service.ts
@@ -22,8 +22,8 @@ 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';
+import { of, Observable, throwError } from 'rxjs';
+import { switchMap, catchError, map } from 'rxjs/operators';
 
 @Injectable({
   providedIn: 'root'
@@ -61,12 +61,23 @@ export class ExperimentService {
   createExperiment(experimentSpec): Observable<ExperimentInfo> {
     const apiUrl = this.baseApi.getRestApi('/v1/experiment');
     return this.httpClient.post<Rest<ExperimentInfo>>(apiUrl, experimentSpec).pipe(
-      switchMap((res) => {
-        if (res.success) {
-          return of(res.result);
+      map((res) => res.result), // return result directly if succeeding
+      catchError((e) => {
+        let message: string;
+        if (e.error instanceof ErrorEvent) {
+          // client side error
+          message = 'Something went wrong with network or workbench';
         } else {
-          throw this.baseApi.createRequestError(res.message, res.code, apiUrl, 'post', experimentSpec);
+          console.log(e);
+          if (e.status === 409) {
+            message = 'You might have a duplicate experiment name';
+          } else if (e.status >= 500) {
+            message = `${e.message}`;
+          } else {
+            message = e.error.message;
+          }
         }
+        return throwError(message);
       })
     );
   }
diff --git a/submarine-workbench/workbench-web-ng/src/app/services/experiment.validator.service.ts b/submarine-workbench/workbench-web-ng/src/app/services/experiment.validator.service.ts
index b9b2461..923e98b 100644
--- a/submarine-workbench/workbench-web-ng/src/app/services/experiment.validator.service.ts
+++ b/submarine-workbench/workbench-web-ng/src/app/services/experiment.validator.service.ts
@@ -56,8 +56,8 @@ export class ExperimentFormService {
    * @param memory - The memory field in Spec
    */
   memoryValidator: ValidatorFn = (memory: FormControl): ValidationErrors | null => {
-    // Must match number + digit ex. 512M
-    return memory.value && /^\d+M$/.test(memory.value)
+    // Must match number + digit ex. 512M or empty
+    return !memory.value || /^\d+M$/.test(memory.value)
       ? null
       : { memoryPatternError: 'Memory pattern must match number + M ex. 512M' };
   };


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