diff --git a/frontend/.prettierrc b/frontend/.prettierrc
index ae751f30..6112aa18 100644
--- a/frontend/.prettierrc
+++ b/frontend/.prettierrc
@@ -1,11 +1,5 @@
{
- printWidth: 80,
- useTabs: false,
- tabWidth: 2,
- semi: true,
- singleQuote: true,
- trailingComma: 'all',
- bracketSpacing: true,
- arrowParens: 'avoid',
- proseWrap: 'preserve',
+ "arrowParens": "avoid",
+ "bracketSpacing": false,
+ "trailingComma": "none"
}
diff --git a/frontend/src/app/resource-form/form-data-volumes/form-data-volumes.component.html b/frontend/src/app/resource-form/form-data-volumes/form-data-volumes.component.html
index c4af5501..f30b6da0 100644
--- a/frontend/src/app/resource-form/form-data-volumes/form-data-volumes.component.html
+++ b/frontend/src/app/resource-form/form-data-volumes/form-data-volumes.component.html
@@ -25,6 +25,7 @@
[pvcs]="pvcs"
[ephemeral]="false"
[defaultStorageClass]="defaultStorageClass"
+ [sizes]="['4Gi', '8Gi', '16Gi', '32Gi', '64Gi', '128Gi', '256Gi', '512Gi']"
>
diff --git a/frontend/src/app/resource-form/form-name/form-name.component.html b/frontend/src/app/resource-form/form-name/form-name.component.html
index 05e0d06c..0c98596c 100644
--- a/frontend/src/app/resource-form/form-name/form-name.component.html
+++ b/frontend/src/app/resource-form/form-name/form-name.component.html
@@ -16,7 +16,9 @@
formControlName="name"
#name
/>
- {{ showNameError() }}
+
+ {{ showNameError() }}
+
diff --git a/frontend/src/app/resource-form/form-name/form-name.component.ts b/frontend/src/app/resource-form/form-name/form-name.component.ts
index cb2eb9d2..d98ca759 100644
--- a/frontend/src/app/resource-form/form-name/form-name.component.ts
+++ b/frontend/src/app/resource-form/form-name/form-name.component.ts
@@ -22,11 +22,11 @@ export class FormNameComponent implements OnInit, OnDestroy {
constructor(private k8s: KubernetesService, private ns: NamespaceService) {}
ngOnInit() {
- // Add the ExistingName validator to the list if it doesn't already exist
+ // Add validator for notebook name (existing name, length, lowercase alphanumeric and '-')
this.parentForm
.get("name")
- .setValidators([Validators.required, this.existingName()]);
-
+ .setValidators([Validators.required, this.existingNameValidator(), Validators.pattern(/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/), Validators.maxLength(52)]);
+
// Keep track of the existing Notebooks in the selected Namespace
// Use these names to check if the input name exists
const nsSub = this.ns.getSelectedNamespace().subscribe(ns => {
@@ -45,18 +45,26 @@ export class FormNameComponent implements OnInit, OnDestroy {
showNameError() {
const nameCtrl = this.parentForm.get("name");
-
+
+ if (nameCtrl.value.length==0) {
+ return `The Notebook Server's name can't be empty`;
+ }
if (nameCtrl.hasError("existingName")) {
return `Notebook Server "${nameCtrl.value}" already exists`;
- } else {
- return "The Notebook Server's name can't be empty";
+ }
+ if (nameCtrl.hasError("pattern")) {
+ return `The Notebook Server's name can only contain lowercase alphanumeric characters or '-' and must start and end with an alphanumeric character`;
+ }
+ if (nameCtrl.hasError("maxlength")) {
+ return `The Notebook Server's name can't be more than 52 characters`;
}
}
- private existingName(): ValidatorFn {
+ private existingNameValidator(): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } => {
const exists = this.notebooks.has(control.value);
return exists ? { existingName: true } : null;
};
}
+
}
diff --git a/frontend/src/app/resource-form/form-specs/form-specs.component.html b/frontend/src/app/resource-form/form-specs/form-specs.component.html
index 332a05f0..7c2a2e22 100644
--- a/frontend/src/app/resource-form/form-specs/form-specs.component.html
+++ b/frontend/src/app/resource-form/form-specs/form-specs.component.html
@@ -12,14 +12,24 @@
CPU
-
- Please provide the CPU requirements
+
+ {{ cpuErrorMessage() }}
Memory
-
- Please provide the RAM requirements
+
+ {{ memoryErrorMessage() }}
diff --git a/frontend/src/app/resource-form/form-specs/form-specs.component.ts b/frontend/src/app/resource-form/form-specs/form-specs.component.ts
index 24559216..6a1c78ed 100644
--- a/frontend/src/app/resource-form/form-specs/form-specs.component.ts
+++ b/frontend/src/app/resource-form/form-specs/form-specs.component.ts
@@ -1,5 +1,41 @@
-import { Component, OnInit, Input } from "@angular/core";
-import { FormGroup } from "@angular/forms";
+import {Component, OnInit, Input} from "@angular/core";
+import {
+ FormGroup,
+ AbstractControl,
+ Validators,
+ ValidatorFn,
+ ValidationErrors,
+ FormControl,
+ FormGroupDirective,
+ NgForm
+} from "@angular/forms";
+
+const MAX_FOR_GPU: ReadonlyMap = new Map([
+ [0, {cpu: 15, ram: 96}],
+ [1, {cpu: 5, ram: 48}]
+]);
+
+type MaxResourceSpec = {cpu: number; ram: number};
+
+function resourcesValidator(): ValidatorFn {
+ return function (control: AbstractControl): ValidationErrors | null {
+ const gpuNumValue = control.get("gpus").get("num").value;
+ const gpu = gpuNumValue === "none" ? 0 : parseInt(gpuNumValue, 10) || 0;
+ const cpu = parseFloat(control.get("cpu").value);
+ const ram = parseFloat(control.get("memory").value);
+ const errors = {};
+
+ const max = MAX_FOR_GPU.get(gpu);
+ if (cpu > max.cpu) {
+ errors["maxCpu"] = {max: max.cpu, gpu};
+ }
+ if (ram > max.ram) {
+ errors["maxRam"] = {max: max.ram, gpu};
+ }
+
+ return Object.entries(errors).length > 0 ? errors : null;
+ };
+}
@Component({
selector: "app-form-specs",
@@ -11,7 +47,70 @@ export class FormSpecsComponent implements OnInit {
@Input() readonlyCPU: boolean;
@Input() readonlyMemory: boolean;
- constructor() {}
+ ngOnInit() {
+ this.parentForm
+ .get("cpu")
+ .setValidators([
+ Validators.required,
+ Validators.pattern(/^[0-9]+([.][0-9]+)?$/),
+ Validators.min(0.5)
+ ]);
+ this.parentForm
+ .get("memory")
+ .setValidators([
+ Validators.required,
+ Validators.pattern(/^[0-9]+([.][0-9]+)?(Gi)$/),
+ Validators.min(1)
+ ]);
+ this.parentForm.setValidators(resourcesValidator());
+ }
+
+ parentErrorKeysErrorStateMatcher(keys: string | string[]) {
+ const arrKeys = ([] as string[]).concat(keys);
+ return {
+ isErrorState(
+ control: FormControl,
+ form: FormGroupDirective | NgForm
+ ): boolean {
+ return (
+ (control.dirty && control.invalid) ||
+ (form.dirty && arrKeys.some(key => form.hasError(key)))
+ );
+ }
+ };
+ }
+
+ cpuErrorMessage(): string {
+ let e: any;
+ const errs = this.parentForm.get("cpu").errors || {};
+
+ if (errs.required) return "Specify number of CPUs";
+ if (errs.pattern) return "Must be a number";
+ if ((e = errs.min)) return `Specify at least ${e.min} CPUs`;
+
+ if (this.parentForm.hasError("maxCpu")) {
+ e = this.parentForm.errors.maxCpu;
+ return (
+ `Can't exceed ${e.max} CPUs` +
+ (e.gpu > 0 ? ` with ${e.gpu} GPU(s) selected` : "")
+ );
+ }
+ }
+
+ memoryErrorMessage(): string {
+ let e: any;
+ const errs = this.parentForm.get("memory").errors || {};
+
+ if (errs.required || errs.pattern)
+ return "Specify amount of memory (e.g. 2Gi)";
+ if ((e = errs.min)) return `Specify at least ${e.min}Gi of memory`;
- ngOnInit() {}
+ if (this.parentForm.hasError("maxRam")) {
+ e = this.parentForm.errors.maxRam;
+ return (
+ `Can't exceed ${e.max}Gi of memory` +
+ (e.gpu > 0 ? ` with ${e.gpu} GPU(s) selected` : "")
+ );
+ }
+ }
}
diff --git a/frontend/src/app/resource-form/form-workspace-volume/form-workspace-volume.component.html b/frontend/src/app/resource-form/form-workspace-volume/form-workspace-volume.component.html
index 4d7c879a..fe39634e 100644
--- a/frontend/src/app/resource-form/form-workspace-volume/form-workspace-volume.component.html
+++ b/frontend/src/app/resource-form/form-workspace-volume/form-workspace-volume.component.html
@@ -18,6 +18,7 @@
[ephemeral]="parentForm.value.noWorkspace"
[namespace]="parentForm.value.namespace"
[defaultStorageClass]="defaultStorageClass"
+ [sizes]="['4Gi', '8Gi', '16Gi', '32Gi']"
>
diff --git a/frontend/src/app/resource-form/volume/volume.component.html b/frontend/src/app/resource-form/volume/volume.component.html
index bfc2efec..d6c6b671 100644
--- a/frontend/src/app/resource-form/volume/volume.component.html
+++ b/frontend/src/app/resource-form/volume/volume.component.html
@@ -22,13 +22,20 @@
}}
+
+ {{ showNameError() }}
+
Size
-
-
+
+ {{
+ sizeOptions
+ }}
+
+
diff --git a/frontend/src/app/resource-form/volume/volume.component.ts b/frontend/src/app/resource-form/volume/volume.component.ts
index be8bdbcc..aa0a399b 100644
--- a/frontend/src/app/resource-form/volume/volume.component.ts
+++ b/frontend/src/app/resource-form/volume/volume.component.ts
@@ -1,5 +1,5 @@
import { Component, OnInit, Input, OnDestroy } from "@angular/core";
-import { FormGroup } from "@angular/forms";
+import { FormGroup, Validators } from "@angular/forms";
import { Volume } from "src/app/utils/types";
import { Subscription } from "rxjs";
@@ -14,13 +14,14 @@ export class VolumeComponent implements OnInit, OnDestroy {
currentPVC: Volume;
existingPVCs: Set = new Set();
-
+
subscriptions = new Subscription();
// ----- @Input Parameters -----
@Input() volume: FormGroup;
@Input() namespace: string;
-
+ @Input() sizes: Set;
+
@Input()
get notebookName() {
return this._notebookName;
@@ -102,8 +103,11 @@ export class VolumeComponent implements OnInit, OnDestroy {
constructor() {}
ngOnInit() {
- // type
- this.subscriptions.add(
+ this.volume
+ .get("name")
+ .setValidators([Validators.required, Validators.pattern(/^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/)]);
+
+ this.subscriptions.add(
this.volume.get("type").valueChanges.subscribe((type: string) => {
this.setVolumeType(type);
})
@@ -158,4 +162,17 @@ export class VolumeComponent implements OnInit, OnDestroy {
this.volume.controls.name.setValue(this.currentVolName);
}
}
+
+ showNameError() {
+ const volumeName = this.volume.get("name");
+
+ if (volumeName.hasError("required")) {
+ return `The volume name can't be empty`
+ }
+ if (volumeName.hasError("pattern")) {
+ return `The volume name can only contain lowercase alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character`;
+ }
+ }
+
+
}
diff --git a/frontend/src/app/utils/common.ts b/frontend/src/app/utils/common.ts
index 9f611fc3..fe2e6bf2 100644
--- a/frontend/src/app/utils/common.ts
+++ b/frontend/src/app/utils/common.ts
@@ -86,7 +86,7 @@ export function addDataVolume(
value: '{notebook-name}-vol-' + (l + 1),
},
size: {
- value: '10Gi',
+ value: '16Gi',
},
mountPath: {
value: '/home/jovyan/data-vol-' + (l + 1),
diff --git a/samples/spawner_ui_config.yaml b/samples/spawner_ui_config.yaml
index ccd033e3..668484ec 100644
--- a/samples/spawner_ui_config.yaml
+++ b/samples/spawner_ui_config.yaml
@@ -59,7 +59,7 @@ spawnerFormDefaults:
value: 'workspace-{notebook-name}'
size:
# The Size of the Workspace Volume (in Gi)
- value: '10Gi'
+ value: '4Gi'
mountPath:
# The Path that the Workspace Volume will be mounted
value: /home/jovyan
@@ -87,7 +87,7 @@ spawnerFormDefaults:
# name:
# value: '{notebook-name}-vol-1'
# size:
- # value: '10Gi'
+ # value: '16Gi'
# class:
# value: standard
# mountPath:
@@ -102,7 +102,7 @@ spawnerFormDefaults:
# name:
# value: '{notebook-name}-vol-2'
# size:
- # value: '10Gi'
+ # value: '16Gi'
# mountPath:
# value: /home/jovyan/vol-2
# accessModes: