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/09/05 06:45:18 UTC
[submarine] branch master updated: SUBMARINE-610. [WEB] Refactor
experiment.component
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 6dec6e1 SUBMARINE-610. [WEB] Refactor experiment.component
6dec6e1 is described below
commit 6dec6e13f229c6bc216c785bbaed827c10cefd30
Author: wang0630 <j2...@gmail.com>
AuthorDate: Fri Sep 4 13:43:49 2020 +0800
SUBMARINE-610. [WEB] Refactor experiment.component
### What is this PR for?
Separate form in `experiment.component` to another file, and using RXJS `Subject` and `Observable` to communicate between components.
### What type of PR is it?
[Refactoring]
### Todos
### What is the Jira issue?
https://issues.apache.org/jira/browse/SUBMARINE-610?filter=-1
### How should this be tested?
https://github.com/wang0630/submarine/runs/1046727907
### Screenshots (if appropriate)
![Aug-30-2020 11-44-15](https://user-images.githubusercontent.com/26138982/91650772-2ee61700-eab6-11ea-91c5-f92307df8126.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 #388 from wang0630/SUBMARINE-610 and squashes the following commits:
f446696 [wang0630] Fix typo
71b850d [wang0630] Add todo in experimentIT.java
5692537 [wang0630] Fix clone btn
fd430c2 [wang0630] Add styling
f3ad296 [wang0630] Remove unnecessary style
7ccc16b [wang0630] Add license
0c53590 [wang0630] Add corresponding tests
ead507a [wang0630] Refactor form to individual component
2a56a29 [wang0630] Refactor form finished, check manually
982f9e9 [wang0630] Before using ViewChild
e2ce531 [wang0630] Refactor services
d011029 [wang0630] Refactor experiment page
b151a42 [wang0630] Files are organized, before launching
ce12d3c [wang0630] Before refactor component
e6962fb [wang0630] Add radio button for job types
---
.../apache/submarine/integration/experimentIT.java | 30 +-
.../integration/pages/ExperimentPage.java | 7 +-
.../src/app/interfaces/modal-props.ts | 25 ++
.../experiment-customized-form.component.html | 197 +++++++++++
.../experiment-customized-form.component.scss | 134 ++++++++
.../experiment-customized-form.component.ts | 369 +++++++++++++++++++++
.../workbench/experiment/experiment.component.html | 257 ++------------
.../workbench/experiment/experiment.component.scss | 98 +-----
.../workbench/experiment/experiment.component.ts | 366 +++-----------------
.../workbench/experiment/experiment.module.ts | 27 +-
.../src/app/pages/workbench/workbench.module.ts | 1 -
.../src/app/services/experiment.form.service.ts | 60 ++++
.../app/services/experiment.validator.service.ts | 7 +-
13 files changed, 925 insertions(+), 653 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 356f481..477277e 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
@@ -27,13 +27,6 @@ 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 org.apache.submarine.CommandExecutor;
-import org.apache.submarine.ProcessData;
-import java.io.File;
public class experimentIT extends AbstractSubmarineIT {
@@ -41,7 +34,7 @@ public class experimentIT extends AbstractSubmarineIT {
@BeforeClass
public static void startUp(){
- LOG.info("[Testcase]: experimentNew");
+ LOG.info("[Test case]: experimentNew");
driver = WebDriverManager.getWebDriver();
}
@@ -52,7 +45,7 @@ public class experimentIT extends AbstractSubmarineIT {
@Test
public void experimentNavigation() throws Exception {
- LOG.info("[Testacse: experimentNavigation]");
+ LOG.info("[Test case]: experimentNavigation]");
// Init the page object
ExperimentPage experimentPage = new ExperimentPage(driver);
// Login
@@ -69,8 +62,9 @@ public class experimentIT extends AbstractSubmarineIT {
// Test create new experiment
LOG.info("new experiment");
- String experimentName = "experiment-e2e-test";
experimentPage.newExperimentButtonClick();
+ experimentPage.customizedBtnClick();
+ String experimentName = "experiment-e2e-test";
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();
@@ -90,15 +84,17 @@ public class experimentIT extends AbstractSubmarineIT {
experimentPage.deleteSpec();
experimentPage.fillTfSpec(2, new String[]{"Ps", "Worker"}, new int[]{1, 1}, new int[]{1, 1}, new int[]{1024, 1024});
Assert.assertTrue(experimentPage.getGoButton().isEnabled());
- /*
+
+ }
+
+ /*
TODO: Launch submarine server and K8s in e2e-test
Comment out because of Experiment creation failure on Travis
- experimentPage.goButtonClick();
+ @Test
+ public void updateExperiment() {
+ ....
+ }
+ */
- // 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 6e70db0..eefe45d 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
@@ -34,7 +34,6 @@ import java.util.List;
public class ExperimentPage {
-
@FindBy(id = "experimentData")
private WebElement dataSection;
@@ -44,6 +43,8 @@ public class ExperimentPage {
@FindBy(id = "openExperiment")
private WebElement newExperimentButton;
+ @FindBy(id = "customized")
+ private WebElement customizedBtn;
/*
* For svg/path/g element tag, we must use //*[name = 'svg'] to select
* //svg will fail
@@ -127,6 +128,10 @@ public class ExperimentPage {
wait.until(ExpectedConditions.elementToBeClickable(newExperimentButton)).click();
}
+ public void customizedBtnClick() {
+ wait.until(ExpectedConditions.elementToBeClickable(customizedBtn)).click();
+ }
+
public void envBtnClick() {
wait.until(ExpectedConditions.elementToBeClickable(envBtn)).click();
}
diff --git a/submarine-workbench/workbench-web-ng/src/app/interfaces/modal-props.ts b/submarine-workbench/workbench-web-ng/src/app/interfaces/modal-props.ts
new file mode 100644
index 0000000..6202e97
--- /dev/null
+++ b/submarine-workbench/workbench-web-ng/src/app/interfaces/modal-props.ts
@@ -0,0 +1,25 @@
+/*!
+ * 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.
+ */
+
+export interface ModalProps {
+ okText?: string;
+ isVisible?: boolean;
+ currentStep?: number;
+ formType?: 'customized' | 'predefined';
+}
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.html b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.html
new file mode 100644
index 0000000..493f274
--- /dev/null
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.html
@@ -0,0 +1,197 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+-->
+
+<div>
+ <nz-steps [nzCurrent]="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]="experiment">
+ <div [ngSwitch]="step" style="margin-top: 30px;">
+ <div *ngSwitchCase="0">
+ <div class="single-field-group">
+ <label for="experimentName">
+ <span class="red-star">*</span>
+ Experiment Name
+ </label>
+ <input nz-input type="text" name="experimentName" id="experimentName" formControlName="experimentName" />
+ </div>
+ <div class="single-field-group">
+ <label for="description">
+ Description
+ </label>
+ <textarea
+ nz-input
+ [nzAutosize]="{ minRows: 3, maxRows: 6 }"
+ name="description"
+ formControlName="description"
+ ></textarea>
+ </div>
+ <div class="single-field-group">
+ <label for="namespace">
+ <span class="red-star">*</span>
+ Namespace
+ </label>
+ <input nz-input name="namespace" formControlName="namespace" />
+ </div>
+ <div class="single-field-group">
+ <label for="cmd">
+ <span class="red-star">*</span>
+ Command
+ </label>
+ <input nz-input name="cmd" formControlName="cmd" placeholder="Command to run" />
+ </div>
+ <div class="single-field-group">
+ <label for="image">
+ <span class="red-star">*</span>
+ Image
+ </label>
+ <input nz-input name="image" formControlName="image" placeholder="Image to use" />
+ </div>
+ </div>
+ <div *ngSwitchCase="1" id="page2">
+ <div>
+ <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">
+ <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
+ 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">
+ <nz-radio-group [(ngModel)]="jobTypes" [ngModelOptions]="{ standalone: true }" id="jobs-container">
+ <label nz-radio nzValue="Tensorflow">Distributed Tensorflow</label>
+ <label nz-radio nzValue="Pytorch">Distributed PyTorch</label>
+ </nz-radio-group>
+ <label class="pg3-form-label"></label>
+ <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">
+ <div id="spec{{ i }}">
+ <label>Spec name</label>
+ <nz-select formControlName="name" nzPlaceHolder="Spec name" [ngSwitch]="jobTypes">
+ <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" nzTheme="fill" class="delete-icon" (click)="deleteItem(specs, i)"></i>
+ </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') }}
+ </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-customized-form/experiment-customized-form.component.scss b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.scss
new file mode 100644
index 0000000..adf39b2
--- /dev/null
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.scss
@@ -0,0 +1,134 @@
+/*!
+ * 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.
+ */
+
+ #experimentOuter{
+ background-color: white;
+ padding-left: 30px;
+ padding-top: 20px;
+}
+
+#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;
+}
+
+.list-container {
+ padding: 0;
+ margin: 1rem 0;
+}
+
+/* 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;
+ font-size: .8rem;
+ &:not(:first-child) {
+ margin-top: 1rem;
+ }
+
+ & > *: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: 20px;
+ margin-top: 20px;
+ }
+}
+
+.memory-input-group {
+ display: flex;
+ & input {
+ width: 70%;
+ }
+
+ & > * {
+ width: 30%;
+ }
+}
+
+/* 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;
+}
+
+/* For jobs */
+#jobs-container {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
+ margin-bottom: 2rem;
+}
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.ts b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.ts
new file mode 100644
index 0000000..1022e71
--- /dev/null
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment-customized-form/experiment-customized-form.component.ts
@@ -0,0 +1,369 @@
+/*
+ * 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, Input, OnDestroy } from '@angular/core';
+import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
+import { ExperimentSpec, Specs, SpecEnviroment, SpecMeta } from '@submarine/interfaces/experiment-spec';
+import { ExperimentService } from '@submarine/services/experiment.service';
+import { ExperimentValidatorService } from '@submarine/services/experiment.validator.service';
+import { NzMessageService } from 'ng-zorro-antd';
+import { nanoid } from 'nanoid';
+import { Subscription } from 'rxjs';
+import { ExperimentFormService } from '@submarine/services/experiment.form.service';
+
+@Component({
+ selector: 'experiment-customized-form',
+ templateUrl: './experiment-customized-form.component.html',
+ styleUrls: ['./experiment-customized-form.component.scss']
+})
+export class ExperimentCustomizedForm implements OnInit, OnDestroy {
+ @Input() mode: 'create' | 'update' | 'clone';
+
+ // About new experiment
+ experiment: FormGroup;
+ step: number = 0;
+ subscriptions: Subscription[] = [];
+
+ // Constants
+ TF_SPECNAMES = ['Master', 'Worker', 'Ps'];
+ PYTORCH_SPECNAMES = ['Master', 'Worker'];
+ MEMORY_UNITS = ['M', 'G'];
+ TOTAL_STEPS = 2;
+
+ // About env page
+ currentEnvPage = 1;
+ PAGESIZE = 5;
+
+ // About spec
+ jobTypes = 'Tensorflow';
+ currentSpecPage = 1;
+
+ // About update
+ @Input() targetId: string = null;
+ @Input() targetSpec: ExperimentSpec = null;
+
+ constructor(
+ private experimentService: ExperimentService,
+ private experimentValidatorService: ExperimentValidatorService,
+ private experimentFormService: ExperimentFormService,
+ private nzMessageService: NzMessageService
+ ) {}
+
+ ngOnInit() {
+ this.experiment = new FormGroup({
+ experimentName: new FormControl(null, Validators.required),
+ description: new FormControl(null, [Validators.required]),
+ namespace: new FormControl('default', [Validators.required]),
+ cmd: new FormControl('', [Validators.required]),
+ image: new FormControl('', [Validators.required]),
+ envs: new FormArray([], [this.experimentValidatorService.nameValidatorFactory('key')]),
+ specs: new FormArray([], [this.experimentValidatorService.nameValidatorFactory('name')])
+ });
+
+ // Bind the component method for callback
+ this.checkStatus = this.checkStatus.bind(this);
+
+ if (this.mode === 'update') {
+ this.updateExperimentInit();
+ } else if (this.mode === 'clone') {
+ this.cloneExperimentInit(this.targetSpec);
+ }
+
+ // Fire status to parent when form value has changed
+ const sub1 = this.experiment.valueChanges.subscribe(this.checkStatus);
+
+ const sub2 = this.experimentFormService.stepService.subscribe((n) => {
+ if (n > 0) {
+ if (this.step === this.TOTAL_STEPS) {
+ this.handleSubmit();
+ } else {
+ this.step += 1;
+ }
+ } else {
+ this.step -= 1;
+ }
+ // Send the current step and okText back to parent
+ this.experimentFormService.modalPropsChange({
+ okText: this.step !== this.TOTAL_STEPS ? 'Next step' : 'Submit',
+ currentStep: this.step
+ });
+ // Run check after step is changed
+ this.checkStatus();
+ });
+
+ this.subscriptions.push(sub1, sub2);
+ }
+
+ ngOnDestroy() {
+ // Clean up the subscriptions
+ this.subscriptions.forEach((sub) => {
+ sub.unsubscribe();
+ });
+ }
+
+ // Getters of experiment request form
+ get experimentName() {
+ return this.experiment.get('experimentName');
+ }
+ get description() {
+ return this.experiment.get('description');
+ }
+ get namespace() {
+ return this.experiment.get('namespace');
+ }
+ get cmd() {
+ return this.experiment.get('cmd');
+ }
+ get envs() {
+ return this.experiment.get('envs') as FormArray;
+ }
+ get image() {
+ return this.experiment.get('image');
+ }
+ get specs() {
+ return this.experiment.get('specs') as FormArray;
+ }
+
+ /**
+ * Reset properies in parent component when the form is about to closed
+ */
+ closeModal() {
+ this.experimentFormService.modalPropsClear();
+ }
+
+ /**
+ * Check the validity of the experiment page
+ */
+ checkStatus() {
+ if (this.step === 0) {
+ this.experimentFormService.btnStatusChange(
+ this.experimentName.invalid || this.namespace.invalid || this.cmd.invalid || this.image.invalid
+ );
+ } else if (this.step === 1) {
+ this.experimentFormService.btnStatusChange(this.envs.invalid);
+ } else if (this.step === this.TOTAL_STEPS) {
+ this.experimentFormService.btnStatusChange(this.specs.invalid);
+ }
+ }
+
+ /**
+ * Event handler for Next step/Submit button
+ */
+ handleSubmit() {
+ if (this.mode === 'create') {
+ const newSpec = this.constructSpec();
+ this.experimentService.createExperiment(newSpec).subscribe({
+ next: () => {},
+ error: (msg) => {
+ this.nzMessageService.error(`${msg}, please try again`, {
+ nzPauseOnHover: true
+ });
+ },
+ complete: () => {
+ this.nzMessageService.success('Experiment creation succeeds');
+ this.experimentFormService.fetchList();
+ this.closeModal();
+ }
+ });
+ } else if (this.mode === 'update') {
+ const newSpec = this.constructSpec();
+ this.experimentService.updateExperiment(this.targetId, newSpec).subscribe(
+ null,
+ (msg) => {
+ this.nzMessageService.error(`${msg}, please try again`, {
+ nzPauseOnHover: true
+ });
+ },
+ () => {
+ this.nzMessageService.success('Modification succeeds!');
+ this.experimentFormService.fetchList();
+ this.closeModal();
+ }
+ );
+ } else if (this.mode === 'clone') {
+ const newSpec = this.constructSpec();
+ this.experimentService.createExperiment(newSpec).subscribe(
+ null,
+ (msg) => {
+ this.nzMessageService.error(`${msg}, please try again`, {
+ nzPauseOnHover: true
+ });
+ },
+ () => {
+ this.nzMessageService.success('Create a new experiment !');
+ this.experimentFormService.fetchList();
+ this.closeModal();
+ }
+ );
+ }
+ }
+
+ /**
+ * Create a new env variable input
+ */
+ createEnv(defaultKey: string = '', defaultValue: string = '') {
+ // Create a new FormGroup
+ return new FormGroup(
+ {
+ key: new FormControl(defaultKey, [Validators.required]),
+ value: new FormControl(defaultValue, [Validators.required])
+ },
+ [this.experimentValidatorService.envValidator]
+ );
+ }
+ /**
+ * Create a new spec
+ */
+ createSpec(
+ defaultName: string = '',
+ defaultReplica: number = 1,
+ defaultCpu: number = 1,
+ defaultMemory: string = '',
+ defaultUnit: string = 'M'
+ ): FormGroup {
+ return new FormGroup(
+ {
+ name: new FormControl(defaultName, [Validators.required]),
+ replicas: new FormControl(defaultReplica, [Validators.min(1), Validators.required]),
+ cpus: new FormControl(defaultCpu, [Validators.min(1), Validators.required]),
+ memory: new FormGroup(
+ {
+ num: new FormControl(defaultMemory, [Validators.required]),
+ unit: new FormControl(defaultUnit, [Validators.required])
+ },
+ [this.experimentValidatorService.memoryValidator]
+ )
+ },
+ [this.experimentValidatorService.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) {
+ this.currentSpecPage += 1;
+ }
+ }
+
+ /**
+ * Construct spec for new experiment creation
+ */
+ constructSpec(): ExperimentSpec {
+ // Construct the spec
+ const meta: SpecMeta = {
+ name: this.experimentName.value,
+ namespace: this.namespace.value,
+ framework: this.jobTypes,
+ 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').get('num').value}${
+ spec.get('memory').get('unit').value
+ }`
+ };
+ }
+ }
+
+ const environment: SpecEnviroment = {
+ image: this.image.value
+ };
+
+ const newExperimentSpec: ExperimentSpec = {
+ meta: meta,
+ environment: environment,
+ spec: specs
+ };
+
+ return newExperimentSpec;
+ }
+
+ /**
+ * 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);
+ }
+
+ updateExperimentInit() {
+ // Prevent user from modifying the name
+ this.experimentName.disable();
+ // Put value back
+ this.experimentName.setValue(this.targetSpec.meta.name);
+ this.cloneExperiment(this.targetSpec);
+ // Check status to enable next btn
+ this.checkStatus();
+ }
+
+ cloneExperimentInit(spec: ExperimentSpec) {
+ // Enable user from modifying the name
+ this.experimentName.enable();
+ // Put value back
+ const id: string = nanoid(8);
+ const cloneExperimentName = spec.meta.name + '-' + id;
+ this.experimentName.setValue(cloneExperimentName.toLocaleLowerCase());
+ this.cloneExperiment(spec);
+ }
+
+ cloneExperiment(spec: ExperimentSpec) {
+ 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, 10), parseInt(cpuCount, 10), memory, unit);
+ this.specs.push(newSpec);
+ }
+ }
+}
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 6d6b76f..30a750a 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)="initExperimentStatus('create')"
+ (click)="initModal('create')"
>
<i nz-icon nzType="plus"></i>
New Experiment
@@ -123,9 +123,9 @@
<td>{{ data.runningTime | date: 'M/d/yyyy, h:mm a' }}</td>
<td>{{ data.duration }}</td>
<td class="td-action">
- <a (click)="onCloneExperiment(data.spec)">Clone</a>
+ <a (click)="initModal('clone', 'customized', null, data.spec)">Clone</a>
<nz-divider nzType="vertical"></nz-divider>
- <a (click)="onUpdateExperiment(data.experimentId, data.spec)">Update</a>
+ <a (click)="initModal('update', 'customized', data.experimentId, data.spec)">Update</a>
<nz-divider nzType="vertical"></nz-divider>
<a
nz-popconfirm
@@ -145,229 +145,40 @@
<router-outlet (experimentInfoChange)="isInfo = true"></router-outlet>
</div>
<nz-modal
- [(nzVisible)]="isVisible"
+ [(nzVisible)]="modalProps.isVisible"
nzTitle="Create Experiment"
- [(nzOkText)]="okText"
- (nzOnCancel)="isVisible = false"
+ (nzOnCancel)="closeModal()"
[nzWidth]="1000"
>
- <div>
- <nz-steps [nzCurrent]="current">
- <nz-step nzTitle="Meta"></nz-step>
- <nz-step nzTitle="Env"></nz-step>
- <nz-step nzTitle="Spec"></nz-step>
- </nz-steps>
- </div>
- <div>
- <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()">
- {{ 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 class="single-field-group">
- <label for="experimentName">
- <span class="red-star">*</span>
- Experiment Name
- </label>
- <input nz-input type="text" name="experimentName" id="experimentName" formControlName="experimentName" />
- </div>
- <div class="single-field-group">
- <label for="description">
- Description
- </label>
- <textarea
- nz-input
- [nzAutosize]="{ minRows: 3, maxRows: 6 }"
- name="description"
- formControlName="description"
- ></textarea>
- </div>
-
- <div class="single-field-group">
- <label for="frameworks">
- <span class="red-star">*</span>
- Frameworks
- </label>
- <nz-select formControlName="frameworks" nzPlaceHolder="Choose" style="width: 48%;">
- <nz-option
- *ngFor="let framework of FRAMEWORK_NAMES"
- [nzValue]="framework"
- [nzLabel]="framework"
- ></nz-option>
- </nz-select>
- </div>
- <div class="single-field-group">
- <label for="namespace">
- <span class="red-star">*</span>
- Namespace
- </label>
- <input nz-input name="namespace" formControlName="namespace" />
- </div>
- <div class="single-field-group">
- <label for="cmd">
- <span class="red-star">*</span>
- Command
- </label>
- <input nz-input name="cmd" formControlName="cmd" placeholder="Command to run" />
- </div>
- <div class="single-field-group">
- <label for="image">
- <span class="red-star">*</span>
- Image
- </label>
- <input nz-input name="image" formControlName="image" placeholder="Image to use" />
- </div>
- </div>
- <div *ngSwitchCase="1" id="page2">
- <div>
- <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">
- <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
- 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)="onCreateSpec()">
- Add new spec
- </button>
- <ul formArrayName="specs" class="list-container">
- <ng-container *ngFor="let spec of specs.controls; index as i">
- <li *ngIf="i | indexInRange: currentSpecPage:PAGESIZE" [formGroupName]="i" class="input-group">
- <div id="spec{{ i }}">
- <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"
- nzTheme="fill"
- class="delete-icon"
- (click)="deleteItem(specs, i)"
- ></i>
- </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') }}
- </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>
+ <nz-button-group id="form-type-container" *ngIf="!modalProps.formType">
+ <button nz-button nzType="primary" id="customized" (click)="modalProps.formType = 'customized'">
+ Define your experiment
+ </button>
+ <button nz-button nzType="primary" id="pre" (click)="modalProps.formtype = 'predefined'" [disabled]="true">
+ From predefined experiment library
+ </button>
+ </nz-button-group>
+ <div *nzModalFooter>
+ <button nz-button nzType="default" (click)="closeModal()">Cancel</button>
+ <button
+ id="go"
+ nz-button
+ nzType="primary"
+ *ngIf="modalProps.formType"
+ [disabled]="nextBtnDisable"
+ (click)="proceedForm()"
+ >
+ {{ modalProps.okText }}
+ </button>
+ <button *ngIf="modalProps.currentStep > 0" nz-button nzType="default" style="float: left;" (click)="prevForm()">
+ Prev Step
+ </button>
</div>
+ <experiment-customized-form
+ [mode]="mode"
+ [targetId]="targetId"
+ [targetSpec]="targetSpec"
+ *ngIf="this.modalProps.formType === 'customized'"
+ ></experiment-customized-form>
</nz-modal>
</nz-layout>
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 ac1dc47..ba2f168 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
@@ -32,6 +32,16 @@
padding-right: 10px;
}
+ #form-type-container {
+ display: grid;
+ grid-auto-rows: 5rem;
+ grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
+ column-gap: 3rem;
+ & button {
+ height: 100%;
+ }
+ }
+
input.ng-invalid.ng-touched {
border: 1px solid red;
}
@@ -39,91 +49,3 @@
textarea.ng-invalid.ng-touched {
border: 1px solid red;
}
-
-.red-star {
- margin-top: 20px;
- color: red;
-}
-
-.list-container {
- padding: 0;
- margin: 1rem 0;
-}
-
-/* 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;
- font-size: .8rem;
- &:not(:first-child) {
- margin-top: 1rem;
- }
-
- & > *: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: 20px;
- margin-top: 20px;
- }
-}
-
-.memory-input-group {
- display: flex;
- & input {
- width: 70%;
- }
-
- & > * {
- width: 30%;
- }
-}
-
-/* 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;
-}
-
-/* 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 f22ee4a..fde4738 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,24 +18,25 @@
*/
import { Component, OnInit } from '@angular/core';
-import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, NavigationStart, Router } from '@angular/router';
import { ExperimentInfo } from '@submarine/interfaces/experiment-info';
-import { ExperimentSpec, Specs, SpecEnviroment, SpecMeta } from '@submarine/interfaces/experiment-spec';
import { ExperimentService } from '@submarine/services/experiment.service';
-import { ExperimentFormService } from '@submarine/services/experiment.validator.service';
-import { nanoid } from 'nanoid';
import { NzMessageService } from 'ng-zorro-antd';
+import { ExperimentSpec } from '@submarine/interfaces/experiment-spec';
+import { ExperimentFormService } from '@submarine/services/experiment.form.service';
+import { ModalProps } from '@submarine/interfaces/modal-props';
@Component({
selector: 'submarine-experiment',
templateUrl: './experiment.component.html',
- styleUrls: ['./experiment.component.scss']
+ styleUrls: ['./experiment.component.scss'],
+ providers: [ExperimentFormService]
})
export class ExperimentComponent implements OnInit {
experimentList: ExperimentInfo[] = [];
checkedList: boolean[] = [];
selectAllChecked: boolean = false;
+
// About experiment information
isInfo = false;
experimentID: string;
@@ -44,27 +45,19 @@ export class ExperimentComponent implements OnInit {
showExperiment = 'All';
searchText = '';
- // About new experiment
- experiment: FormGroup;
- current = 0;
- okText = 'Next Step';
- isVisible = false;
+ // About form management
+ modalProps: ModalProps = {
+ okText: 'Next step',
+ isVisible: false,
+ currentStep: 0,
+ formType: null
+ };
+ nextBtnDisable: boolean = true;
- // About update
+ // About update and clone
mode: 'create' | 'update' | 'clone' = '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;
- PAGESIZE = 5;
-
- // About spec
- currentSpecPage = 1;
+ targetId: string = null;
+ targetSpec: ExperimentSpec = null;
statusColor: { [key: string]: string } = {
Accepted: 'gold',
@@ -74,24 +67,14 @@ export class ExperimentComponent implements OnInit {
};
constructor(
- private experimentService: ExperimentService,
- private experimentFormService: ExperimentFormService,
private nzMessageService: NzMessageService,
private route: ActivatedRoute,
- private router: Router
+ private router: Router,
+ private experimentService: ExperimentService,
+ private experimentFormService: ExperimentFormService
) {}
ngOnInit() {
- this.experiment = new FormGroup({
- experimentName: new FormControl(null, Validators.required),
- description: new FormControl(null, [Validators.required]),
- frameworks: new FormControl('TensorFlow', [Validators.required]),
- namespace: new FormControl('default', [Validators.required]),
- 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';
this.experimentID = this.route.snapshot.params.id;
@@ -107,250 +90,47 @@ export class ExperimentComponent implements OnInit {
}
});
- this.reloadCheck();
- }
-
- // Getters of experiment request form
- get experimentName() {
- return this.experiment.get('experimentName');
- }
- get description() {
- return this.experiment.get('description');
- }
- get frameworks() {
- return this.experiment.get('frameworks');
- }
- get namespace() {
- return this.experiment.get('namespace');
- }
- get cmd() {
- return this.experiment.get('cmd');
- }
- get envs() {
- return this.experiment.get('envs') as FormArray;
- }
- get image() {
- return this.experiment.get('image');
- }
- get specs() {
- return this.experiment.get('specs') as FormArray;
- }
- /**
- * Check the validity of the experiment page
- *
- */
- checkStatus() {
- if (this.current === 0) {
- return (
- this.experimentName.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;
- }
- }
-
- /**
- * 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(mode: 'create' | 'update' | 'clone') {
- this.mode = mode;
- this.current = 0;
- this.okText = 'Next step';
- this.isVisible = true;
- this.updateId = null;
- // Reset the form
- this.experimentName.enable();
- this.envs.clear();
- this.specs.clear();
- this.experiment.reset({ frameworks: 'TensorFlow', namespace: 'default' });
- }
-
- /**
- * Event handler for Next step/Submit button
- */
- handleOk() {
- if (this.current === 1) {
- this.okText = 'Submit';
- } else if (this.current === 2) {
- if (this.mode === 'create') {
- const newSpec = this.constructSpec();
- this.experimentService.createExperiment(newSpec).subscribe({
- next: (result) => {
- this.fetchExperimentList();
- },
- 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(
- () => {
- this.fetchExperimentList();
- },
- (msg) => {
- this.nzMessageService.error(`${msg}, please try again`, {
- nzPauseOnHover: true
- });
- },
- () => {
- this.nzMessageService.success('Modification succeeds!');
- this.isVisible = false;
- }
- );
- } else if (this.mode === 'clone') {
- const newSpec = this.constructSpec();
- this.experimentService.createExperiment(newSpec).subscribe(
- () => {
- this.fetchExperimentList();
- },
- (msg) => {
- this.nzMessageService.error(`${msg}, please try again`, {
- nzPauseOnHover: true
- });
- },
- () => {
- this.nzMessageService.success('Create a new experiment !');
- this.isVisible = false;
- }
- );
- }
- }
-
- if (this.current < 2) {
- this.current++;
- }
- }
+ // Subscriptions to experimentFormService
+ this.experimentFormService.fetchListService.subscribe(() => {
+ this.fetchExperimentList();
+ });
+ this.experimentFormService.btnStatusService.subscribe((status) => {
+ this.nextBtnDisable = status;
+ });
+ this.experimentFormService.modalPropsService.subscribe((props) => {
+ this.modalProps = { ...this.modalProps, ...props };
+ });
- /**
- * Create a new env variable input
- */
- createEnv(defaultKey: string = '', defaultValue: string = '') {
- // Create a new FormGroup
- return new FormGroup(
- {
- key: new FormControl(defaultKey, [Validators.required]),
- value: new FormControl(defaultValue, [Validators.required])
- },
- [this.experimentFormService.envValidator]
- );
- }
- /**
- * Create a new spec
- */
- createSpec(
- defaultName: string = '',
- defaultReplica: number = 1,
- defaultCpu: number = 1,
- defaultMemory: string = '',
- defaultUnit: string = 'M'
- ): FormGroup {
- return new FormGroup(
- {
- name: new FormControl(defaultName, [Validators.required]),
- replicas: new FormControl(defaultReplica, [Validators.min(1), Validators.required]),
- cpus: new FormControl(defaultCpu, [Validators.min(1), Validators.required]),
- memory: new FormGroup(
- {
- num: new FormControl(defaultMemory, [Validators.required]),
- unit: new FormControl(defaultUnit, [Validators.required])
- },
- [this.experimentFormService.memoryValidator]
- )
- },
- [this.experimentFormService.specValidator]
- );
+ this.reloadCheck();
}
- /**
- * 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;
+ initModal(
+ initMode: 'create' | 'update' | 'clone',
+ initFormType = null,
+ id: string = null,
+ spec: ExperimentSpec = null
+ ) {
+ this.mode = initMode;
+ this.modalProps.isVisible = true;
+ this.modalProps.formType = initFormType;
+
+ if (initMode === 'update') {
+ // Keep id for later request
+ this.targetId = id;
+ this.targetSpec = spec;
}
}
- /**
- * 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) {
- this.currentSpecPage += 1;
- }
+ closeModal() {
+ this.experimentFormService.modalPropsClear();
}
- /**
- * 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').get('num').value}${
- spec.get('memory').get('unit').value
- }`
- };
- }
- }
-
- const environment: SpecEnviroment = {
- image: this.image.value
- };
-
- const newExperimentSpec: ExperimentSpec = {
- meta: meta,
- environment: environment,
- spec: specs
- };
-
- return newExperimentSpec;
+ proceedForm() {
+ this.experimentFormService.stepChange(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);
+ prevForm() {
+ this.experimentFormService.stepChange(-1);
}
fetchExperimentList() {
@@ -376,50 +156,6 @@ export class ExperimentComponent implements OnInit {
});
}
- 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.cloneExperiment(spec);
- }
-
- onCloneExperiment(spec: ExperimentSpec) {
- // Open Modal in update mode
- this.initExperimentStatus('clone');
- // Prevent user from modifying the name
- this.experimentName.enable();
- // Put value back
- const id: string = nanoid(8);
- const cloneExperimentName = spec.meta.name + '-' + id;
- this.experimentName.setValue(cloneExperimentName.toLocaleLowerCase());
- this.cloneExperiment(spec);
- }
-
- cloneExperiment(spec: ExperimentSpec) {
- 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, 10), parseInt(cpuCount, 10), memory, unit);
- this.specs.push(newSpec);
- }
- }
-
onDeleteExperiment(id: string, onMessage: boolean) {
this.experimentService.deleteExperiment(id).subscribe(
() => {
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.module.ts b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.module.ts
index 966f647..c5563d2 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.module.ts
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/experiment/experiment.module.ts
@@ -8,7 +8,10 @@ import { ExperimentInfoComponent } from './experiment-info/experiment-info.compo
import { HyperParamsComponent } from './experiment-info/hyper-params/hyper-params.component';
import { MetricsComponent } from './experiment-info/metrics/metrics.component';
import { OutputsComponent } from './experiment-info/outputs/outputs.component';
-
+import { ExperimentCustomizedForm } from './experiment-customized-form/experiment-customized-form.component';
+import { PipeSharedModule } from '@submarine/pipe/pipe-shared.module';
+import { ExperimentComponent } from './experiment.component';
+import { RouterModule } from '@angular/router';
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
@@ -29,8 +32,24 @@ import { OutputsComponent } from './experiment-info/outputs/outputs.component';
*/
@NgModule({
- exports: [ReactiveFormsModule],
- imports: [NgZorroAntdModule, CommonModule, FormsModule, NgxChartsModule],
- declarations: [ExperimentInfoComponent, HyperParamsComponent, MetricsComponent, ChartsComponent, OutputsComponent]
+ exports: [ReactiveFormsModule, ExperimentComponent],
+ imports: [
+ ReactiveFormsModule,
+ NgZorroAntdModule,
+ CommonModule,
+ FormsModule,
+ NgxChartsModule,
+ RouterModule,
+ PipeSharedModule
+ ],
+ declarations: [
+ ExperimentComponent,
+ ExperimentInfoComponent,
+ HyperParamsComponent,
+ MetricsComponent,
+ ChartsComponent,
+ OutputsComponent,
+ ExperimentCustomizedForm
+ ]
})
export class ExperimentModule {}
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 a652add..af8b64d 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
@@ -41,7 +41,6 @@ import { EnvironmentComponent } from './environment/environment.component';
WorkbenchComponent,
HomeComponent,
WorkspaceComponent,
- ExperimentComponent,
DataComponent,
ModelComponent,
EnvironmentComponent
diff --git a/submarine-workbench/workbench-web-ng/src/app/services/experiment.form.service.ts b/submarine-workbench/workbench-web-ng/src/app/services/experiment.form.service.ts
new file mode 100644
index 0000000..f937357
--- /dev/null
+++ b/submarine-workbench/workbench-web-ng/src/app/services/experiment.form.service.ts
@@ -0,0 +1,60 @@
+/*!
+ * 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 { Injectable } from '@angular/core';
+import { Subject } from 'rxjs';
+import { ModalProps } from '@submarine/interfaces/modal-props';
+
+@Injectable()
+export class ExperimentFormService {
+ // Subject(observable source)
+ private stepServiceSource = new Subject<number>();
+ private fetchListServiceSource = new Subject<boolean>();
+ private btnStatusServiceSource = new Subject<boolean>();
+ private modalPropsServiceSource = new Subject<ModalProps>();
+
+ // Observable streams
+ stepService = this.stepServiceSource.asObservable();
+ fetchListService = this.fetchListServiceSource.asObservable();
+ btnStatusService = this.btnStatusServiceSource.asObservable();
+ modalPropsService = this.modalPropsServiceSource.asObservable();
+
+ // Event emitter
+ stepChange(step: number) {
+ this.stepServiceSource.next(step);
+ }
+ btnStatusChange(status: boolean) {
+ this.btnStatusServiceSource.next(status);
+ }
+ modalPropsChange(props: ModalProps) {
+ this.modalPropsServiceSource.next(props);
+ }
+ // Syntax sugar for reset modal
+ modalPropsClear() {
+ this.modalPropsChange({
+ okText: 'Next step',
+ isVisible: false,
+ currentStep: 0,
+ formType: null
+ });
+ }
+ fetchList() {
+ this.fetchListServiceSource.next(true);
+ }
+}
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 df09c9c..d15ca2a 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
@@ -17,14 +17,13 @@
* under the License.
*/
-import { FormGroup, ValidatorFn, ValidationErrors, FormControl, FormArray } from '@angular/forms';
+import { FormGroup, ValidatorFn, ValidationErrors, FormArray } from '@angular/forms';
import { Injectable } from '@angular/core';
-import { ExperimentModule } from '@submarine/pages/workbench/experiment/experiment.module';
@Injectable({
- providedIn: ExperimentModule
+ providedIn: 'root'
})
-export class ExperimentFormService {
+export class ExperimentValidatorService {
/**
* The validator for env key/value pair
* @param envGroup A FormGroup resides in `envs` FromArray in createExperiment
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@submarine.apache.org
For additional commands, e-mail: dev-help@submarine.apache.org