[UI] Make sure form validation displays non-valid fields as red in all forms (#7064)

* Add validation to multi-container component

This covers the following forms:
- Add commands when adding a Composite Command

Co-authored-by: Philippe Martin <phmartin@redhat.com>

* Add validation to multi-key-value component

This covers the following forms:
- Add Environment variables in Create Container
- Add Deployment annotations in Create Container
- Add Service annotations in Create Container

Co-authored-by: Philippe Martin <phmartin@redhat.com>

* Add validation to multi-text component

This covers the following forms:
- Add Command in Create Container
- Add Args in Create Container
- Add Args in Create Image

Co-authored-by: Philippe Martin <phmartin@redhat.com>

* Add validation to select-container component

This covers the following forms:
- Select or Create container in Add Exec Command
- Select or create image component in Add Image Command
- Select or create Resource in Add Apply command

Co-authored-by: Philippe Martin <phmartin@redhat.com>

* Add validation to volume-mounts component

This covers the following forms:
- Select or Create volume mount in Create container

Co-authored-by: Philippe Martin <phmartin@redhat.com>

* Add error helper message for invalid volume size quantities

* Fix Cypress tests

* Generate static UI

* fixup! Add error helper message for invalid volume size quantities

Co-authored-by: Philippe Martin <phmartin@redhat.com>

* Generate static UI

---------

Co-authored-by: Philippe Martin <phmartin@redhat.com>
This commit is contained in:
Armel Soro
2023-09-05 17:14:37 +02:00
committed by GitHub
parent 3f93ac0744
commit adc96994d9
14 changed files with 230 additions and 133 deletions

View File

@@ -11,6 +11,6 @@
<body class="mat-typography"> <body class="mat-typography">
<div id="loading">Loading, please wait...</div> <div id="loading">Loading, please wait...</div>
<app-root></app-root> <app-root></app-root>
<script src="runtime.1289ea0acffcdc5e.js" type="module"></script><script src="polyfills.8b3b37cedaf377c3.js" type="module"></script><script src="main.4d8dc3ef32c88ca3.js" type="module"></script> <script src="runtime.1289ea0acffcdc5e.js" type="module"></script><script src="polyfills.8b3b37cedaf377c3.js" type="module"></script><script src="main.9400449aa2437590.js" type="module"></script>
</body></html> </body></html>

File diff suppressed because one or more lines are too long

View File

@@ -62,7 +62,7 @@ describe('devfile editor spec', () => {
cy.getByDataCy('container-env-value-2').type("val3"); cy.getByDataCy('container-env-value-2').type("val3");
cy.getByDataCy('volume-mount-add').click(); cy.getByDataCy('volume-mount-add').click();
cy.getByDataCy('volume-mount-path-0').type("/mnt/vol1"); cy.getByDataCy('volume-mount-path-0').type("/mnt/vol1", {force: true});
cy.getByDataCy('volume-mount-name-0').click().get('mat-option').contains('volume1').click(); cy.getByDataCy('volume-mount-name-0').click().get('mat-option').contains('volume1').click();
cy.getByDataCy('endpoints-add').click(); cy.getByDataCy('endpoints-add').click();
@@ -70,7 +70,7 @@ describe('devfile editor spec', () => {
cy.getByDataCy('endpoint-targetPort-0').type("4001"); cy.getByDataCy('endpoint-targetPort-0').type("4001");
cy.getByDataCy('volume-mount-add').click(); cy.getByDataCy('volume-mount-add').click();
cy.getByDataCy('volume-mount-path-1').type("/mnt/vol2"); cy.getByDataCy('volume-mount-path-1').type("/mnt/vol2", {force: true});
cy.getByDataCy('volume-mount-name-1').click().get('mat-option').contains('(New Volume)').click(); cy.getByDataCy('volume-mount-name-1').click().get('mat-option').contains('(New Volume)').click();
cy.getByDataCy('volume-name').type('volume2'); cy.getByDataCy('volume-name').type('volume2');
cy.getByDataCy('volume-create').click(); cy.getByDataCy('volume-create').click();
@@ -134,11 +134,11 @@ describe('devfile editor spec', () => {
cy.getByDataCy('container-source-mapping').type('/mnt/sources'); cy.getByDataCy('container-source-mapping').type('/mnt/sources');
cy.getByDataCy('volume-mount-add').click(); cy.getByDataCy('volume-mount-add').click();
cy.getByDataCy('volume-mount-path-0').type("/mnt/vol1"); cy.getByDataCy('volume-mount-path-0').type("/mnt/vol1", {force: true});
cy.getByDataCy('volume-mount-name-0').click().get('mat-option').contains('volume1').click(); cy.getByDataCy('volume-mount-name-0').click().get('mat-option').contains('volume1').click();
cy.getByDataCy('volume-mount-add').click(); cy.getByDataCy('volume-mount-add').click();
cy.getByDataCy('volume-mount-path-1').type("/mnt/vol2"); cy.getByDataCy('volume-mount-path-1').type("/mnt/vol2", {force: true});
cy.getByDataCy('volume-mount-name-1').click().get('mat-option').contains('(New Volume)').click(); cy.getByDataCy('volume-mount-name-1').click().get('mat-option').contains('(New Volume)').click();
cy.getByDataCy('volume-name').type('volume2'); cy.getByDataCy('volume-name').type('volume2');
cy.getByDataCy('volume-create').click(); cy.getByDataCy('volume-create').click();
@@ -397,11 +397,11 @@ describe('devfile editor spec', () => {
cy.getByDataCy('container-image').type('an-image'); cy.getByDataCy('container-image').type('an-image');
cy.getByDataCy('volume-mount-add').click(); cy.getByDataCy('volume-mount-add').click();
cy.getByDataCy('volume-mount-path-0').type("/mnt/vol1"); cy.getByDataCy('volume-mount-path-0').type("/mnt/vol1", {force: true});
cy.getByDataCy('volume-mount-name-0').click().get('mat-option').contains('volume1').click(); cy.getByDataCy('volume-mount-name-0').click().get('mat-option').contains('volume1').click();
cy.getByDataCy('volume-mount-add').click(); cy.getByDataCy('volume-mount-add').click();
cy.getByDataCy('volume-mount-path-1').type("/mnt/vol2"); cy.getByDataCy('volume-mount-path-1').type("/mnt/vol2", {force: true});
cy.getByDataCy('volume-mount-name-1').click().get('mat-option').contains('(New Volume)').click(); cy.getByDataCy('volume-mount-name-1').click().get('mat-option').contains('(New Volume)').click();
cy.getByDataCy('volume-name').type('volume2'); cy.getByDataCy('volume-name').type('volume2');
cy.getByDataCy('volume-create').click(); cy.getByDataCy('volume-create').click();

View File

@@ -1,14 +1,15 @@
<h3>{{title}}</h3> <h3>{{title}}</h3>
<div class="group"> <div class="group">
<span *ngFor="let command of commands; let i=index"> <span *ngFor="let control of form.controls; index as i">
<mat-form-field appearance="fill"> <mat-form-field appearance="fill">
<mat-select [value]="command" (selectionChange)="onCommandChange(i, $event.value)"> <mat-label><span>Command</span></mat-label>
<mat-select [formControl]="control">
<mat-option *ngFor="let commandElement of commandList" [value]="commandElement">{{commandElement}}</mat-option> <mat-option *ngFor="let commandElement of commandList" [value]="commandElement">{{commandElement}}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</span> </span>
<button *ngIf="commands.length > 0" mat-icon-button (click)="addCommand()"> <button *ngIf="form.controls.length > 0" mat-icon-button (click)="addCommand('')">
<mat-icon class="tab-icon material-icons-outlined">add</mat-icon> <mat-icon class="tab-icon material-icons-outlined">add</mat-icon>
</button> </button>
<button *ngIf="commands.length == 0" mat-flat-button (click)="addCommand()">{{addLabel}}</button> <button *ngIf="form.controls.length == 0" mat-flat-button (click)="addCommand('')">{{addLabel}}</button>
</div> </div>

View File

@@ -1,5 +1,14 @@
import { Component, Input } from '@angular/core'; import {Component, forwardRef, Input} from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms'; import {
AbstractControl,
ControlValueAccessor,
FormArray,
FormControl,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR, ValidationErrors, Validator,
Validators
} from '@angular/forms';
@Component({ @Component({
selector: 'app-multi-command', selector: 'app-multi-command',
@@ -10,10 +19,15 @@ import { NG_VALUE_ACCESSOR } from '@angular/forms';
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
multi: true, multi: true,
useExisting: MultiCommandComponent useExisting: MultiCommandComponent
} },
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MultiCommandComponent),
multi: true,
},
] ]
}) })
export class MultiCommandComponent { export class MultiCommandComponent implements ControlValueAccessor, Validator {
@Input() addLabel: string = ""; @Input() addLabel: string = "";
@Input() commandList: string[] = []; @Input() commandList: string[] = [];
@@ -21,10 +35,16 @@ export class MultiCommandComponent {
onChange = (_: string[]) => {}; onChange = (_: string[]) => {};
commands: string[] = []; form = new FormArray<FormControl>([]);
writeValue(value: any) { constructor() {
this.commands = value; this.form.valueChanges.subscribe(value => {
this.onChange(value);
});
}
writeValue(value: string[]) {
value.forEach(v => this.addCommand(v));
} }
registerOnChange(onChange: any) { registerOnChange(onChange: any) {
@@ -33,13 +53,19 @@ export class MultiCommandComponent {
registerOnTouched(_: any) {} registerOnTouched(_: any) {}
addCommand() { newCommand(cmdName : string) {
this.commands.push(""); return new FormControl(cmdName, [Validators.required]);
this.onChange(this.commands);
} }
onCommandChange(i: number, cmd: string) { addCommand(cmdName: string) {
this.commands[i] = cmd; this.form.push(this.newCommand(cmdName));
this.onChange(this.commands); }
/* Validator implementation */
validate(control: AbstractControl): ValidationErrors | null {
if (!this.form.valid) {
return {'internal': true};
}
return null;
} }
} }

View File

@@ -1,16 +1,18 @@
<div class="group"> <div class="group">
<span *ngFor="let entry of entries; let i=index"> <div *ngFor="let control of form.controls; index as i">
<mat-form-field class="mid-width" appearance="outline"> <ng-container [formGroup]="control">
<mat-label><span>Name</span></mat-label> <mat-form-field class="mid-width" appearance="outline">
<input [attr.data-cy]="dataCyPrefix+'-name-'+i" matInput [value]="entry.name" (change)="onKeyChange(i, $event)" (input)="onKeyChange(i, $event)"> <mat-label><span>Name</span></mat-label>
</mat-form-field> <input [attr.data-cy]="dataCyPrefix+'-name-'+i" matInput formControlName="name">
<mat-form-field class="mid-width" appearance="outline"> </mat-form-field>
<mat-label><span>Value</span></mat-label> <mat-form-field class="mid-width" appearance="outline">
<input [attr.data-cy]="dataCyPrefix+'-value-'+i" matInput [value]="entry.value" (change)="onValueChange(i, $event)" (input)="onValueChange(i, $event)"> <mat-label><span>Value</span></mat-label>
</mat-form-field> <input [attr.data-cy]="dataCyPrefix+'-value-'+i" matInput formControlName="value">
</span> </mat-form-field>
<button [attr.data-cy]="dataCyPrefix+'-plus'" *ngIf="entries.length > 0" mat-icon-button (click)="addEntry()"> </ng-container>
</div>
<button [attr.data-cy]="dataCyPrefix+'-plus'" *ngIf="form.controls.length > 0" mat-icon-button (click)="addEntry('', '')">
<mat-icon class="tab-icon material-icons-outlined">add</mat-icon> <mat-icon class="tab-icon material-icons-outlined">add</mat-icon>
</button> </button>
<button [attr.data-cy]="dataCyPrefix+'-add'" *ngIf="entries.length == 0" mat-flat-button (click)="addEntry()">{{addLabel}}</button> <button [attr.data-cy]="dataCyPrefix+'-add'" *ngIf="form.controls.length == 0" mat-flat-button (click)="addEntry('', '')">{{addLabel}}</button>
</div> </div>

View File

@@ -1,5 +1,16 @@
import { Component, Input, forwardRef } from '@angular/core'; import {Component, forwardRef, Input} from '@angular/core';
import { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator, Validators } from '@angular/forms'; import {
AbstractControl,
ControlValueAccessor,
FormArray,
FormControl,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
interface KeyValue { interface KeyValue {
name: string; name: string;
@@ -28,13 +39,19 @@ export class MultiKeyValueComponent implements ControlValueAccessor, Validator {
@Input() dataCyPrefix: string = ""; @Input() dataCyPrefix: string = "";
@Input() addLabel: string = ""; @Input() addLabel: string = "";
form = new FormArray<FormGroup>([]);
onChange = (_: KeyValue[]) => {}; onChange = (_: KeyValue[]) => {};
onValidatorChange = () => {}; onValidatorChange = () => {};
entries: KeyValue[] = []; constructor() {
this.form.valueChanges.subscribe(value => {
this.onChange(value);
});
}
writeValue(value: KeyValue[]) { writeValue(value: KeyValue[]) {
this.entries = value; value.forEach(v => this.addEntry(v.name, v.value));
} }
registerOnChange(onChange: any) { registerOnChange(onChange: any) {
@@ -43,30 +60,21 @@ export class MultiKeyValueComponent implements ControlValueAccessor, Validator {
registerOnTouched(_: any) {} registerOnTouched(_: any) {}
addEntry() { newKeyValueForm(kv: KeyValue): FormGroup {
this.entries.push({name: "", value: ""}); return new FormGroup({
this.onChange(this.entries); name: new FormControl(kv.name, [Validators.required]),
value: new FormControl(kv.value, [Validators.required]),
});
} }
onKeyChange(i: number, e: Event) { addEntry(name: string, value: string) {
const target = e.target as HTMLInputElement; this.form.push(this.newKeyValueForm({name, value}));
this.entries[i].name = target.value;
this.onChange(this.entries);
}
onValueChange(i: number, e: Event) {
const target = e.target as HTMLInputElement;
this.entries[i].value = target.value;
this.onChange(this.entries);
} }
/* Validator implementation */ /* Validator implementation */
validate(control: AbstractControl): ValidationErrors | null { validate(control: AbstractControl): ValidationErrors | null {
for (let i=0; i<this.entries.length; i++) { if (!this.form.valid) {
const entry = this.entries[i]; return {'internal': true};
if (entry.name == "" || entry.value == "") {
return {'internal': true};
}
} }
return null; return null;
} }

View File

@@ -1,13 +1,13 @@
<h3 *ngIf="title">{{title}}</h3> <h3 *ngIf="title">{{title}}</h3>
<div class="group"> <div class="group">
<span *ngFor="let text of texts; let i=index"> <span *ngFor="let control of form.controls; index as i">
<mat-form-field class="inline" appearance="outline"> <mat-form-field class="inline" appearance="outline">
<mat-label><span>{{label}}</span></mat-label> <mat-label><span>{{label}}</span></mat-label>
<input matInput [value]="text" (change)="onTextChange(i, $event)"> <input matInput [formControl]="control">
</mat-form-field> </mat-form-field>
</span> </span>
<button *ngIf="texts.length > 0" mat-icon-button (click)="addText()"> <button *ngIf="form.controls.length > 0" mat-icon-button (click)="addText('')">
<mat-icon class="tab-icon material-icons-outlined">add</mat-icon> <mat-icon class="tab-icon material-icons-outlined">add</mat-icon>
</button> </button>
<button *ngIf="texts.length == 0" mat-flat-button (click)="addText()">{{addLabel}}</button> <button *ngIf="form.controls.length == 0" mat-flat-button (click)="addText('')">{{addLabel}}</button>
</div> </div>

View File

@@ -1,5 +1,15 @@
import { Component, Input } from '@angular/core'; import {Component, forwardRef, Input} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import {
AbstractControl,
ControlValueAccessor,
FormArray,
FormControl,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
@Component({ @Component({
selector: 'app-multi-text', selector: 'app-multi-text',
@@ -10,10 +20,15 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
multi: true, multi: true,
useExisting: MultiTextComponent useExisting: MultiTextComponent
} },
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MultiTextComponent),
multi: true,
},
] ]
}) })
export class MultiTextComponent implements ControlValueAccessor { export class MultiTextComponent implements ControlValueAccessor, Validator {
@Input() label: string = ""; @Input() label: string = "";
@Input() addLabel: string = ""; @Input() addLabel: string = "";
@@ -21,13 +36,20 @@ export class MultiTextComponent implements ControlValueAccessor {
onChange = (_: string[]) => {}; onChange = (_: string[]) => {};
texts: string[] = []; form = new FormArray<FormControl>([]);
writeValue(value: any) { constructor() {
if (value == null) { this.form.valueChanges.subscribe(value => {
value = []; this.onChange(value);
} });
this.texts = value; }
newText(text: string): FormControl {
return new FormControl(text, [Validators.required]);
}
writeValue(value: string[]) {
value?.forEach(v => this.addText(v));
} }
registerOnChange(onChange: any) { registerOnChange(onChange: any) {
@@ -36,14 +58,15 @@ export class MultiTextComponent implements ControlValueAccessor {
registerOnTouched(_: any) {} registerOnTouched(_: any) {}
addText() { addText(text: string) {
this.texts.push(""); this.form.push(this.newText(text));
this.onChange(this.texts);
} }
onTextChange(i: number, e: Event) { /* Validator implementation */
const target = e.target as HTMLInputElement; validate(control: AbstractControl): ValidationErrors | null {
this.texts[i] = target.value; if (!this.form.valid) {
this.onChange(this.texts); return {'internal': true};
}
return null;
} }
} }

View File

@@ -1,6 +1,6 @@
<mat-form-field appearance="fill"> <mat-form-field appearance="fill">
<mat-label>{{label}}</mat-label> <mat-label>{{label}}</mat-label>
<mat-select data-cy="select-container" [value]="container" (selectionChange)="onSelectChange($event.value)"> <mat-select [formControl]="formCtrl" data-cy="select-container" (selectionChange)="onSelectChange($event.value)">
<mat-option *ngFor="let container of containers" [value]="container">{{container}}</mat-option> <mat-option *ngFor="let container of containers" [value]="container">{{container}}</mat-option>
<mat-option value="!">(New {{label}})</mat-option> <mat-option value="!">(New {{label}})</mat-option>
</mat-select> </mat-select>

View File

@@ -1,5 +1,12 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'; import {Component, EventEmitter, forwardRef, Input, Output} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import {
AbstractControl,
ControlValueAccessor,
FormArray, FormControl,
FormGroup, NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors, Validator, Validators
} from '@angular/forms';
@Component({ @Component({
selector: 'app-select-container', selector: 'app-select-container',
@@ -10,21 +17,30 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
multi: true, multi: true,
useExisting: SelectContainerComponent useExisting: SelectContainerComponent
} },
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => SelectContainerComponent),
multi: true,
},
] ]
}) })
export class SelectContainerComponent implements ControlValueAccessor { export class SelectContainerComponent implements ControlValueAccessor, Validator {
@Input() containers: string[] = []; @Input() containers: string[] = [];
@Input() label: string = ""; @Input() label: string = "";
@Output() createNew = new EventEmitter<boolean>(); @Output() createNew = new EventEmitter<boolean>();
container: string = ""; formCtrl: FormControl;
onChange = (_: string) => {}; onChange = (_: string) => {};
writeValue(value: any) { constructor() {
this.container = value; this.formCtrl = new FormControl('', [Validators.required]);
}
writeValue(value: string) {
this.formCtrl.setValue(value);
} }
registerOnChange(onChange: any) { registerOnChange(onChange: any) {
@@ -39,4 +55,12 @@ export class SelectContainerComponent implements ControlValueAccessor {
} }
this.createNew.emit(v == "!"); this.createNew.emit(v == "!");
} }
/* Validator implementation */
validate(control: AbstractControl): ValidationErrors | null {
if (!this.formCtrl.valid) {
return {'internal': true};
}
return null;
}
} }

View File

@@ -1,25 +1,27 @@
<div class="group"> <div class="group">
<div *ngFor="let vm of volumeMounts; let i=index"> <div *ngFor="let control of form.controls; index as i">
<mat-form-field class="inline" appearance="outline"> <ng-container [formGroup]="control">
<mat-label><span>Volume</span></mat-label> <mat-form-field class="inline" appearance="outline">
<mat-select [attr.data-cy]="'volume-mount-name-'+i" [value]="vm.name" (selectionChange)="onNameChange(i, $event.value)"> <mat-label><span>Volume</span></mat-label>
<mat-option *ngFor="let volume of volumes" [value]="volume">{{volume}}</mat-option> <mat-select formControlName="name" [attr.data-cy]="'volume-mount-name-'+i" (selectionChange)="onNameChange(i, $event.value)">
<mat-option value="!">(New Volume)</mat-option> <mat-option *ngFor="let volume of volumes" [value]="volume">{{volume}}</mat-option>
</mat-select> <mat-option value="!">(New Volume)</mat-option>
</mat-form-field> </mat-select>
<mat-form-field class="inline" appearance="outline"> </mat-form-field>
<mat-label><span>Mount Path</span></mat-label> <mat-form-field class="inline" appearance="outline">
<input (input)="onPathChange(i, $event)" [attr.data-cy]="'volume-mount-path-'+i" matInput [value]="vm.path" (change)="onPathChange(i, $event)"> <mat-label><span>Mount Path</span></mat-label>
</mat-form-field> <input formControlName="path" [attr.data-cy]="'volume-mount-path-'+i" matInput>
</mat-form-field>
<app-volume <app-volume
*ngIf="showNewVolume[i]" *ngIf="showNewVolume[i]"
(created)="onNewVolumeCreated(i, $event)" (created)="onNewVolumeCreated(i, $event)"
></app-volume> ></app-volume>
</ng-container>
</div> </div>
<button data-cy="volume-mount-add" *ngIf="volumeMounts.length > 0" mat-icon-button (click)="add()"> <button data-cy="volume-mount-add" *ngIf="form.controls.length > 0" mat-icon-button (click)="add('', '')">
<mat-icon class="tab-icon material-icons-outlined">add</mat-icon> <mat-icon class="tab-icon material-icons-outlined">add</mat-icon>
</button> </button>
<button data-cy="volume-mount-add" *ngIf="volumeMounts.length == 0" mat-flat-button (click)="add()">Add Volume Mount</button> <button data-cy="volume-mount-add" *ngIf="form.controls.length == 0" mat-flat-button (click)="add('', '')">Add Volume Mount</button>
</div> </div>

View File

@@ -1,6 +1,17 @@
import { Component, EventEmitter, Input, Output, forwardRef } from '@angular/core'; import {Component, EventEmitter, forwardRef, Input, Output} from '@angular/core';
import { AbstractControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms'; import {
import { Volume, VolumeMount } from 'src/app/api-gen'; AbstractControl,
ControlValueAccessor,
FormArray,
FormControl,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {Volume, VolumeMount} from 'src/app/api-gen';
@Component({ @Component({
selector: 'app-volume-mounts', selector: 'app-volume-mounts',
@@ -19,20 +30,27 @@ import { Volume, VolumeMount } from 'src/app/api-gen';
}, },
] ]
}) })
export class VolumeMountsComponent implements Validator { export class VolumeMountsComponent implements ControlValueAccessor, Validator {
@Input() volumes: string[] = []; @Input() volumes: string[] = [];
@Output() createNewVolume = new EventEmitter<Volume>(); @Output() createNewVolume = new EventEmitter<Volume>();
volumeMounts: VolumeMount[] = []; form = new FormArray<FormGroup>([]);
showNewVolume: boolean[] = []; showNewVolume: boolean[] = [];
onChange = (_: VolumeMount[]) => {}; onChange = (_: VolumeMount[]) => {};
onValidatorChange = () => {}; onValidatorChange = () => {};
writeValue(value: any) { constructor() {
this.volumeMounts = value; this.form.valueChanges.subscribe(value => {
this.onChange(value);
});
}
writeValue(value: VolumeMount[]) {
value.forEach(v => this.add(v.name, v.path));
} }
registerOnChange(onChange: any) { registerOnChange(onChange: any) {
@@ -41,29 +59,24 @@ export class VolumeMountsComponent implements Validator {
registerOnTouched(_: any) {} registerOnTouched(_: any) {}
add() { newVolumeMount(vol: VolumeMount): FormGroup {
this.volumeMounts.push({name: "", path: ""}); return new FormGroup({
this.onChange(this.volumeMounts); name: new FormControl(vol.name, [Validators.required]),
path: new FormControl(vol.path, [Validators.required]),
});
} }
onPathChange(i: number, e: Event) { add(name: string, path: string) {
const target = e.target as HTMLInputElement; this.form.push(this.newVolumeMount({name, path}));
this.volumeMounts[i].path = target.value;
this.onChange(this.volumeMounts);
} }
onNameChange(i: number, name: string) { onNameChange(i: number, name: string) {
if (name != "!") {
this.volumeMounts[i].name = name;
this.onChange(this.volumeMounts);
}
this.showNewVolume[i] = name == "!"; this.showNewVolume[i] = name == "!";
} }
onNewVolumeCreated(i: number, v: Volume) { onNewVolumeCreated(i: number, v: Volume) {
this.volumes.push(v.name); this.volumes.push(v.name);
this.volumeMounts[i].name = v.name; this.form.at(i).get('name')?.setValue(v.name);
this.createNewVolume.next(v); this.createNewVolume.next(v);
this.showNewVolume[i] = false; this.showNewVolume[i] = false;
this.onValidatorChange(); this.onValidatorChange();
@@ -71,11 +84,8 @@ export class VolumeMountsComponent implements Validator {
/* Validator implementation */ /* Validator implementation */
validate(control: AbstractControl): ValidationErrors | null { validate(control: AbstractControl): ValidationErrors | null {
for (let i=0; i<this.volumeMounts.length; i++) { if (!this.form.valid) {
const vm = this.volumeMounts[i]; return {'internal': true};
if (vm.name == "" || vm.path == "") {
return {'internal': true};
}
} }
return null; return null;
} }

View File

@@ -10,6 +10,7 @@
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline" class="mid-width"> <mat-form-field appearance="outline" class="mid-width">
<mat-label><span>Size</span></mat-label> <mat-label><span>Size</span></mat-label>
<mat-error>Example of valid quantities: 300k (300*1000), 30Mi(30*1024²), 3Gi (3*1024³), 3G (3*1000³)</mat-error>
<input placeholder="Minimal size of the volume" data-cy="volume-size" matInput formControlName="size"> <input placeholder="Minimal size of the volume" data-cy="volume-size" matInput formControlName="size">
</mat-form-field> </mat-form-field>
<mat-checkbox data-cy="volume-ephemeral" formControlName="ephemeral">Volume is Ephemeral</mat-checkbox> <mat-checkbox data-cy="volume-ephemeral" formControlName="ephemeral">Volume is Ephemeral</mat-checkbox>