mirror of
https://github.com/redhat-developer/odo.git
synced 2025-10-19 03:06:19 +03:00
[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:
@@ -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)
|
||||
|
||||
3
ui/src/app/api-gen/.openapi-generator/FILES
generated
3
ui/src/app/api-gen/.openapi-generator/FILES
generated
@@ -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
18
ui/src/app/api-gen/model/annotation.ts
generated
Normal 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; };
|
||||
}
|
||||
|
||||
9
ui/src/app/api-gen/model/container.ts
generated
9
ui/src/app/api-gen/model/container.ts
generated
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
40
ui/src/app/api-gen/model/endpoint.ts
generated
Normal 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
18
ui/src/app/api-gen/model/env.ts
generated
Normal 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;
|
||||
}
|
||||
|
||||
3
ui/src/app/api-gen/model/models.ts
generated
3
ui/src/app/api-gen/model/models.ts
generated
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
2
ui/src/app/controls/endpoints/endpoints.component.css
Normal file
2
ui/src/app/controls/endpoints/endpoints.component.css
Normal file
@@ -0,0 +1,2 @@
|
||||
.mid-width { width: 50%; }
|
||||
.quart-width { width: 25%; }
|
||||
43
ui/src/app/controls/endpoints/endpoints.component.html
Normal file
43
ui/src/app/controls/endpoints/endpoints.component.html
Normal 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>
|
||||
23
ui/src/app/controls/endpoints/endpoints.component.spec.ts
Normal file
23
ui/src/app/controls/endpoints/endpoints.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
74
ui/src/app/controls/endpoints/endpoints.component.ts
Normal file
74
ui/src/app/controls/endpoints/endpoints.component.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
div.group { margin-bottom: 16px; }
|
||||
.mid-width { width: 50%; }
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
useExisting: MultiTextComponent
|
||||
}
|
||||
]
|
||||
|
||||
})
|
||||
export class MultiTextComponent implements ControlValueAccessor {
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user