Skip to content

Commit

Permalink
[ui] create volumes from container / exec command creation
Browse files Browse the repository at this point in the history
  • Loading branch information
feloy committed Aug 9, 2023
1 parent b02450a commit d25a4aa
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 35 deletions.
40 changes: 34 additions & 6 deletions ui/cypress/e2e/spec.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,30 @@ describe('devfile editor spec', () => {
cy.selectTab(TAB_CONTAINERS);
cy.getByDataCy('container-name').type('created-container');
cy.getByDataCy('container-image').type('an-image');

cy.getByDataCy('volume-mount-add').click();
cy.getByDataCy('volume-mount-path-0').type("/mnt/vol1");
cy.getByDataCy('volume-mount-name-0').click().get('mat-option').contains('volume1').click();

cy.getByDataCy('volume-mount-add').click();
cy.getByDataCy('volume-mount-path').type("/mnt/vol1");
cy.getByDataCy('volume-mount-name').click().get('mat-option').contains('volume1').click();
cy.getByDataCy('volume-mount-path-1').type("/mnt/vol2");
cy.getByDataCy('volume-mount-name-1').click().get('mat-option').contains('(New Volume)').click();
cy.getByDataCy('volume-name').type('volume2');
cy.getByDataCy('volume-create').click();

cy.getByDataCy('container-create').click();

cy.getByDataCy('container-info').first()
.should('contain.text', 'created-container')
.should('contain.text', 'an-image')
.should('contain.text', 'volume1')
.should('contain.text', '/mnt/vol1');
.should('contain.text', '/mnt/vol1')
.should('contain.text', 'volume2')
.should('contain.text', '/mnt/vol2');

cy.selectTab(TAB_VOLUMES);
cy.getByDataCy('volume-info').eq(1)
.should('contain.text', 'volume2');
});

