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/14 22:00:14 UTC
[submarine] branch master updated: SUBMARINE-554. [WEB] UI for
Submarine experiment creation
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 d11db22 SUBMARINE-554. [WEB] UI for Submarine experiment creation
d11db22 is described below
commit d11db228997c3a12e1f9fce70f5f8776b39c0b74
Author: wang0630 <j2...@gmail.com>
AuthorDate: Tue Jul 14 11:40:18 2020 +0800
SUBMARINE-554. [WEB] UI for Submarine experiment creation
### What is this PR for?
Renew UI for experiment creation.
### What type of PR is it?
[Improvement]
### Todos
* [ ] - Task
### What is the Jira issue?
[SUBMARINE-554](https://issues.apache.org/jira/browse/SUBMARINE-554)
### How should this be tested?
https://travis-ci.com/github/wang0630/submarine/builds/174373510
### Screenshots (if appropriate)
![new](https://user-images.githubusercontent.com/26138982/86907595-de061200-c147-11ea-82ac-66ae3e8ea114.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 #342 from wang0630/SUBMARINE-554 and squashes the following commits:
558f360 [wang0630] SUBMARINE-554. [WEB] UI for experiment creation
b912354 [wang0630] SUBMARINE-554. [WEB] UI for experiment creation
2f2a933 [wang0630] SUBMARINE-554. [WEB] UI for experiment creation
f450d6b [wang0630] SUBMARINE-554. [WEB] UI for experiment creation
daa96d7 [wang0630] SUBMARINE-554. [WEB] UI for experiment creation
45c2b0a [wang0630] Duplicate key left
4982677 [wang0630] Make function call to pipe
119d844 [wang0630] Make css more structural
1635477 [wang0630] After renew
9218c42 [wang0630] SUBMARINE-554. [WEB] UI for experiment creation
c809a31 [wang0630] Start env page
12d3356 [wang0630] Change frameworks
---
.../org/apache/submarine/AbstractSubmarineIT.java | 17 ++
.../org/apache/submarine/WebDriverManager.java | 1 +
.../apache/submarine/integration/experimentIT.java | 44 ++++-
submarine-workbench/workbench-web-ng/package.json | 1 +
.../workbench/experiment/experiment.component.html | 216 ++++++++++++---------
.../workbench/experiment/experiment.component.scss | 87 +++++++--
.../workbench/experiment/experiment.component.ts | 136 +++++++++++--
.../src/app/pages/workbench/workbench.module.ts | 4 +-
.../condition.pipe.ts} | 58 ++----
.../pipe-shared.module.ts} | 59 +-----
.../app/services/experiment.validator.service.ts | 95 +++++++++
11 files changed, 491 insertions(+), 227 deletions(-)
diff --git a/submarine-test/test-e2e/src/test/java/org/apache/submarine/AbstractSubmarineIT.java b/submarine-test/test-e2e/src/test/java/org/apache/submarine/AbstractSubmarineIT.java
index c04389a..87fd83a 100644
--- a/submarine-test/test-e2e/src/test/java/org/apache/submarine/AbstractSubmarineIT.java
+++ b/submarine-test/test-e2e/src/test/java/org/apache/submarine/AbstractSubmarineIT.java
@@ -115,6 +115,23 @@ abstract public class AbstractSubmarineIT {
});
}
+ protected WebElement buttonCheck(final By locator, final long timeWait) {
+ Wait<WebDriver> wait = new FluentWait<>(driver)
+ .withTimeout(timeWait, TimeUnit.SECONDS)
+ .pollingEvery(1, TimeUnit.SECONDS)
+ .ignoring(NoSuchElementException.class);
+ return wait.until(ExpectedConditions.elementToBeClickable(locator));
+ }
+
+ protected void takeScreenShot(final String path) {
+ File scrFile1 = ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE);
+ try {
+ FileUtils.copyFile(scrFile1, new File(path));
+ } catch (java.io.IOException e) {
+ e.fillInStackTrace();
+ }
+ }
+
protected void createNewNote() {
clickAndWait(By.xpath("//div[contains(@class, \"col-md-4\")]/div/h5/a[contains(.,'Create new" +
" note')]"));
diff --git a/submarine-test/test-e2e/src/test/java/org/apache/submarine/WebDriverManager.java b/submarine-test/test-e2e/src/test/java/org/apache/submarine/WebDriverManager.java
index 515dca2..3122fb7 100644
--- a/submarine-test/test-e2e/src/test/java/org/apache/submarine/WebDriverManager.java
+++ b/submarine-test/test-e2e/src/test/java/org/apache/submarine/WebDriverManager.java
@@ -61,6 +61,7 @@ public class WebDriverManager {
}
}
+
String url;
if (System.getenv("url") != null) {
url = System.getenv("url");
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 334108e..a525749 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,9 +17,14 @@
package org.apache.submarine.integration;
+import org.apache.commons.io.FileUtils;
import org.apache.submarine.AbstractSubmarineIT;
import org.apache.submarine.WebDriverManager;
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.annotations.AfterClass;
@@ -28,6 +33,9 @@ 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 {
@@ -54,21 +62,41 @@ public class experimentIT extends AbstractSubmarineIT {
pollingWait(By.cssSelector("a[routerlink='/workbench/dashboard']"), MAX_BROWSER_TIMEOUT_SEC);
// Routing to workspace
+ LOG.info("url");
pollingWait(By.xpath("//span[contains(text(), \"Experiment\")]"), MAX_BROWSER_TIMEOUT_SEC).click();
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8080/workbench/experiment");
// 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, 60);
- wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//label[contains(text(), \"Experiment Name\")]")));
- 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.assertTrue(pollingWait(By.xpath("//div[@id='page2']"), 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();
- Assert.assertTrue(pollingWait(By.xpath("//label[@class='pg3-form-label']"), MAX_BROWSER_TIMEOUT_SEC).isDisplayed());
+ // 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");
+
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();
}
}
diff --git a/submarine-workbench/workbench-web-ng/package.json b/submarine-workbench/workbench-web-ng/package.json
index eb92afe..4c23700 100644
--- a/submarine-workbench/workbench-web-ng/package.json
+++ b/submarine-workbench/workbench-web-ng/package.json
@@ -9,6 +9,7 @@
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
+ "autoformat": "prettier --write --trailing-comma none --single-quote 'src/**/*.{js,json,ts,html}'",
"webdriver": "webdriver-manager update --ignore_ssl = true"
},
"private": true,
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 1f5ddba..39dacd9 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
@@ -145,26 +145,20 @@
[(nzOkText)]="okText"
[nzOkLoading]="isOkLoading"
(nzOnCancel)="isVisible = false"
- [nzWidth]="740"
+ [nzWidth]="1000"
>
<div>
<nz-steps [nzCurrent]="current">
- <nz-step nzTitle="Basic Information"></nz-step>
- <nz-step nzTitle="Configuration"></nz-step>
- <nz-step nzTitle="Scheduling Cycle"></nz-step>
+ <nz-step nzTitle="Meta"></nz-step>
+ <nz-step nzTitle="Env"></nz-step>
+ <nz-step nzTitle="Spec"></nz-step>
</nz-steps>
</div>
<div>
<form [formGroup]="createExperiment">
<div *nzModalFooter>
<button nz-button nzType="default" (click)="isVisible = false">Cancel</button>
- <button
- id="go"
- nz-button
- nzType="primary"
- [disabled]="!(createExperiment.get('experimentName').valid && createExperiment.get('description').valid)"
- (click)="handleOk()"
- >
+ <button id="go" nz-button nzType="primary" [disabled]="checkStatus()" (click)="handleOk()">
{{ okText }}
</button>
<button
@@ -179,106 +173,154 @@
</div>
<div [ngSwitch]="current" style="margin-top: 30px;">
<div *ngSwitchCase="0">
- <div>
- <label class="form-label">
+ <div class="single-field-group">
+ <label for="experimentName">
<span class="red-star">*</span>
- Experiment Name:
+ Experiment Name
</label>
- <label for="experimentName"></label>
- <input
- type="text"
- id="experimentName"
- style="margin-top: 32px; width: 350px;"
- class="form-control"
- formControlName="experimentName"
- />
+ <input nz-input type="text" name="experimentName" id="experimentName" formControlName="experimentName" />
</div>
- <div>
- <label class="form-label">
+ <div class="single-field-group">
+ <label for="description">
<span class="red-star">*</span>
- Description:
- </label>
- <label>
- <textarea
- rows="6"
- class="form-control"
- style="margin-top: 32px; width: 350px;"
- formControlName="description"
- ></textarea>
+ Description
</label>
+ <textarea
+ nz-input
+ [nzAutosize]="{ minRows: 3, maxRows: 6 }"
+ name="description"
+ formControlName="description"
+ ></textarea>
</div>
- </div>
- <div *ngSwitchCase="1" id="page2">
- <div>
- <label class="form-label">
+
+ <div class="single-field-group">
+ <label for="frameworks">
<span class="red-star">*</span>
- Experiment Spec:
+ Frameworks
</label>
- <nz-select
- formControlName="experimentSpec"
- nzPlaceHolder="Choose"
- style="width: 350px; margin-top: 30px;"
- >
+ <nz-select formControlName="frameworks" nzPlaceHolder="Choose" style="width: 48%;">
<nz-option
- *ngFor="let experimentSpec of ExperimentSpecs"
- [nzValue]="experimentSpec"
- [nzLabel]="experimentSpec"
+ *ngFor="let framework of frameworkNames"
+ [nzValue]="framework"
+ [nzLabel]="framework"
></nz-option>
</nz-select>
</div>
- <div>
- <label class="form-label">
+ <div class="single-field-group">
+ <label for="namespace">
<span class="red-star">*</span>
- Rule Template:
+ Namespace
</label>
- <nz-select formControlName="ruleTemplate" nzPlaceHolder="Choose" style="width: 350px; margin-top: 30px;">
- <nz-option
- *ngFor="let ruleTemplate of ruleTemplates"
- [nzValue]="ruleTemplate"
- [nzLabel]="ruleTemplate"
- ></nz-option>
- </nz-select>
+ <input nz-input name="namespace" formControlName="namespace" />
</div>
- <div>
- <label class="form-label">
+ <div class="single-field-group">
+ <label for="cmd">
<span class="red-star">*</span>
- Rule Type:
+ Command
</label>
- <nz-radio-group formControlName="ruleType" style="width: 350px; margin-top: 30px; margin-bottom: 30px;">
- <label *ngFor="let ruletype of ruleTypes" nz-radio [nzValue]="ruletype">{{ ruletype }}</label>
- </nz-radio-group>
+ <input nz-input name="cmd" formControlName="cmd" placeholder="Command to run" />
</div>
- </div>
- <div *ngSwitchCase="2">
- <div>
- <label class="pg3-form-label">
+ <div class="single-field-group">
+ <label for="image">
<span class="red-star">*</span>
- Start Date:
+ Image
</label>
- <nz-date-picker
- [nzFormat]="'yyyy/MM/dd'"
- formControlName="startDate"
- style="width: 350px; margin-top: 30px;"
- ></nz-date-picker>
+ <input nz-input name="image" formControlName="image" placeholder="Image to use" />
</div>
+ </div>
+ <div *ngSwitchCase="1" id="page2">
<div>
- <label class="pg3-form-label">
- <span class="red-star">*</span>
- Schedule Cycle:
- </label>
- <nz-select
- formControlName="scheduleCycle"
- nzPlaceHolder="Choose"
- style="width: 195px; margin-top: 30px; margin-bottom: 30px;"
- >
- <nz-option
- *ngFor="let scheduleCycle of scheduleCycles"
- [nzValue]="scheduleCycle"
- [nzLabel]="scheduleCycle"
- ></nz-option>
- </nz-select>
+ <button nz-button id="env-btn" type="default" (click)="createEnvInput()">
+ 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" />
+ <i nz-icon nzType="close-circle" nzTheme="fill" (click)="deleteItem(envs, i)"></i>
+ </li>
+ <div
+ class="alert-message"
+ *ngIf="env.get('key').dirty | logicalAnd: env.get('value').dirty:env.hasError('envMissing')"
+ >
+ {{ env.getError('envMissing') }}
+ </div>
+ <div
+ class="alert-message"
+ *ngIf="env.get('key').dirty | logicalAnd: env.get('key').hasError('duplicateError')"
+ >
+ {{ env.get('key').getError('duplicateError') }}
+ </div>
+ </ng-container>
+ </ul>
+ <nz-pagination
+ [(nzPageIndex)]="currentEnvPage"
+ [nzPageSize]="PAGESIZE"
+ [nzTotal]="envs.controls.length"
+ nzSimple
+ ></nz-pagination>
</div>
</div>
+ <div *ngSwitchCase="2">
+ <label class="pg3-form-label"></label>
+ <button nz-button id="spec-btn" type="default" (click)="createSpec()">
+ 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" />
+ <i
+ nz-icon
+ nzType="close-circle"
+ nzTheme="fill"
+ class="delete-icon"
+ (click)="deleteItem(specs, i)"
+ ></i>
+ </li>
+ <div
+ class="alert-message"
+ *ngIf="spec.get('name').dirty | logicalAnd: spec.get('name').hasError('duplicateError')"
+ >
+ {{ spec.get('name').getError('duplicateError') }}
+ </div>
+ <div
+ class="alert-message"
+ *ngIf="spec.get('memory').dirty | logicalAnd: spec.get('memory').hasError('memoryPatternError')"
+ >
+ {{ spec.get('memory').getError('memoryPatternError') }}
+ </div>
+ <div
+ class="alert-message"
+ *ngIf="spec.get('replicas').dirty | logicalAnd: spec.get('replicas').hasError('min')"
+ >
+ replicas must be at least 1
+ </div>
+ <div
+ class="alert-message"
+ *ngIf="spec.get('cpus').dirty | logicalAnd: spec.get('cpus').hasError('min')"
+ >
+ cpus must be at least 1
+ </div>
+ </ng-container>
+ </ul>
+ <nz-pagination
+ [(nzPageIndex)]="currentSpecPage"
+ [nzPageSize]="PAGESIZE"
+ [nzTotal]="specs.controls.length"
+ nzSimple
+ ></nz-pagination>
+ </div>
</div>
</form>
</div>
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.scss b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.scss
index 3838a1b..1cc8199 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
@@ -45,24 +45,75 @@
color: red;
}
-.form-label {
- float:left;
- width:200px;
- text-align: right;
- padding-right: 12px;
- margin-top: 32px;
- margin-left: 10px;
- clear: left;
- color: black;
+.list-container {
+ padding: 0;
+ margin: 1rem 0;
}
-.pg3-form-label {
- float:left;
- width:200px;
- text-align: right;
- padding-right: 12px;
- margin-top: 32px;
- margin-left: 100px;
- clear: left;
- color: black;
+/* For env section */
+.env-page-container {
+ display: flex;
+ flex-direction: column;
+}
+
+.env-input-container {
+ padding: 0;
+ margin: 1rem 0;
+}
+
+.input-group {
+ display: flex;
+ align-items: center;
+ &:not(:first-child) {
+ margin-top: 1rem;
+ }
+
+ & input {
+ flex: 0 2 20%;
+ &:not(:last-child) {
+ margin-right: .5rem;
+ }
+ }
+
+ & i {
+ cursor: pointer;
+ font-size: 1.2rem;
+ }
+}
+
+/* utility */
+.single-field-group {
+ display: flex;
+ align-items: center;
+ margin-bottom: 1.5rem;
+ & label {
+ flex: 0 0 25%;
+ text-align: right;
+ font-weight: 500;
+ &:not(:last-child) {
+ margin-right: 1.5rem;
+ }
+ }
+ & input {
+ flex: 0 0 48%;
+ }
+ & textarea {
+ flex: 0 0 55%;
+ }
+}
+
+.alert-message {
+ color: red;
+ margin-top: .3rem;
+ margin-left: .5rem;
}
+
+.test {
+ display: flex;
+ 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 298ff82..c71acb3 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.ts
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.ts
@@ -18,10 +18,11 @@
*/
import { Component, OnInit } from '@angular/core';
-import { FormControl, FormGroup, Validators } from '@angular/forms';
+import { FormControl, FormGroup, Validators, FormArray } from '@angular/forms';
import { ActivatedRoute, NavigationStart, Router } from '@angular/router';
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';
@Component({
@@ -45,13 +46,19 @@ export class ExperimentComponent implements OnInit {
okText = 'Next Step';
isVisible = false;
- ExperimentSpecs = ['Adhoc', 'Predefined'];
- ruleTemplates = ['Template1', 'Template2'];
- ruleTypes = ['Strong', 'Weak'];
- scheduleCycles = ['Month', 'Week'];
+ // ExperimentSpecs = ['Adhoc', 'Predefined'];
+ frameworkNames = ['Tensorflow', 'Pytorch'];
+
+ // About env page
+ currentEnvPage = 1;
+ PAGESIZE = 5;
+
+ // About spec
+ currentSpecPage = 1;
constructor(
private experimentService: ExperimentService,
+ private experimentFormService: ExperimentFormService,
private nzMessageService: NzMessageService,
private route: ActivatedRoute,
private router: Router
@@ -61,11 +68,14 @@ export class ExperimentComponent implements OnInit {
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')
+ // 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]),
+ specs: new FormArray([], [this.experimentFormService.nameValidatorFactory('name')])
});
this.fetchExperimentList();
this.isInfo = this.router.url !== '/workbench/experiment';
@@ -83,21 +93,111 @@ export class ExperimentComponent implements OnInit {
});
}
+ // Getters of experiment request form
+ get experimentName() {
+ return this.createExperiment.get('experimentName');
+ }
+ get description() {
+ return this.createExperiment.get('description');
+ }
+ get frameworks() {
+ return this.createExperiment.get('frameworks');
+ }
+ get namespace() {
+ return this.createExperiment.get('namespace');
+ }
+ get cmd() {
+ return this.createExperiment.get('cmd');
+ }
+ get envs() {
+ return this.createExperiment.get('envs') as FormArray;
+ }
+ get image() {
+ return this.createExperiment.get('image');
+ }
+ get specs() {
+ return this.createExperiment.get('specs') as FormArray;
+ }
+ /**
+ * Check the validity of the experiment page
+ *
+ */
+ checkStatus() {
+ if (this.current == 0) {
+ return (
+ this.experimentName.invalid ||
+ this.description.invalid ||
+ this.frameworks.invalid ||
+ this.namespace.invalid ||
+ this.cmd.invalid ||
+ this.image.invalid
+ );
+ } else if (this.current == 1) {
+ return this.envs.invalid;
+ } else if (this.current == 2) {
+ return this.specs.invalid;
+ }
+ }
+
handleOk() {
if (this.current === 1) {
this.okText = 'Submit';
+ }
+
+ if (this.current < 2) {
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++;
}
}
+ /**
+ * Create a new env variable input
+ */
+ createEnvInput() {
+ // Create a new FormGroup
+ const env = new FormGroup(
+ {
+ key: new FormControl(''),
+ value: new FormControl()
+ },
+ [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(
+ {
+ name: new FormControl(''),
+ replicas: new FormControl(null, [Validators.min(1)]),
+ cpus: new FormControl(null, [Validators.min(1)]),
+ memory: new FormControl('', [this.experimentFormService.memoryValidator])
+ },
+ [this.experimentFormService.specValidator]
+ );
+ this.specs.push(spec);
+ // If the new page is created, jump to that page
+ if (this.specs.controls.length > 1 && this.specs.controls.length % this.PAGESIZE === 1) {
+ this.currentSpecPage += 1;
+ }
+ }
+
+ /**
+ * Delete list items(envs or specs)
+ *
+ * @param arr - The FormArray containing the item
+ * @param index - The index of the item
+ */
+ deleteItem(arr: FormArray, index: number) {
+ arr.removeAt(index);
+ }
+
fetchExperimentList() {
this.experimentService.fetchExperimentList().subscribe((list) => {
this.experimentList = list;
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 ba34d02..84ebbe0 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
@@ -22,6 +22,7 @@ import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { WorkbenchRoutingModule } from '@submarine/pages/workbench/workbench-routing.module';
+import { PipeSharedModule } from '@submarine/pipe/pipe-shared.module';
import { NgZorroAntdModule } from 'ng-zorro-antd';
import { DataComponent } from './data/data.component';
import { ExperimentComponent } from './experiment/experiment.component';
@@ -51,7 +52,8 @@ import { WorkspaceModule } from './workspace/workspace.module';
FormsModule,
WorkspaceModule,
ExperimentModule,
- InterpreterModule
+ InterpreterModule,
+ PipeSharedModule
]
})
export class WorkbenchModule {}
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.scss b/submarine-workbench/workbench-web-ng/src/app/pipe/condition.pipe.ts
similarity index 50%
copy from submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.scss
copy to submarine-workbench/workbench-web-ng/src/app/pipe/condition.pipe.ts
index 3838a1b..f5cd2c3 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.scss
+++ b/submarine-workbench/workbench-web-ng/src/app/pipe/condition.pipe.ts
@@ -1,4 +1,4 @@
-/*!
+/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
@@ -17,52 +17,18 @@
* under the License.
*/
- #experimentOuter{
- background-color: white;
- padding-left: 30px;
- padding-top: 20px;
- }
+import { Pipe, PipeTransform } from '@angular/core';
- #experimentData{
- margin-top: 16px;
- margin-left: 25px;
- margin-right: 25px;
- background-color:white;
- padding-left: 10px;
- padding-right: 10px;
- }
-
- input.ng-invalid.ng-touched {
- border: 1px solid red;
- }
-
- textarea.ng-invalid.ng-touched {
- border: 1px solid red;
-}
-
-.red-star {
- margin-top: 20px;
- color: red;
-}
-
-.form-label {
- float:left;
- width:200px;
- text-align: right;
- padding-right: 12px;
- margin-top: 32px;
- margin-left: 10px;
- clear: left;
- color: black;
+@Pipe({ name: 'logicalAnd' }) // The name of the pipe to use in the template
+export class LogicalAnd implements PipeTransform {
+ transform(value: boolean, ...flags: boolean[]) {
+ return value && !flags.includes(false);
+ }
}
-.pg3-form-label {
- float:left;
- width:200px;
- text-align: right;
- padding-right: 12px;
- margin-top: 32px;
- margin-left: 100px;
- clear: left;
- color: black;
+@Pipe({ name: 'indexInRange' })
+export class IndexInRange implements PipeTransform {
+ transform(index: number, currentPage: number, itemPerPage: number): boolean {
+ return index < currentPage * itemPerPage && index >= (currentPage - 1) * itemPerPage;
+ }
}
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.scss b/submarine-workbench/workbench-web-ng/src/app/pipe/pipe-shared.module.ts
similarity index 50%
copy from submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.scss
copy to submarine-workbench/workbench-web-ng/src/app/pipe/pipe-shared.module.ts
index 3838a1b..e9ce712 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.component.scss
+++ b/submarine-workbench/workbench-web-ng/src/app/pipe/pipe-shared.module.ts
@@ -1,4 +1,4 @@
-/*!
+/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
@@ -17,52 +17,13 @@
* under the License.
*/
- #experimentOuter{
- background-color: white;
- padding-left: 30px;
- padding-top: 20px;
- }
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { LogicalAnd, IndexInRange } from './condition.pipe';
- #experimentData{
- margin-top: 16px;
- margin-left: 25px;
- margin-right: 25px;
- background-color:white;
- padding-left: 10px;
- padding-right: 10px;
- }
-
- input.ng-invalid.ng-touched {
- border: 1px solid red;
- }
-
- textarea.ng-invalid.ng-touched {
- border: 1px solid red;
-}
-
-.red-star {
- margin-top: 20px;
- color: red;
-}
-
-.form-label {
- float:left;
- width:200px;
- text-align: right;
- padding-right: 12px;
- margin-top: 32px;
- margin-left: 10px;
- clear: left;
- color: black;
-}
-
-.pg3-form-label {
- float:left;
- width:200px;
- text-align: right;
- padding-right: 12px;
- margin-top: 32px;
- margin-left: 100px;
- clear: left;
- color: black;
-}
+@NgModule({
+ imports: [CommonModule],
+ declarations: [LogicalAnd, IndexInRange],
+ exports: [LogicalAnd, IndexInRange]
+})
+export class PipeSharedModule {}
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
new file mode 100644
index 0000000..b9b2461
--- /dev/null
+++ b/submarine-workbench/workbench-web-ng/src/app/services/experiment.validator.service.ts
@@ -0,0 +1,95 @@
+/*
+ * 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 { FormGroup, ValidatorFn, ValidationErrors, FormControl, FormArray } from '@angular/forms';
+import { Injectable } from '@angular/core';
+import { ExperimentModule } from '@submarine/pages/workbench/experiment/experiment.module';
+
+@Injectable({
+ providedIn: ExperimentModule
+})
+export class ExperimentFormService {
+ /**
+ * The validator for env key/value pair
+ * @param envGroup A FormGroup resides in `envs` FromArray in createExperiment
+ */
+ 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' };
+ };
+
+ specValidator: ValidatorFn = (specGroup: FormGroup): ValidationErrors | null => {
+ const name = specGroup.get('name');
+ const replicas = specGroup.get('replicas');
+ const cpus = specGroup.get('cpus');
+ 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' };
+ };
+
+ /**
+ * Validate memory input in Spec
+ *
+ * @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)
+ ? null
+ : { memoryPatternError: 'Memory pattern must match number + M ex. 512M' };
+ };
+
+ /**
+ * Validate name or key property
+ * Name and key cannot have its duplicate, must be unique
+ * @param fieldName - The field name of the form
+ * @returns The actual ValidatorFn to check duplicates
+ */
+ nameValidatorFactory: (fieldName: string) => ValidatorFn = (fieldName) => {
+ return (arr: FormArray): ValidationErrors | null => {
+ const duplicateSet = new Set();
+ for (let i = 0; i < arr.length; i++) {
+ const nameControl = arr.controls[i].get(fieldName);
+ // We don't consider empty string
+ if (!nameControl.value) continue;
+
+ if (duplicateSet.has(nameControl.value)) {
+ // Found duplicates, manually set errors on FormControl level
+ nameControl.setErrors({
+ duplicateError: 'Duplicate key or name'
+ });
+ } else {
+ duplicateSet.add(nameControl.value);
+ if (nameControl.hasError('duplicateError')) {
+ delete nameControl.errors.duplicateError;
+ nameControl.updateValueAndValidity();
+ }
+ }
+ }
+ return null;
+ };
+ };
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@submarine.apache.org
For additional commands, e-mail: dev-help@submarine.apache.org