[ui] Complete container creation (#7035)

* API returns more info about container

* Display more info about containers

* Update UI static files

* Fix unit tests

* Get/Set sources configuration

* [ui] create container with sources mount configuration

* e2e tests + ui static files

* Set containers's envvars

* Regenerate UI static files

* Add Annotation to POST /container

* [api] Create Container with Annotations

* [ui] Annotations when creating container

* Regenerate UI static files

* [api] Endpoints when adding container

* [ui] Endpoints when adding container

* Regenerate UI static files
This commit is contained in:
Philippe Martin
2023-08-29 09:28:03 +02:00
committed by GitHub
parent a9492307d7
commit e59cfa8852
44 changed files with 1367 additions and 95 deletions

View File

@@ -39,7 +39,7 @@ describe('devfile editor spec', () => {
.should('contain.text', 'with arg');
});
it('displays a created container', () => {
it('displays a created container without source configuration', () => {
cy.init();
cy.selectTab(TAB_VOLUMES);
@@ -51,6 +51,87 @@ describe('devfile editor spec', () => {
cy.selectTab(TAB_CONTAINERS);
cy.getByDataCy('container-name').type('created-container');
cy.getByDataCy('container-image').type('an-image');
cy.getByDataCy('container-env-add').click();
cy.getByDataCy('container-env-name-0').type("VAR1");
cy.getByDataCy('container-env-value-0').type("val1");
cy.getByDataCy('container-env-plus').click();
cy.getByDataCy('container-env-name-1').type("VAR2");
cy.getByDataCy('container-env-value-1').type("val2");
cy.getByDataCy('container-env-plus').click();
cy.getByDataCy('container-env-name-2').type("VAR3");
cy.getByDataCy('container-env-value-2').type("val3");
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('endpoints-add').click();
cy.getByDataCy('endpoint-name-0').type("ep1");
cy.getByDataCy('endpoint-targetPort-0').type("4001");
cy.getByDataCy('volume-mount-add').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-more-params').click();
cy.getByDataCy('container-deploy-anno-add').click();
cy.getByDataCy('container-deploy-anno-name-0').type("DEPANNO1");
cy.getByDataCy('container-deploy-anno-value-0').type("depval1");
cy.getByDataCy('container-deploy-anno-plus').click();
cy.getByDataCy('container-deploy-anno-name-1').type("DEPANNO2");
cy.getByDataCy('container-deploy-anno-value-1').type("depval2");
cy.getByDataCy('container-svc-anno-add').click();
cy.getByDataCy('container-svc-anno-name-0').type("SVCANNO1");
cy.getByDataCy('container-svc-anno-value-0').type("svcval1");
cy.getByDataCy('container-svc-anno-plus').click();
cy.getByDataCy('container-svc-anno-name-1').type("SVCANNO2");
cy.getByDataCy('container-svc-anno-value-1').type("svcval2");
cy.getByDataCy('container-create').click();
cy.getByDataCy('container-info').first()
.should('contain.text', 'created-container')
.should('contain.text', 'an-image')
.should('contain.text', 'VAR1: val1')
.should('contain.text', 'VAR2: val2')
.should('contain.text', 'VAR3: val3')
.should('contain.text', 'volume1')
.should('contain.text', '/mnt/vol1')
.should('contain.text', 'volume2')
.should('contain.text', '/mnt/vol2')
.should('not.contain.text', 'Mount Sources')
.should('contain.text', 'ep1')
.should('contain.text', '4001')
.should('contain.text', 'Deployment Annotations')
.should('contain.text', 'DEPANNO1: depval1')
.should('contain.text', 'DEPANNO2: depval2')
.should('contain.text', 'Service Annotations')
.should('contain.text', 'SVCANNO1: svcval1')
.should('contain.text', 'SVCANNO2: svcval2');
cy.selectTab(TAB_VOLUMES);
cy.getByDataCy('volume-info').eq(1)
.should('contain.text', 'volume2');
});
it('displays a created container with source configuration', () => {
cy.init();
cy.selectTab(TAB_VOLUMES);
cy.getByDataCy('volume-name').type('volume1');
cy.getByDataCy('volume-size').type('512Mi');
cy.getByDataCy('volume-ephemeral').click();
cy.getByDataCy('volume-create').click();
cy.selectTab(TAB_CONTAINERS);
cy.getByDataCy('container-name').type('created-container');
cy.getByDataCy('container-image').type('an-image');
cy.getByDataCy('container-more-params').click();
cy.getByDataCy('container-sources-configuration').click();
cy.getByDataCy('container-sources-specific-directory').click();
cy.getByDataCy('container-source-mapping').type('/mnt/sources');
cy.getByDataCy('volume-mount-add').click();
cy.getByDataCy('volume-mount-path-0').type("/mnt/vol1");
@@ -70,7 +151,9 @@ describe('devfile editor spec', () => {
.should('contain.text', 'volume1')
.should('contain.text', '/mnt/vol1')
.should('contain.text', 'volume2')
.should('contain.text', '/mnt/vol2');
.should('contain.text', '/mnt/vol2')
.should('contain.text', 'Mount Sources')
.should('contain.text', '/mnt/sources');
cy.selectTab(TAB_VOLUMES);
cy.getByDataCy('volume-info').eq(1)

View File

@@ -8,6 +8,7 @@ configuration.ts
encoder.ts
git_push.sh
index.ts
model/annotation.ts
model/applyCommand.ts
model/command.ts
model/componentCommandPostRequest.ts
@@ -30,6 +31,8 @@ model/devstateImagePostRequest.ts
model/devstateQuantityValidPostRequest.ts
model/devstateResourcePostRequest.ts
model/devstateVolumePostRequest.ts
model/endpoint.ts
model/env.ts
model/events.ts
model/execCommand.ts
model/generalError.ts

18
ui/src/app/api-gen/model/annotation.ts generated Normal file
View File

@@ -0,0 +1,18 @@
/**
* odo dev
* API interface for \'odo dev\'
*
* The version of the OpenAPI document: 0.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface Annotation {
deployment: { [key: string]: string; };
service: { [key: string]: string; };
}

View File

@@ -9,7 +9,10 @@
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { Endpoint } from './endpoint';
import { VolumeMount } from './volumeMount';
import { Env } from './env';
import { Annotation } from './annotation';
export interface Container {
@@ -22,5 +25,11 @@ export interface Container {
cpuRequest: string;
cpuLimit: string;
volumeMounts: Array<VolumeMount>;
annotation: Annotation;
endpoints: Array<Endpoint>;
env: Array<Env>;
configureSources: boolean;
mountSources: boolean;
sourceMapping: string;
}

View File

@@ -9,18 +9,21 @@
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { Endpoint } from './endpoint';
import { VolumeMount } from './volumeMount';
import { Env } from './env';
import { Annotation } from './annotation';
export interface DevstateContainerPostRequest {
/**
* Name of the container
*/
name?: string;
name: string;
/**
* Container image
*/
image?: string;
image: string;
/**
* Entrypoint of the container
*/
@@ -29,6 +32,10 @@ export interface DevstateContainerPostRequest {
* Args passed to the Container entrypoint
*/
args?: Array<string>;
/**
* Environment variables to define
*/
env?: Array<Env>;
/**
* Requested memory for the deployed container
*/
@@ -49,5 +56,22 @@ export interface DevstateContainerPostRequest {
* Volume to mount into the container filesystem
*/
volumeMounts?: Array<VolumeMount>;
/**
* If false, mountSources and sourceMapping values are not considered
*/
configureSources?: boolean;
/**
* If true, sources are mounted into container\'s filesystem
*/
mountSources?: boolean;
/**
* Specific directory on which to mount sources
*/
sourceMapping?: string;
annotation?: Annotation;
/**
* Endpoints exposed by the container
*/
endpoints?: Array<Endpoint>;
}

40
ui/src/app/api-gen/model/endpoint.ts generated Normal file
View File

@@ -0,0 +1,40 @@
/**
* odo dev
* API interface for \'odo dev\'
*
* The version of the OpenAPI document: 0.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface Endpoint {
name: string;
exposure?: Endpoint.ExposureEnum;
path?: string;
protocol?: Endpoint.ProtocolEnum;
secure?: boolean;
targetPort: number;
}
export namespace Endpoint {
export type ExposureEnum = 'public' | 'internal' | 'none';
export const ExposureEnum = {
Public: 'public' as ExposureEnum,
Internal: 'internal' as ExposureEnum,
None: 'none' as ExposureEnum
};
export type ProtocolEnum = 'http' | 'https' | 'ws' | 'wss' | 'tcp' | 'udp';
export const ProtocolEnum = {
Http: 'http' as ProtocolEnum,
Https: 'https' as ProtocolEnum,
Ws: 'ws' as ProtocolEnum,
Wss: 'wss' as ProtocolEnum,
Tcp: 'tcp' as ProtocolEnum,
Udp: 'udp' as ProtocolEnum
};
}

18
ui/src/app/api-gen/model/env.ts generated Normal file
View File

@@ -0,0 +1,18 @@
/**
* odo dev
* API interface for \'odo dev\'
*
* The version of the OpenAPI document: 0.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface Env {
name: string;
value: string;
}

View File

@@ -1,3 +1,4 @@
export * from './annotation';
export * from './applyCommand';
export * from './command';
export * from './componentCommandPostRequest';
@@ -20,6 +21,8 @@ export * from './devstateImagePostRequest';
export * from './devstateQuantityValidPostRequest';
export * from './devstateResourcePostRequest';
export * from './devstateVolumePostRequest';
export * from './endpoint';
export * from './env';
export * from './events';
export * from './execCommand';
export * from './generalError';

View File

@@ -48,6 +48,8 @@ import { ConfirmComponent } from './components/confirm/confirm.component';
import { VolumesComponent } from './tabs/volumes/volumes.component';
import { VolumeComponent } from './forms/volume/volume.component';
import { VolumeMountsComponent } from './controls/volume-mounts/volume-mounts.component';
import { MultiKeyValueComponent } from './controls/multi-key-value/multi-key-value.component';
import { EndpointsComponent } from './controls/endpoints/endpoints.component';
@NgModule({
declarations: [
@@ -74,6 +76,8 @@ import { VolumeMountsComponent } from './controls/volume-mounts/volume-mounts.co
VolumesComponent,
VolumeComponent,
VolumeMountsComponent,
MultiKeyValueComponent,
EndpointsComponent,
],
imports: [
BrowserModule,

View File

@@ -0,0 +1,2 @@
.mid-width { width: 50%; }
.quart-width { width: 25%; }

View File

@@ -0,0 +1,43 @@
<div *ngFor="let control of form.controls; index as i">
<ng-container [formGroup]="control">
<mat-form-field class="mid-width" appearance="outline">
<mat-label><span>Name</span></mat-label>
<input [attr.data-cy]="'endpoint-name-'+i" matInput formControlName="name">
</mat-form-field>
<mat-form-field class="quart-width" appearance="outline">
<mat-label><span>Target Port</span></mat-label>
<input [attr.data-cy]="'endpoint-targetPort-'+i" type="number" matInput formControlName="targetPort">
</mat-form-field>
<mat-form-field class="quart-width" appearance="outline">
<mat-label>Exposure</mat-label>
<mat-select [attr.data-cy]="'endpoint-exposure-'+i" formControlName="exposure">
<mat-option value="">(default, public)</mat-option>
<mat-option value="public">public</mat-option>
<mat-option value="internal">internal</mat-option>
<mat-option value="none">none</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mid-width" appearance="outline">
<mat-label><span>Path</span></mat-label>
<input [attr.data-cy]="'endpoint-path-'+i" matInput formControlName="path">
</mat-form-field>
<mat-form-field class="quart-width" appearance="outline">
<mat-label>Protocol</mat-label>
<mat-select [attr.data-cy]="'endpoint-protocol-'+i" formControlName="protocol">
<mat-option value="">(default, http)</mat-option>
<mat-option value="http">http</mat-option>
<mat-option value="https">https</mat-option>
<mat-option value="ws">ws</mat-option>
<mat-option value="wss">wss</mat-option>
<mat-option value="tcp">tcp</mat-option>
<mat-option value="udp">udp</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox [attr.data-cy]="'endpoint-secure-'+i" formControlName="secure">Protocol Is Secure</mat-checkbox>
</ng-container>
</div>
<button data-cy="endpoints-plus" *ngIf="form.value.length > 0" mat-icon-button (click)="addEndpoint()">
<mat-icon class="tab-icon material-icons-outlined">add</mat-icon>
</button>
<button data-cy="endpoints-add" *ngIf="form.value.length == 0" mat-flat-button (click)="addEndpoint()">Add an Endpoint</button>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EndpointsComponent } from './endpoints.component';
describe('EndpointsComponent', () => {
let component: EndpointsComponent;
let fixture: ComponentFixture<EndpointsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ EndpointsComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(EndpointsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,74 @@
import { Component, forwardRef } from '@angular/core';
import { AbstractControl, ControlValueAccessor, FormArray, FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator, Validators } from '@angular/forms';
interface Endpoint {
}
@Component({
selector: 'app-endpoints',
templateUrl: './endpoints.component.html',
styleUrls: ['./endpoints.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: EndpointsComponent
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => EndpointsComponent),
multi: true,
},
]
})
export class EndpointsComponent implements ControlValueAccessor, Validator {
onChange = (_: Endpoint[]) => {};
onValidatorChange = () => {};
form = new FormArray<FormGroup>([]);
constructor() {
this.form.valueChanges.subscribe(value => {
this.onChange(value);
});
}
newEndpoint(): FormGroup {
return new FormGroup({
name: new FormControl("", [Validators.required]),
targetPort: new FormControl("", [Validators.required, Validators.pattern("^[0-9]*$")]),
exposure: new FormControl(""),
path: new FormControl(""),
protocol: new FormControl(""),
secure: new FormControl(false),
});
}
addEndpoint() {
this.form.push(this.newEndpoint());
}
/* ControlValueAccessor implementation */
writeValue(value: Endpoint[]) {
}
registerOnChange(onChange: any) {
this.onChange = onChange;
}
registerOnTouched(_: any) {}
/* Validator implementation */
validate(control: AbstractControl): ValidationErrors | null {
if (!this.form.valid) {
return {'internal': true};
}
return null;
}
registerOnValidatorChange?(onValidatorChange: () => void): void {
this.onValidatorChange = onValidatorChange;
}
}

View File

@@ -0,0 +1,2 @@
div.group { margin-bottom: 16px; }
.mid-width { width: 50%; }

View File

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

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MultiKeyValueComponent } from './multi-key-value.component';
describe('MultiKeyValueComponent', () => {
let component: MultiKeyValueComponent;
let fixture: ComponentFixture<MultiKeyValueComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MultiKeyValueComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(MultiKeyValueComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,77 @@
import { Component, Input, forwardRef } from '@angular/core';
import { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator, Validators } from '@angular/forms';
interface KeyValue {
name: string;
value: string;
}
@Component({
selector: 'app-multi-key-value',
templateUrl: './multi-key-value.component.html',
styleUrls: ['./multi-key-value.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: MultiKeyValueComponent
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MultiKeyValueComponent),
multi: true,
},
]
})
export class MultiKeyValueComponent implements ControlValueAccessor, Validator {
@Input() dataCyPrefix: string = "";
@Input() addLabel: string = "";
onChange = (_: KeyValue[]) => {};
onValidatorChange = () => {};
entries: KeyValue[] = [];
writeValue(value: KeyValue[]) {
this.entries = value;
}
registerOnChange(onChange: any) {
this.onChange = onChange;
}
registerOnTouched(_: any) {}
addEntry() {
this.entries.push({name: "", value: ""});
this.onChange(this.entries);
}
onKeyChange(i: number, e: Event) {
const target = e.target as HTMLInputElement;
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 */
validate(control: AbstractControl): ValidationErrors | null {
for (let i=0; i<this.entries.length; i++) {
const entry = this.entries[i];
if (entry.name == "" || entry.value == "") {
return {'internal': true};
}
}
return null;
}
registerOnValidatorChange?(onValidatorChange: () => void): void {
this.onValidatorChange = onValidatorChange;
}
}

View File

@@ -12,7 +12,6 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
useExisting: MultiTextComponent
}
]
})
export class MultiTextComponent implements ControlValueAccessor {

View File

@@ -1,4 +1,3 @@
<h3>Volume Mounts</h3>
<div class="group">
<div *ngFor="let vm of volumeMounts; let i=index">
<mat-form-field class="inline" appearance="outline">

View File

@@ -16,7 +16,7 @@ import { Volume, VolumeMount } from 'src/app/api-gen';
provide: NG_VALIDATORS,
useExisting: forwardRef(() => VolumeMountsComponent),
multi: true,
},
},
]
})
export class VolumeMountsComponent implements Validator {

View File

@@ -1,3 +1,12 @@
.main { padding: 16px; }
mat-form-field.full-width { width: 100%; }
mat-form-field.mid-width { width: 50%; }
.mid-width { width: 50%; }
.source-configuration-details {
margin-left: 16px;
}
div.buttonbar {
margin-top: 16px;
}
.outbutton {
text-align: right;
}

View File

@@ -12,36 +12,81 @@
<input placeholder="Image to start the container" data-cy="container-image" matInput formControlName="image">
</mat-form-field>
<h3>Command and Arguments</h3>
<div class="description">Command and Arguments can be used to override the entrypoint of the image</div>
<app-multi-text formControlName="command" label="Command" addLabel="Add command"></app-multi-text>
<app-multi-text formControlName="args" label="Arg" addLabel="Add arg"></app-multi-text>
<h3>Environment Variables</h3>
<div class="description">Environment Variables to define in the running container</div>
<app-multi-key-value dataCyPrefix="container-env" addLabel="Add Environment Variable" formControlName="env"></app-multi-key-value>
<h3>Volume Mounts</h3>
<div class="description">Volumes to mount into the container's filesystem</div>
<app-volume-mounts
[volumes]="volumeNames"
formControlName="volumeMounts"
(createNewVolume)="onCreateNewVolume($event)"></app-volume-mounts>
<h3>Resource Usage</h3>
<mat-form-field appearance="outline" class="mid-width">
<mat-label><span>Memory Request</span></mat-label>
<mat-error>{{quantityErrMsgMemory}}</mat-error>
<input placeholder="memory requested for the container. Ex: 1Gi" data-cy="container-memory-request" matInput formControlName="memoryRequest">
</mat-form-field>
<mat-form-field appearance="outline" class="mid-width">
<mat-label><span>Memory Limit</span></mat-label>
<mat-error>{{quantityErrMsgMemory}}</mat-error>
<input placeholder="memory limit for the container. Ex: 1Gi" data-cy="container-memory-limit" matInput formControlName="memoryLimit">
</mat-form-field>
<mat-form-field appearance="outline" class="mid-width">
<mat-label><span>CPU Request</span></mat-label>
<mat-error>{{quantityErrMsgCPU}}</mat-error>
<input placeholder="CPU requested for the container. Ex: 500m" data-cy="container-cpu-request" matInput formControlName="cpuRequest">
</mat-form-field>
<mat-form-field appearance="outline" class="mid-width">
<mat-label><span>CPU Limit</span></mat-label>
<mat-error>{{quantityErrMsgCPU}}</mat-error>
<input placeholder="CPU limit for the container. Ex: 1" data-cy="container-cpu-limit" matInput formControlName="cpuLimit">
</mat-form-field>
<h3>Endpoints</h3>
<div class="description">Endpoints exposed by the container</div>
<app-endpoints formControlName="endpoints"></app-endpoints>
<div class="outbutton"><button data-cy="container-more-params" *ngIf="!seeMore" mat-flat-button (click)="more()">More parameters...</button></div>
<div *ngIf="seeMore">
<h3>Resource Usage</h3>
<div class="description">CPU and Memory resources necessary for container's execution</div>
<mat-form-field appearance="outline" class="mid-width">
<mat-label><span>Memory Request</span></mat-label>
<mat-error>{{quantityErrMsgMemory}}</mat-error>
<input placeholder="memory requested for the container. Ex: 1Gi" data-cy="container-memory-request" matInput formControlName="memoryRequest">
</mat-form-field>
<mat-form-field appearance="outline" class="mid-width">
<mat-label><span>Memory Limit</span></mat-label>
<mat-error>{{quantityErrMsgMemory}}</mat-error>
<input placeholder="memory limit for the container. Ex: 1Gi" data-cy="container-memory-limit" matInput formControlName="memoryLimit">
</mat-form-field>
<mat-form-field appearance="outline" class="mid-width">
<mat-label><span>CPU Request</span></mat-label>
<mat-error>{{quantityErrMsgCPU}}</mat-error>
<input placeholder="CPU requested for the container. Ex: 500m" data-cy="container-cpu-request" matInput formControlName="cpuRequest">
</mat-form-field>
<mat-form-field appearance="outline" class="mid-width">
<mat-label><span>CPU Limit</span></mat-label>
<mat-error>{{quantityErrMsgCPU}}</mat-error>
<input placeholder="CPU limit for the container. Ex: 1" data-cy="container-cpu-limit" matInput formControlName="cpuLimit">
</mat-form-field>
<h3>Sources</h3>
<div class="description">Declare if and how sources are mounted into the container's filesystem. By default, sources are automatically mounted into $PROJECTS_ROOT or /projects directory</div>
<div><mat-checkbox data-cy="container-sources-configuration" formControlName="configureSources">Configure Source mount</mat-checkbox></div>
<div *ngIf="form.get('configureSources')?.value" class="source-configuration-details">
<div style="display: inline-flex" class="mid-width">
<mat-checkbox data-cy="container-mount-sources" formControlName="mountSources">Mount sources into container</mat-checkbox>
<mat-checkbox data-cy="container-sources-specific-directory" matTooltip="${PROJECTS_ROOT} or /projects by default" formControlName="_specificDir">Into specific directory</mat-checkbox>
</div>
<mat-form-field appearance="outline" class="mid-width">
<mat-label><span>Mount sources into</span></mat-label>
<input placeholder="Container's directory on which to mount sources" data-cy="container-source-mapping" matInput formControlName="sourceMapping">
</mat-form-field>
</div>
<h3>Deployment Annotations</h3>
<div class="description">Annotations added to the Kubernetes Deployment created for running this container</div>
<app-multi-key-value dataCyPrefix="container-deploy-anno" addLabel="Add Annotation" formControlName="deployAnnotations"></app-multi-key-value>
<h3>Service Annotations</h3>
<div class="description">Annotations added to the Kubernetes Service created for accessing this container</div>
<app-multi-key-value dataCyPrefix="container-svc-anno" addLabel="Add Annotation" formControlName="svcAnnotations"></app-multi-key-value>
</div>
<div class="outbutton"><button data-cy="container-less-params" *ngIf="seeMore" mat-flat-button (click)="less()">Less parameters...</button></div>
</form>
<button data-cy="container-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new container" (click)="create()">Create</button>
<button *ngIf="cancelable" mat-flat-button (click)="cancel()">Cancel</button>
<div class="buttonbar">
<button data-cy="container-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new container" (click)="create()">Create</button>
<button *ngIf="cancelable" mat-flat-button (click)="cancel()">Cancel</button>
</div>
</div>

View File

@@ -27,6 +27,7 @@ export class ContainerComponent {
quantityErrMsgCPU = 'Numeric value, with optional unit m, k, M, G, T, P, E';
volumesToCreate: Volume[] = [];
seeMore: boolean = false;
constructor(
private devstate: DevstateService,
@@ -37,16 +38,59 @@ export class ContainerComponent {
image: new FormControl("", [Validators.required]),
command: new FormControl([]),
args: new FormControl([]),
env: new FormControl([]),
volumeMounts: new FormControl([]),
memoryRequest: new FormControl("", null, [this.devstate.isQuantity()]),
memoryLimit: new FormControl("", null, [this.devstate.isQuantity()]),
cpuRequest: new FormControl("", null, [this.devstate.isQuantity()]),
cpuLimit: new FormControl("", null, [this.devstate.isQuantity()]),
volumeMounts: new FormControl([]),
})
configureSources: new FormControl(false),
mountSources: new FormControl(true),
_specificDir: new FormControl(false),
sourceMapping: new FormControl(""),
deployAnnotations: new FormControl([]),
svcAnnotations: new FormControl([]),
endpoints: new FormControl([]),
});
this.form.valueChanges.subscribe((value: any) => {
this.updateSourceFields(value);
});
this.updateSourceFields(this.form.value);
}
updateSourceFields(value: any) {
const sourceMappingEnabled = value.mountSources && value._specificDir;
if (!sourceMappingEnabled && !this.form.get('sourceMapping')?.disabled) {
this.form.get('sourceMapping')?.disable();
this.form.get('sourceMapping')?.setValue('');
this.form.get('_specificDir')?.setValue(false);
}
if (sourceMappingEnabled && !this.form.get('sourceMapping')?.enabled ) {
this.form.get('sourceMapping')?.enable();
}
const specificDirEnabled = value.mountSources;
if (!specificDirEnabled && !this.form.get('_specificDir')?.disabled) {
this.form.get('_specificDir')?.disable();
}
if (specificDirEnabled && !this.form.get('_specificDir')?.enabled ) {
this.form.get('_specificDir')?.enable();
}
}
create() {
this.telemetry.track("[ui] create container");
const toObject = (o: {name: string, value: string}[]) => {
return o.reduce((acc: any, val: {name: string, value: string}) => { acc[val.name] = val.value; return acc; }, {});
};
const container = this.form.value;
container.annotation = {
deployment: toObject(container.deployAnnotations),
service: toObject(container.svcAnnotations),
};
this.created.emit({
container: this.form.value,
volumes: this.volumesToCreate,
@@ -60,4 +104,11 @@ export class ContainerComponent {
onCreateNewVolume(v: Volume) {
this.volumesToCreate.push(v);
}
more() {
this.seeMore = true;
}
less() {
this.seeMore = false;
}
}

View File

@@ -19,11 +19,20 @@ export class DevstateService {
image: container.image,
command: container.command,
args: container.args,
env: container.env,
memReq: container.memoryRequest,
memLimit: container.memoryLimit,
cpuReq: container.cpuRequest,
cpuLimit: container.cpuLimit,
volumeMounts: container.volumeMounts,
configureSources: container.configureSources,
mountSources: container.mountSources,
sourceMapping: container.sourceMapping,
annotation: {
deployment: container.annotation.deployment,
service: container.annotation.service
},
endpoints: container.endpoints,
});
}

View File

@@ -14,3 +14,26 @@ mat-card-content { padding: 16px; }
table.aligned > tr > td {
vertical-align: top;
}
div.endpoint-list {
display: float;
}
mat-card.endpoint {
width: fit-content;
float: left;
margin: 0 8px;
}
mat-card.endpoint mat-card-header {
padding: 8px 8px 0 8px;
}
mat-card.endpoint mat-card-title {
font-size: 16px;
line-height: 24px;
}
mat-card.endpoint mat-card-subtitle {
font-size: 12px;
line-height: 24px;
}
mat-card.endpoint mat-card-content {
padding: 8px;
}

View File

@@ -18,6 +18,14 @@
<td>Args:</td>
<td><code>{{container.args.join(" ")}}</code></td>
</tr>
<tr *ngIf="container.env.length">
<td>Environment variables:</td>
<td>
<div *ngFor="let env of container.env">
{{env.name}}: {{env.value}}
</div>
</td>
</tr>
<tr *ngIf="container.volumeMounts.length > 0">
<td>Volume Mounts:</td>
<td>
@@ -44,6 +52,47 @@
<td>CPU Limit:</td>
<td><code>{{container.cpuLimit}}</code></td>
</tr>
<tr *ngIf="container.annotation.deployment">
<td>Deployment Annotations:</td>
<td>
<div *ngFor="let anno of container.annotation.deployment | keyvalue">
{{anno.key}}: {{anno.value}}
</div>
</td>
</tr>
<tr *ngIf="container.annotation.service">
<td>Service Annotations:</td>
<td>
<div *ngFor="let anno of container.annotation.service | keyvalue">
{{anno.key}}: {{anno.value}}
</div>
</td>
</tr>
<tr *ngIf="container.configureSources">
<td>Mount Sources:</td>
<td><code>{{container.mountSources ? "Yes" : "No"}}</code></td>
</tr>
<tr *ngIf="container.configureSources && container.mountSources && container.sourceMapping">
<td>Mount Sources Into:</td>
<td><code>{{container.sourceMapping}}</code></td>
</tr>
<tr *ngIf="container.endpoints.length">
<td>Endpoints:</td>
<td class="container-list">
<mat-card class="endpoint" *ngFor="let ep of container.endpoints">
<mat-card-header>
<mat-card-title>{{ep.name}}</mat-card-title>
<mat-card-subtitle>{{ep.targetPort}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div>exposure: {{ep.exposure ?? 'public'}}</div>
<div>protocol: {{ep.protocol ?? 'http'}}</div>
<div *ngIf="ep.secure">secure</div>
<div *ngIf="ep.path">path: {{ep.path}}</div>
</mat-card-content>
</mat-card>
</td>
</tr>
</table>
</mat-card-content>

View File

@@ -25,6 +25,14 @@ h2:has(+.description) {
margin-bottom: 0;
}
h3 {
color: #3f51b5;
}
h3:has(+.description) {
margin-bottom: 0;
}
.description {
font-style: italic;
font-size: smaller;