it('displays a created image', () => {
Expand Down Expand Up @@ -142,9 +156,17 @@ describe('devfile editor spec', () => {
cy.getByDataCy('select-container').click().get('mat-option').contains('(New Container)').click();
cy.getByDataCy('container-name').type('a-created-container');
cy.getByDataCy('container-image').type('an-image');

cy.getByDataCy('volume-mount-add').click();
cy.getByDataCy('volume-mount-path-0').type("/mnt/vol1");
cy.getByDataCy('volume-mount-name-0').click().get('mat-option').contains('volume1').click();

cy.getByDataCy('volume-mount-add').click();
cy.getByDataCy('volume-mount-path').type("/mnt/vol1");
cy.getByDataCy('volume-mount-name').click().get('mat-option').contains('volume1').click();
cy.getByDataCy('volume-mount-path-1').type("/mnt/vol2");
cy.getByDataCy('volume-mount-name-1').click().get('mat-option').contains('(New Volume)').click();
cy.getByDataCy('volume-name').type('volume2');
cy.getByDataCy('volume-create').click();

cy.getByDataCy('container-create').click();

cy.getByDataCy('select-container').should('contain', 'a-created-container');
Expand All @@ -161,7 +183,13 @@ describe('devfile editor spec', () => {
.should('contain.text', 'a-created-container')
.should('contain.text', 'an-image')
.should('contain.text', 'volume1')
.should('contain.text', '/mnt/vol1');
.should('contain.text', '/mnt/vol1')
.should('contain.text', 'volume2')
.should('contain.text', '/mnt/vol2');

cy.selectTab(TAB_VOLUMES);
cy.getByDataCy('volume-info').eq(1)
.should('contain.text', 'volume2');
});

it('creates an apply image command with a new image', () => {
Expand Down
14 changes: 11 additions & 3 deletions ui/src/app/controls/volume-mounts/volume-mounts.component.html
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
<h3>Volume Mounts</h3>
<div class="group">
<div *ngFor="let vm of volumeMounts; let i=index">
<mat-form-field appearance="fill">
<mat-select data-cy="volume-mount-name" [value]="vm.name" (selectionChange)="onNameChange(i, $event.value)">
<mat-form-field class="inline" appearance="outline">
<mat-label><span>Volume</span></mat-label>
<mat-select [attr.data-cy]="'volume-mount-name-'+i" [value]="vm.name" (selectionChange)="onNameChange(i, $event.value)">
<mat-option *ngFor="let volume of volumes" [value]="volume">{{volume}}</mat-option>
<mat-option value="!">(New Volume)</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="inline" appearance="outline">
<mat-label><span>Mount Path</span></mat-label>
<input data-cy="volume-mount-path" matInput [value]="vm.path" (change)="onPathChange(i, $event)">
<input [attr.data-cy]="'volume-mount-path-'+i" matInput [value]="vm.path" (change)="onPathChange(i, $event)">
</mat-form-field>

<app-volume
*ngIf="showNewVolume[i]"
(created)="onNewVolumeCreated(i, $event)"
></app-volume>
</div>
<button data-cy="volume-mount-add" *ngIf="volumeMounts.length > 0" mat-icon-button (click)="add()">
<mat-icon class="tab-icon material-icons-outlined">add</mat-icon>
</button>
<button data-cy="volume-mount-add" *ngIf="volumeMounts.length == 0" mat-flat-button (click)="add()">Add Volume Mount</button>
</div>

48 changes: 41 additions & 7 deletions ui/src/app/controls/volume-mounts/volume-mounts.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, Input } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { VolumeMount } from 'src/app/api-gen';
import { Component, EventEmitter, Input, Output, forwardRef } from '@angular/core';
import { AbstractControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms';
import { Volume, VolumeMount } from 'src/app/api-gen';

@Component({
selector: 'app-volume-mounts',
Expand All @@ -11,14 +11,22 @@ import { VolumeMount } from 'src/app/api-gen';
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: VolumeMountsComponent
}
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => VolumeMountsComponent),
multi: true,
},
]
})
export class VolumeMountsComponent {
export class VolumeMountsComponent implements Validator {

@Input() volumes: string[] = [];

@Output() createNewVolume = new EventEmitter<Volume>();

volumeMounts: VolumeMount[] = [];
showNewVolume: boolean[] = [];

onChange = (_: VolumeMount[]) => {};

Expand All @@ -44,7 +52,33 @@ export class VolumeMountsComponent {
}

onNameChange(i: number, name: string) {
this.volumeMounts[i].name = name;
this.onChange(this.volumeMounts);
if (name != "!") {
this.volumeMounts[i].name = name;
this.onChange(this.volumeMounts);
}

this.showNewVolume[i] = name == "!";
}

onNewVolumeCreated(i: number, v: Volume) {
this.volumes.push(v.name);
this.volumeMounts[i].name = v.name;
this.createNewVolume.next(v);
this.showNewVolume[i] = false;
}

/* Validator implementation */
validate(control: AbstractControl): ValidationErrors | null {
for (let i=0; i<this.volumeMounts.length; i++) {
const vm = this.volumeMounts[i];
if (vm.name == "" || vm.path == "") {
return {'internal': true};
}
}
return null;
}

registerOnValidatorChange?(fn: () => void): void {

}
}
31 changes: 26 additions & 5 deletions ui/src/app/forms/command-exec/command-exec.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DevstateService } from 'src/app/services/devstate.service';
import { PATTERN_COMMAND_ID } from '../patterns';
import { Container, Volume } from 'src/app/api-gen';
import { TelemetryService } from 'src/app/services/telemetry.service';
import { ToCreate } from '../container/container.component';

@Component({
selector: 'app-command-exec',
Expand All @@ -18,6 +19,7 @@ export class CommandExecComponent {
containerList: string[] = [];
showNewContainer: boolean = false;
containerToCreate: Container | null = null;
volumesToCreate: Volume[] = [];
volumeNames: string[] | undefined = [];

constructor(
Expand All @@ -43,6 +45,21 @@ export class CommandExecComponent {
});
}

createVolumes(volumes: Volume[], i: number, next: () => any) {
if (volumes.length == i) {
next();
return;
}
const res = this.devstate.addVolume(volumes[i]);
res.subscribe({
next: value => {
this.createVolumes(volumes, i+1, next);
},
error: error => {
alert(error.error.message);
}
});
}

create() {
this.telemetry.track("[ui] create exec command");
Expand All @@ -58,7 +75,8 @@ export class CommandExecComponent {
});
}

if (this.containerToCreate != null &&
this.createVolumes(this.volumesToCreate, 0, () => {
if (this.containerToCreate != null &&
this.containerToCreate?.name == this.form.controls["component"].value) {
const res = this.devstate.addContainer(this.containerToCreate);
res.subscribe({
Expand All @@ -69,9 +87,10 @@ export class CommandExecComponent {
alert(error.error.message);
}
});
} else {
subcreate();
}
} else {
subcreate();
}
});
}

cancel() {
Expand All @@ -86,10 +105,12 @@ export class CommandExecComponent {
this.showNewContainer = v;
}

onNewContainerCreated(container: Container) {
onNewContainerCreated(toCreate: ToCreate) {
const container = toCreate.container;
this.containerList.push(container.name);
this.form.controls["component"].setValue(container.name);
this.showNewContainer = false;
this.containerToCreate = container;
this.volumesToCreate.push(...toCreate.volumes);
}
}
5 changes: 4 additions & 1 deletion ui/src/app/forms/container/container.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ <h2>Add a new container</h2>
</mat-form-field>
<app-multi-text formControlName="command" title="Command" label="Command" addLabel="Add command"></app-multi-text>
<app-multi-text formControlName="args" title="Arguments to command" label="Arg" addLabel="Add arg"></app-multi-text>
<app-volume-mounts [volumes]="volumeNames" formControlName="volumeMounts"></app-volume-mounts>
<app-volume-mounts
[volumes]="volumeNames"
formControlName="volumeMounts"
(createNewVolume)="onCreateNewVolume($event)"></app-volume-mounts>
<mat-form-field appearance="outline" class="mid-width">
<mat-label><span>Memory Request</span></mat-label>
<mat-error>{{quantityErrMsgMemory}}</mat-error>
Expand Down
20 changes: 17 additions & 3 deletions ui/src/app/forms/container/container.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { PATTERN_COMPONENT_ID } from '../patterns';
import { DevstateService } from 'src/app/services/devstate.service';
import { Container } from 'src/app/api-gen';
import { Container, Volume } from 'src/app/api-gen';
import { TelemetryService } from 'src/app/services/telemetry.service';

export interface ToCreate {
container: Container;
volumes: Volume[];
}

@Component({
selector: 'app-container',
templateUrl: './container.component.html',
Expand All @@ -14,13 +19,15 @@ export class ContainerComponent {
@Input() volumeNames: string[] = [];
@Input() cancelable: boolean = false;
@Output() canceled = new EventEmitter<void>();
@Output() created = new EventEmitter<Container>();
@Output() created = new EventEmitter<ToCreate>();

form: FormGroup;

quantityErrMsgMemory = 'Numeric value, with optional unit Ki, Mi, Gi, Ti, Pi, Ei';
quantityErrMsgCPU = 'Numeric value, with optional unit m, k, M, G, T, P, E';

volumesToCreate: Volume[] = [];

constructor(
private devstate: DevstateService,
private telemetry: TelemetryService
Expand All @@ -40,10 +47,17 @@ export class ContainerComponent {

create() {
this.telemetry.track("[ui] create container");
this.created.emit(this.form.value);
this.created.emit({
container: this.form.value,
volumes: this.volumesToCreate,
});
}

cancel() {
this.canceled.emit();
}

onCreateNewVolume(v: Volume) {
this.volumesToCreate.push(v);
}
}
41 changes: 31 additions & 10 deletions ui/src/app/tabs/containers/containers.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { StateService } from 'src/app/services/state.service';
import { DevstateService } from 'src/app/services/devstate.service';
import { Container, Volume } from 'src/app/api-gen';
import { ToCreate } from 'src/app/forms/container/container.component';

@Component({
selector: 'app-containers',
Expand Down Expand Up @@ -56,16 +57,36 @@ export class ContainersComponent implements OnInit {
}
}

onCreated(container: Container) {
const result = this.devstate.addContainer(container);
result.subscribe({
next: value => {
this.state.changeDevfileYaml(value);
},
error: error => {
alert(error.error.message);
}
});
createVolumes(volumes: Volume[], i: number, next: () => any) {
if (volumes.length == i) {
next();
return;
}
const res = this.devstate.addVolume(volumes[i]);
res.subscribe({
next: value => {
this.createVolumes(volumes, i+1, next);
},
error: error => {
alert(error.error.message);
}
});
}

onCreated(toCreate: ToCreate) {
const container = toCreate.container;
this.createVolumes(toCreate.volumes, 0, () => {
const result = this.devstate.addContainer(container);
result.subscribe({
next: value => {
this.state.changeDevfileYaml(value);
},
error: error => {
alert(error.error.message);
}
});
});

}

scrollToBottom() {
Expand Down

0 comments on commit d25a4aa

Please sign in to comment.