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 c0a19e5 SUBMARINE-585. [WEB] Edit existing experiments through UI
c0a19e5 is described below
commit c0a19e5bf49da826859745b50227c588f6966a31
Author: wang0630 <[email protected]>
AuthorDate: Mon Aug 3 00:53:15 2020 +0800
SUBMARINE-585. [WEB] Edit existing experiments through UI
### What is this PR for?
Allow editing experiments through UI.
### What type of PR is it?
[Feature]
### Todos
* [ ] - Task
### What is the Jira issue?
[SUBMARINE-585](https://issues.apache.org/jira/browse/SUBMARINE-585)
### How should this be tested?
https://travis-ci.com/github/wang0630/submarine/builds/178169720
### Screenshots (if appropriate)

### Questions:
* Does the licenses files need update? No
* Is there breaking changes for older versions? No
* Does this needs documentation? No
Author: wang0630 <[email protected]>
Closes #368 from wang0630/SUBMARINE-585 and squashes the following commits:
d164857 [wang0630] SUBMARINE-585. [WEB] Edit existing experiments through UI
96069c1 [wang0630] Finish form layout
7118c9d [wang0630] Patch finished
b3cea1f [wang0630] Open and edit logic finished
9e1b749 [wang0630] Basic edit logic
---
.../apache/submarine/integration/experimentIT.java | 23 ++-
.../integration/pages/ExperimentPage.java | 77 +++++++--
.../src/app/interfaces/experiment-spec.ts | 1 +
.../workbench/experiment/experiment.component.html | 106 +++++++-----
.../workbench/experiment/experiment.component.scss | 36 ++--
.../workbench/experiment/experiment.component.ts | 185 +++++++++++++++------
.../src/app/services/experiment.service.ts | 28 +++-
.../app/services/experiment.validator.service.ts | 19 +--
8 files changed, 334 insertions(+), 141 deletions(-)
diff --git
a/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/experimentIT.java
b/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/experimentIT.java
index a450f82..356f481 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
@@ -69,8 +69,9 @@ public class experimentIT extends AbstractSubmarineIT {
// Test create new experiment
LOG.info("new experiment");
+ String experimentName = "experiment-e2e-test";
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");
+ experimentPage.fillMeta(experimentName, "e2e des", "default", "python
/var/tf_mnist/mnist_with_summaries.py --log_dir=/train/log --learning_rate=0.01
--batch_size=150", "gcr.io/kubeflow-ci/tf-mnist-with-summaries:1.0");
Assert.assertTrue(experimentPage.getGoButton().isEnabled());
experimentPage.goButtonClick();
@@ -82,16 +83,22 @@ public class experimentIT extends AbstractSubmarineIT {
// 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());
+ experimentPage.fillTfSpec(1, new String[]{"Master"}, new int[]{-1}, new
int[]{-1}, new int[]{512});
+ Assert.assertFalse(experimentPage.getGoButton().isEnabled());
// 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"});
+ experimentPage.fillTfSpec(2, new String[]{"Ps", "Worker"}, new int[]{1,
1}, new int[]{1, 1}, new int[]{1024, 1024});
Assert.assertTrue(experimentPage.getGoButton().isEnabled());
- experimentPage.goButtonClick();
+ /*
+ TODO: Launch submarine server and K8s in e2e-test
+ Comment out because of Experiment creation failure on Travis
+
+ experimentPage.goButtonClick();
+
+ // Patch request
+ LOG.info("In spec patch");
+ experimentPage.editTfSpec(experimentName);
+ */
}
}
diff --git
a/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/pages/ExperimentPage.java
b/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/pages/ExperimentPage.java
index b173ffb..6e70db0 100644
---
a/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/pages/ExperimentPage.java
+++
b/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/pages/ExperimentPage.java
@@ -17,18 +17,27 @@
package org.apache.submarine.integration.pages;
+import org.junit.Assert;
+import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Actions;
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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.List;
public class ExperimentPage {
+
+ @FindBy(id = "experimentData")
+ private WebElement dataSection;
+
@FindBy(id = "go")
private WebElement goButton;
@@ -72,8 +81,8 @@ public class ExperimentPage {
@FindBy(id = "spec-btn")
private WebElement specBtn;
- @FindBy(xpath = "//input[contains(@name, 'spec')]")
- private List<WebElement> specNames;
+ @FindBy(xpath = "//div[contains(@id, 'spec')]//span")
+ private List<WebElement> specNamesSelect;
@FindBy(xpath = "//input[contains(@name, 'replica')]")
private List<WebElement> replicas;
@@ -84,6 +93,9 @@ public class ExperimentPage {
@FindBy(xpath = "//input[contains(@name, 'memory')]")
private List<WebElement> memory;
+ @FindBy(css = "ul.ant-select-dropdown-menu-root")
+ private WebElement dropDownMenu;
+
// Notification
@FindBy(xpath = "//div[contains(@class, 'ant-message-error')]//span")
private WebElement errorNotification;
@@ -91,12 +103,14 @@ public class ExperimentPage {
@FindBy(xpath = "//div[contains(@class, 'ant-message-success')]//span")
private WebElement successNotification;
+ public final static Logger LOG =
LoggerFactory.getLogger(ExperimentPage.class);
private WebDriverWait wait;
+ private Actions action;
public ExperimentPage(WebDriver driver) {
- // NoSuchElementException will be thrown if the elements are not found
- PageFactory.initElements(new AjaxElementLocatorFactory(driver, 5), this);
+ PageFactory.initElements(new AjaxElementLocatorFactory(driver, 10), this);
wait = new WebDriverWait(driver, 15);
+ action = new Actions(driver);
}
// Getter
@@ -104,15 +118,6 @@ public class ExperimentPage {
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();
@@ -130,7 +135,6 @@ public class ExperimentPage {
wait.until(ExpectedConditions.elementToBeClickable(specBtn)).click();
}
-
// Real actions
public void fillMeta(String name, String des, String namespaceStr, String
cmdStr, String imageStr) {
experimentName.clear();
@@ -156,17 +160,56 @@ public class ExperimentPage {
}
}
- public void fillTfSpec(int specCount, String[] inputNames, int[]
replicaCount, int[] cpuCount, String[] inputMemory) {
+ public void fillTfSpec(int specCount, String[] inputNames, int[]
replicaCount, int[] cpuCount, int[] inputMemory) {
for (int i = 0; i < specCount; i++) {
specBtnClick();
}
+
for (int i = 0; i < specCount; i++) {
- specNames.get(i).sendKeys(inputNames[i]);
+ // Click the dropdown menu
+ action.moveToElement(specNamesSelect.get(i)).click().perform();
+ wait.until(ExpectedConditions.visibilityOf(dropDownMenu));
+ String optionStr = ".//li[text()[contains(.,'" + inputNames[i] + "')]]";
+ WebElement option = dropDownMenu.findElement(By.xpath(optionStr));
+ action.moveToElement(option).click().perform();
+ // Rest of the inputs
+ replicas.get(i).clear();
replicas.get(i).sendKeys(Integer.toString(replicaCount[i]));
+ cpus.get(i).clear();
cpus.get(i).sendKeys(Integer.toString(cpuCount[i]));
- memory.get(i).sendKeys(inputMemory[i]);
+ memory.get(i).sendKeys(Integer.toString(inputMemory[i]));
}
+ }
+ public void editTfSpec(String targetName) {
+ String nameFieldStr = "//td[text()[contains(.,'" + targetName + "')]]";
+ String editButtonStr = ".//tbody" + nameFieldStr +
"//following-sibling::td[@class='td-action']/a[1]";
+ LOG.info(editButtonStr);
+ WebElement editButton = dataSection.findElement(By.xpath(editButtonStr));
+ editButton.click();
+ goButtonClick();
+ goButtonClick();
+ for (WebElement m : memory) {
+ m.clear();
+ m.sendKeys("512");
+ }
+
+ Assert.assertTrue(getGoButton().isEnabled());
+ goButtonClick();
+ /*
+ Must wait until the whole page is loaded, or StaleElementReference will
be thrown
+ dataSection element is destroyed due to page reloading
+ */
+ wait.until(ExpectedConditions.visibilityOf(dataSection));
+ editButton = dataSection.findElement(By.xpath(editButtonStr));
+ wait.until(ExpectedConditions.elementToBeClickable(editButton));
+ editButton.click();
+ goButtonClick();
+ goButtonClick();
+ for (WebElement m : memory) {
+ String v = m.getAttribute("value");
+ Assert.assertEquals(v, "512");
+ }
}
}
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 2ea061b..e10ceb6 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
@@ -19,6 +19,7 @@
export interface SpecMeta {
name: string;
+ description?: string;
namespace: string;
framework: string;
cmd: string;
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 04a37e7..3f383bb 100644
---
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.html
+++
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.html
@@ -63,7 +63,7 @@
id="openExperiment"
nzType="primary"
style="margin-right: 5px; margin-bottom: 15px; margin-top: 15px;"
- (click)="isVisible = true"
+ (click)="initExperimentStatus('create')"
>
<i nz-icon nzType="plus"></i>
New Experiment
@@ -121,28 +121,18 @@
<td>{{ data.runningTime | date: 'M/d/yyyy, h:mm a' }}</td>
<td>{{ data.duration }}</td>
<td class="td-action">
- <a (click)="startExperiment(data)">Start</a>
+ <a (click)="onUpdateExperiment(data.experimentId,
data.spec)">Update</a>
<nz-divider nzType="vertical"></nz-divider>
- <a nz-dropdown [nzDropdownMenu]="more">
- More
- <i nz-icon nzType="down"></i>
+ <a
+ nz-popconfirm
+ nzPlacement="left"
+ nzTitle="Confirm to delete?"
+ nzCancelText="Cancel"
+ nzOkText="Ok"
+ (nzOnConfirm)="onDeleteExperiment(data.experimentId, true)"
+ >
+ Delete
</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, true)"
- >
- Delete
- </li>
- </ul>
- </nz-dropdown-menu>
</td>
</tr>
</tbody>
@@ -154,7 +144,6 @@
[(nzVisible)]="isVisible"
nzTitle="Create Experiment"
[(nzOkText)]="okText"
- [nzOkLoading]="isOkLoading"
(nzOnCancel)="isVisible = false"
[nzWidth]="1000"
>
@@ -193,7 +182,6 @@
</div>
<div class="single-field-group">
<label for="description">
- <span class="red-star">*</span>
Description
</label>
<textarea
@@ -211,7 +199,7 @@
</label>
<nz-select formControlName="frameworks" nzPlaceHolder="Choose"
style="width: 48%;">
<nz-option
- *ngFor="let framework of frameworkNames"
+ *ngFor="let framework of FRAMEWORK_NAMES"
[nzValue]="framework"
[nzLabel]="framework"
></nz-option>
@@ -241,14 +229,20 @@
</div>
<div *ngSwitchCase="1" id="page2">
<div>
- <button nz-button id="env-btn" type="default"
(click)="createEnvInput()">
+ <button nz-button id="env-btn" type="default"
(click)="onCreateEnv()">
Add new env
</button>
<ul formArrayName="envs" class="list-container">
<ng-container *ngFor="let env of envs.controls; index as i">
<li *ngIf="i | indexInRange: currentEnvPage:PAGESIZE"
[formGroupName]="i" class="input-group">
- <input nz-input name="key{{ i }}" placeholder="Key"
formControlName="key" />
- <input nz-input name="value{{ i }}" placeholder="Value"
formControlName="value" />
+ <div>
+ <label for="key{{ i }}">Key</label>
+ <input nz-input name="key{{ i }}" placeholder="Key"
formControlName="key" />
+ </div>
+ <div>
+ <label for="value{{ i }}">Value</label>
+ <input nz-input name="value{{ i }}" placeholder="Value"
formControlName="value" />
+ </div>
<i nz-icon nzType="close-circle" nzTheme="fill"
(click)="deleteItem(envs, i)"></i>
</li>
<div
@@ -275,22 +269,52 @@
</div>
<div *ngSwitchCase="2">
<label class="pg3-form-label"></label>
- <button nz-button id="spec-btn" type="default"
(click)="createSpec()">
+ <button nz-button id="spec-btn" type="default"
(click)="onCreateSpec()">
Add new spec
</button>
<ul formArrayName="specs" class="list-container">
<ng-container *ngFor="let spec of specs.controls; index as i">
<li *ngIf="i | indexInRange: currentSpecPage:PAGESIZE"
[formGroupName]="i" class="input-group">
- <input nz-input name="spec{{ i }}" placeholder="spec name"
formControlName="name" />
- <input
- nz-input
- name="replica{{ i }}"
- type="number"
- placeholder="number of replica"
- formControlName="replicas"
- />
- <input nz-input name="cpu{{ i }}" type="number"
placeholder="number of cpu" formControlName="cpus" />
- <input nz-input name="memory{{ i }}" placeholder="memory"
formControlName="memory" />
+ <div id="spec{{ i }}">
+ <label>Spec name</label>
+ <nz-select formControlName="name" nzPlaceHolder="Spec
name" [ngSwitch]="frameworks.value">
+ <div *ngSwitchCase="'Tensorflow'">
+ <nz-option *ngFor="let spec of TF_SPECNAMES"
[nzValue]="spec" [nzLabel]="spec"></nz-option>
+ </div>
+ <div *ngSwitchCase="'Pytorch'">
+ <nz-option *ngFor="let spec of PYTORCH_SPECNAMES"
[nzValue]="spec" [nzLabel]="spec"></nz-option>
+ </div>
+ </nz-select>
+ </div>
+ <div>
+ <label>Number of Replica</label>
+ <input
+ nz-input
+ name="replica{{ i }}"
+ type="number"
+ placeholder="number of replica"
+ formControlName="replicas"
+ />
+ </div>
+ <div>
+ <label>Number of cpu</label>
+ <input
+ nz-input
+ name="cpu{{ i }}"
+ type="number"
+ placeholder="number of cpu"
+ formControlName="cpus"
+ />
+ </div>
+ <div id="memory{{ i }}">
+ <label>Memory</label>
+ <div formGroupName="memory" class="memory-input-group">
+ <input nz-input name="memory{{ i }}" placeholder="Enter
number" formControlName="num" />
+ <nz-select formControlName="unit">
+ <nz-option *ngFor="let unit of MEMORY_UNITS"
[nzValue]="unit" [nzLabel]="unit"></nz-option>
+ </nz-select>
+ </div>
+ </div>
<i
nz-icon
nzType="close-circle"
@@ -301,6 +325,12 @@
</li>
<div
class="alert-message"
+ *ngIf="spec.get('name').dirty | logicalAnd:
spec.get('memory').dirty:spec.hasError('specError')"
+ >
+ {{ spec.getError('specError') }}
+ </div>
+ <div
+ class="alert-message"
*ngIf="spec.get('name').dirty | logicalAnd:
spec.get('name').hasError('duplicateError')"
>
{{ spec.get('name').getError('duplicateError') }}
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 57998b8..ac1dc47 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
@@ -64,20 +64,38 @@
.input-group {
display: flex;
align-items: center;
+ font-size: .8rem;
&:not(:first-child) {
margin-top: 1rem;
}
- & input {
- flex: 0 2 20%;
- &:not(:last-child) {
- margin-right: .5rem;
- }
+ & > *:not(:last-child) {
+ margin-right: .8rem;
+ }
+
+ & > div:nth-child(2), & > div:nth-child(3) {
+ flex: 0 1 20%;
+ }
+
+ & > div:nth-child(1), & > div:nth-child(4) {
+ flex: 0 1 25%;
}
& i {
cursor: pointer;
- font-size: 1.2rem;
+ font-size: 20px;
+ margin-top: 20px;
+ }
+}
+
+.memory-input-group {
+ display: flex;
+ & input {
+ width: 70%;
+ }
+
+ & > * {
+ width: 30%;
}
}
@@ -108,8 +126,4 @@
margin-left: .5rem;
}
-.test {
- display: flex;
- flex-direction: column;
- align-items: center;
-}
+/* For memory */
\ No newline at end of file
diff --git
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.ts
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.ts
index d950cd5..e360cce 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
@@ -50,8 +50,14 @@ export class ExperimentComponent implements OnInit {
okText = 'Next Step';
isVisible = false;
- // ExperimentSpecs = ['Adhoc', 'Predefined'];
- frameworkNames = ['Tensorflow', 'Pytorch'];
+ // About update
+ mode: 'create' | 'update' = 'create';
+ updateId: string = null;
+
+ FRAMEWORK_NAMES = ['Tensorflow', 'Pytorch'];
+ TF_SPECNAMES = ['Master', 'Worker', 'Ps'];
+ PYTORCH_SPECNAMES = ['Master', 'Worker'];
+ MEMORY_UNITS = ['M', 'G'];
// About env page
currentEnvPage = 1;
@@ -72,10 +78,8 @@ export class ExperimentComponent implements OnInit {
this.experiment = new FormGroup({
experimentName: new FormControl(null, Validators.required),
description: new FormControl(null, [Validators.required]),
- // experimentSpec: new FormControl('Adhoc'),
frameworks: new FormControl('Tensorflow', [Validators.required]),
namespace: new FormControl('default', [Validators.required]),
- // ruleType: new FormControl('Strong'),
cmd: new FormControl('', [Validators.required]),
envs: new FormArray([],
[this.experimentFormService.nameValidatorFactory('key')]),
image: new FormControl('', [Validators.required]),
@@ -132,7 +136,6 @@ export class ExperimentComponent implements OnInit {
if (this.current === 0) {
return (
this.experimentName.invalid ||
- this.description.invalid ||
this.frameworks.invalid ||
this.namespace.invalid ||
this.cmd.invalid ||
@@ -146,12 +149,20 @@ export class ExperimentComponent implements OnInit {
}
/**
- * Init a new experiment form, clear all status
+ * Init a new experiment form, clear all status, clear all form controls and
open the form in the mode specified in the argument
+ *
+ * @param mode - The mode which the form should open in
*/
- initExperimentStatus() {
- this.isVisible = false;
+ initExperimentStatus(mode: 'create' | 'update') {
+ this.mode = mode;
this.current = 0;
this.okText = 'Next step';
+ this.isVisible = true;
+ this.updateId = null;
+ // Reset the form
+ this.envs.clear();
+ this.specs.clear();
+ this.experiment.reset({ frameworks: 'Tensorflow', namespace: 'default' });
}
/**
@@ -161,22 +172,49 @@ export class ExperimentComponent implements OnInit {
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.mode === 'create') {
+ 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.isVisible = false;
+ }
+ });
+ } else if (this.mode === 'update') {
+ const newSpec = this.constructSpec();
+ this.experimentService.updateExperiment(this.updateId,
newSpec).subscribe(
+ (result) => {
+ // Find the old index
+ const index = this.experimentList.findIndex(
+ (experiment) => experiment.experimentId === result.experimentId
+ );
+ // Create a new list, meanwhile maintaining the order
+ this.experimentList = [
+ ...this.experimentList.slice(0, index),
+ result,
+ ...this.experimentList.slice(index + 1)
+ ];
+ },
+ (msg) => {
+ this.nzMessageService.error(`${msg}, please try again`, {
+ nzPauseOnHover: true
+ });
+ },
+ () => {
+ this.nzMessageService.success('Modification succeeds!');
+ this.isVisible = false;
+ }
+ );
+ }
}
if (this.current < 2) {
@@ -187,35 +225,60 @@ export class ExperimentComponent implements OnInit {
/**
* Create a new env variable input
*/
- createEnvInput() {
+ createEnv(defaultKey: string = '', defaultValue: string = '') {
// Create a new FormGroup
- const env = new FormGroup(
+ return new FormGroup(
{
- key: new FormControl(''),
- value: new FormControl()
+ key: new FormControl(defaultKey, [Validators.required]),
+ value: new FormControl(defaultValue, [Validators.required])
},
[this.experimentFormService.envValidator]
);
- this.envs.push(env);
- // If the new page is created, jump to that page
- if (this.envs.controls.length > 1 && this.envs.controls.length %
this.PAGESIZE === 1) {
- this.currentEnvPage += 1;
- }
}
/**
* Create a new spec
- *
*/
- createSpec() {
- const spec = new FormGroup(
+ createSpec(
+ defaultName: string = '',
+ defaultReplica: number = 1,
+ defaultCpu: number = 1,
+ defaultMemory: string = '',
+ defaultUnit: string = 'M'
+ ): FormGroup {
+ return new FormGroup(
{
- name: new FormControl(''),
- replicas: new FormControl(null, [Validators.min(1)]),
- cpus: new FormControl(null, [Validators.min(1)]),
- memory: new FormControl('',
[this.experimentFormService.memoryValidator])
+ name: new FormControl(defaultName, [Validators.required]),
+ replicas: new FormControl(defaultReplica, [Validators.min(1),
Validators.required]),
+ cpus: new FormControl(defaultCpu, [Validators.min(1),
Validators.required]),
+ memory: new FormGroup(
+ {
+ num: new FormControl(defaultMemory, [Validators.required]),
+ unit: new FormControl(defaultUnit, [Validators.required])
+ },
+ [this.experimentFormService.memoryValidator]
+ )
},
[this.experimentFormService.specValidator]
);
+ }
+
+ /**
+ * Handler for the create env button
+ */
+ onCreateEnv() {
+ const env = this.createEnv();
+ this.envs.push(env);
+ // If the new page is created, jump to that page
+ if (this.envs.controls.length > 1 && this.envs.controls.length %
this.PAGESIZE === 1) {
+ this.currentEnvPage += 1;
+ }
+ }
+
+ /**
+ * Handler for the create spec button
+ */
+ onCreateSpec() {
+ const spec = this.createSpec();
this.specs.push(spec);
// If the new page is created, jump to that page
if (this.specs.controls.length > 1 && this.specs.controls.length %
this.PAGESIZE === 1) {
@@ -246,7 +309,9 @@ export class ExperimentComponent implements OnInit {
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}`
+ resources:
`cpu=${spec.get('cpus').value},memory=${spec.get('memory').get('num').value}${
+ spec.get('memory').get('unit').value
+ }`
};
}
}
@@ -297,8 +362,36 @@ export class ExperimentComponent implements OnInit {
});
}
- onDeleteExperiment(data: ExperimentInfo, onMessage: boolean) {
- this.experimentService.deleteExperiment(data.experimentId).subscribe(
+ onUpdateExperiment(id: string, spec: ExperimentSpec) {
+ // Open Modal in update mode
+ this.initExperimentStatus('update');
+ // Keep id for later request
+ this.updateId = id;
+
+ // Prevent user from modifying the name
+ this.experimentName.disable();
+
+ // Put value back
+ this.experimentName.setValue(spec.meta.name);
+ this.description.setValue(spec.meta.description);
+ this.namespace.setValue(spec.meta.namespace);
+ this.cmd.setValue(spec.meta.cmd);
+ this.image.setValue(spec.environment.image);
+
+ for (const [key, value] of Object.entries(spec.meta.envVars)) {
+ const env = this.createEnv(key, value);
+ this.envs.push(env);
+ }
+
+ for (const [specName, info] of Object.entries(spec.spec)) {
+ const [cpuCount, memory, unit] = info.resources.match(/\d+|[MG]/g);
+ const newSpec = this.createSpec(specName, parseInt(info.replicas),
parseInt(cpuCount), memory, unit);
+ this.specs.push(newSpec);
+ }
+ }
+
+ onDeleteExperiment(id: string, onMessage: boolean) {
+ this.experimentService.deleteExperiment(id).subscribe(
() => {
if (onMessage === true) {
this.nzMessageService.success('Delete Experiment Successfully!');
@@ -328,7 +421,7 @@ export class ExperimentComponent implements OnInit {
deleteExperiments() {
for (let i = this.checkedList.length - 1; i >= 0; i--) {
if (this.checkedList[i] === true) {
- this.onDeleteExperiment(this.experimentList[i], false);
+ this.onDeleteExperiment(this.experimentList[i].experimentId, false);
}
}
@@ -353,8 +446,4 @@ export class ExperimentComponent implements OnInit {
startExperiment(Experiment) {
console.log(Experiment);
}
- // TODO(jasoonn): Edit experiment
- editExperiment(Experiment) {
- console.log(Experiment);
- }
}
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 3de0985..393eee3 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
@@ -24,6 +24,7 @@ import { ExperimentInfo } from
'@submarine/interfaces/experiment-info';
import { BaseApiService } from '@submarine/services/base-api.service';
import { of, Observable, throwError } from 'rxjs';
import { switchMap, catchError, map } from 'rxjs/operators';
+import { ExperimentSpec } from '@submarine/interfaces/experiment-spec';
@Injectable({
providedIn: 'root'
@@ -58,7 +59,7 @@ export class ExperimentService {
);
}
- createExperiment(experimentSpec): Observable<ExperimentInfo> {
+ createExperiment(experimentSpec: ExperimentSpec): Observable<ExperimentInfo>
{
const apiUrl = this.baseApi.getRestApi('/v1/experiment');
return this.httpClient.post<Rest<ExperimentInfo>>(apiUrl,
experimentSpec).pipe(
map((res) => res.result), // return result directly if succeeding
@@ -82,21 +83,32 @@ export class ExperimentService {
);
}
- editExperiment(experimentSpec): Observable<ExperimentInfo> {
- const apiUrl = this.baseApi.getRestApi('/v1/experiment');
+ updateExperiment(id: string, experimentSpec: ExperimentSpec):
Observable<ExperimentInfo> {
+ const apiUrl = this.baseApi.getRestApi(`/v1/experiment/${id}`);
return this.httpClient.patch<Rest<ExperimentInfo>>(apiUrl,
experimentSpec).pipe(
- switchMap((res) => {
- if (res.success) {
- return of(res.result);
+ map((res) => res.result),
+ catchError((e) => {
+ console.log(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,
'patch', experimentSpec);
+ 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);
})
);
}
deleteExperiment(id: string): Observable<ExperimentInfo> {
- const apiUrl = this.baseApi.getRestApi('/v1/experiment/' + id);
+ const apiUrl = this.baseApi.getRestApi(`/v1/experiment/${id}`);
return this.httpClient.delete<Rest<any>>(apiUrl).pipe(
switchMap((res) => {
if (res.success) {
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 923e98b..df09c9c 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
@@ -32,9 +32,7 @@ export class ExperimentFormService {
envValidator: ValidatorFn = (envGroup: FormGroup): ValidationErrors | null
=> {
const key = envGroup.get('key');
const keyValue = envGroup.get('value');
- return (key.value && keyValue.value) || (!key.value && !keyValue.value)
- ? null
- : { envMissing: 'Missing key or value' };
+ return !(key.invalid || keyValue.invalid) ? null : { envMissing: 'Missing
key or value' };
};
specValidator: ValidatorFn = (specGroup: FormGroup): ValidationErrors | null
=> {
@@ -44,22 +42,21 @@ export class ExperimentFormService {
const memory = specGroup.get('memory');
const allValid = !(name.invalid || replicas.invalid || cpus.invalid ||
memory.invalid);
- const exists =
- (name.value && replicas.value && cpus.value && memory.value) ||
- !(name.value || replicas.value || cpus.value || memory.value);
- return allValid && exists ? null : { specError: 'Invalid or missing input'
};
+ return allValid ? null : { specError: 'Invalid or missing input' };
};
/**
* Validate memory input in Spec
*
- * @param memory - The memory field in Spec
+ * @param memory - The memory group in Spec, containing actual number and
unit
*/
- memoryValidator: ValidatorFn = (memory: FormControl): ValidationErrors |
null => {
+ memoryValidator: ValidatorFn = (memoryGroup: FormGroup): ValidationErrors |
null => {
// Must match number + digit ex. 512M or empty
- return !memory.value || /^\d+M$/.test(memory.value)
+ const memory =
`${memoryGroup.get('num').value}${memoryGroup.get('unit').value}`;
+
+ return /^\d+[GM]$/.test(memory)
? null
- : { memoryPatternError: 'Memory pattern must match number + M ex. 512M'
};
+ : { memoryPatternError: 'Memory pattern must match number + (G or M) ex.
512M' };
};
/**
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]