[ui] Update commands (#7073)

* [api] patch exec command

* Common changes for all command types

* [ui] edit exec command

* [api] patch Apply Command

* [ui] edit apply command

* [ui] Update image command

* [api] update composite command

* [ui] Update composite command

* [uui] Make select-container component not valid when (new ...) is selected

* [ui] e2e tests

* static ui files
This commit is contained in:
Philippe Martin
2023-09-07 10:58:16 +02:00
committed by GitHub
parent 00d39889b7
commit 56b868d16c
34 changed files with 1469 additions and 54 deletions

View File

@@ -68,7 +68,7 @@ describe('devfile editor spec', () => {
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", {force: true});
cy.getByDataCy('volume-mount-name-1').click().get('mat-option').contains('(New Volume)').click();
@@ -113,7 +113,7 @@ describe('devfile editor spec', () => {
cy.selectTab(TAB_VOLUMES);
cy.getByDataCy('volume-info').eq(1)
.should('contain.text', 'volume2');
.should('contain.text', 'volume2');
});
it('displays a created container with source configuration', () => {
@@ -157,7 +157,7 @@ describe('devfile editor spec', () => {
cy.selectTab(TAB_VOLUMES);
cy.getByDataCy('volume-info').eq(1)
.should('contain.text', 'volume2');
.should('contain.text', 'volume2');
});
it('displays a created image', () => {
@@ -205,7 +205,7 @@ describe('devfile editor spec', () => {
cy.getByDataCy('image-build-context').type('/new/path/to/build/context');
cy.getByDataCy('image-dockerfile-uri').type('/new/path/to/dockerfile');
cy.getByDataCy('image-save').click();
cy.getByDataCy('image-info').first()
.should('contain.text', 'created-image')
.should('contain.text', 'another-image-name')
@@ -215,7 +215,7 @@ describe('devfile editor spec', () => {
cy.getByDataCy('image-build-startup').first()
.should('contain.text', 'Yes, forced');
});
});
it('displays a created image with forced build', () => {
cy.init();
@@ -262,7 +262,7 @@ describe('devfile editor spec', () => {
cy.getByDataCy('resource-deploy-startup').first()
.should('contain.text', 'Yes, the resource is not referenced by any command');
});
});
it('displays a created resource, with uri (default)', () => {
cy.init();
@@ -332,8 +332,8 @@ describe('devfile editor spec', () => {
cy.getByDataCy('resource-save').click();
cy.getByDataCy('resource-info').first()
.should('contain.text', 'created-resource')
.should('contain.text', 'another-resource-manifest');
.should('contain.text', 'created-resource')
.should('contain.text', 'another-resource-manifest');
});
it('displays a created volume', () => {
@@ -395,7 +395,7 @@ 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", {force: true});
cy.getByDataCy('volume-mount-name-0').click().get('mat-option').contains('volume1').click();
@@ -405,7 +405,7 @@ describe('devfile editor spec', () => {
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');
@@ -431,6 +431,36 @@ describe('devfile editor spec', () => {
.should('contain.text', 'volume2');
});
it('updates an exec command', () => {
cy.init();
cy.selectTab(TAB_COMMANDS);
cy.getByDataCy('add').click();
cy.getByDataCy('new-command-exec').click();
cy.getByDataCy('command-exec-name').type('created-command');
cy.getByDataCy('command-exec-command-line').type('a-cmdline');
cy.getByDataCy('command-exec-working-dir').type('/path/to/working/dir');
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('container-create').click();
cy.getByDataCy('command-exec-create').click();
cy.getByDataCy('command-edit').click();
cy.getByDataCy('command-exec-command-line').type('{selectAll}{del}another-cmdline');
cy.getByDataCy('command-exec-working-dir').type('{selectAll}{del}/another/path/to/working/dir');
cy.getByDataCy('command-exec-save').click();
cy.getByDataCy('command-info').first()
.should('contain.text', 'created-command')
.should('contain.text', 'another-cmdline')
.should('contain.text', '/another/path/to/working/dir');
});
it('creates an apply image command with a new image', () => {
cy.init();
@@ -463,6 +493,44 @@ describe('devfile editor spec', () => {
.should('contain.text', 'No, the image is referenced by a command');
});
it('upates an apply image command', () => {
cy.init();
cy.selectTab(TAB_COMMANDS);
cy.getByDataCy('add').click();
cy.getByDataCy('new-command-image').click();
cy.getByDataCy('command-image-name').type('created-command');
cy.getByDataCy('select-container').click().get('mat-option').contains('(New Image)').click();
cy.getByDataCy('image-name').type('a-created-image');
cy.getByDataCy('image-image-name').type('an-image-name');
cy.getByDataCy('image-build-context').type('/context/dir');
cy.getByDataCy('image-dockerfile-uri').type('/path/to/Dockerfile');
cy.getByDataCy('image-create').click();
cy.getByDataCy('command-image-create').click();
cy.getByDataCy('command-edit').click();
cy.getByDataCy('select-container').click().get('mat-option').contains('(New Image)').click();
cy.getByDataCy('image-name').type('another-created-image');
cy.getByDataCy('image-image-name').type('another-image-name');
cy.getByDataCy('image-build-context').type('/another/context/dir');
cy.getByDataCy('image-dockerfile-uri').type('/another/path/to/Dockerfile');
cy.getByDataCy('image-create').click();
cy.getByDataCy('command-image-save').click();
cy.getByDataCy('command-info').first()
.should('contain.text', 'created-command')
.should('contain.text', 'another-created-image');
cy.selectTab(TAB_IMAGES);
cy.getByDataCy('image-info').eq(1)
.should('contain.text', 'another-created-image')
.should('contain.text', 'another-image-name')
.should('contain.text', '/another/context/dir')
.should('contain.text', '/another/path/to/Dockerfile');
});
it('creates an apply resource command with a new resource using manifest', () => {
cy.init();
@@ -518,6 +586,44 @@ describe('devfile editor spec', () => {
.should('contain.text', '/my/manifest.yaml');
});
it('updates an apply resource command', () => {
cy.init();
cy.selectTab(TAB_COMMANDS);
cy.getByDataCy('add').click();
cy.getByDataCy('new-command-apply').click();
cy.getByDataCy('command-apply-name').type('created-command');
cy.getByDataCy('select-container').click().get('mat-option').contains('(New Resource)').click();
cy.getByDataCy('resource-name').type('a-created-resource');
cy.getByDataCy('resource-toggle-inlined').click();
cy.getByDataCy('resource-manifest').type('spec: {}');
cy.getByDataCy('resource-create').click();
cy.getByDataCy('command-apply-create').click();
cy.getByDataCy('command-edit').click();
cy.getByDataCy('select-container').click().get('mat-option').contains('(New Resource)').click();
cy.getByDataCy('resource-name').type('another-created-resource');
cy.getByDataCy('resource-toggle-inlined').click();
cy.getByDataCy('resource-manifest').type('spec: {{} field: value }');
cy.getByDataCy('resource-create').click();
cy.getByDataCy('command-apply-save').click();
cy.getByDataCy('command-info').first()
.should('contain.text', 'created-command')
.should('contain.text', 'another-created-resource');
cy.selectTab(TAB_RESOURCES);
cy.getByDataCy('resource-info').eq(1)
.should('contain.text', 'another-created-resource')
.should('contain.text', 'spec: { field: value }');
cy.getByDataCy('resource-deploy-startup').eq(1)
.should('contain.text', 'No, the resource is referenced by a command');
});
it('reloads the Devfile upon changes in the filesystem', () => {
cy.init();
cy.fixture('input/devfile-new-version.yaml').then(yaml => {
@@ -529,10 +635,10 @@ describe('devfile editor spec', () => {
cy.selectTab(TAB_CONTAINERS);
cy.getByDataCy('container-info').first()
.should('contain.text', 'my-cont1')
.should('contain.text', 'some-image:latest')
.should('contain.text', 'some command')
.should('contain.text', 'some arg');
.should('contain.text', 'my-cont1')
.should('contain.text', 'some-image:latest')
.should('contain.text', 'some command')
.should('contain.text', 'some arg');
});
it('adds an event with an existing command', () => {

View File

@@ -18,14 +18,17 @@ model/container.ts
model/devfileContent.ts
model/devfileGet200Response.ts
model/devfilePutRequest.ts
model/devstateApplyCommandCommandNamePatchRequest.ts
model/devstateApplyCommandPostRequest.ts
model/devstateChartGet200Response.ts
model/devstateCommandCommandNameMovePostRequest.ts
model/devstateCommandCommandNameSetDefaultPostRequest.ts
model/devstateCompositeCommandCommandNamePatchRequest.ts
model/devstateCompositeCommandPostRequest.ts
model/devstateContainerPostRequest.ts
model/devstateDevfilePutRequest.ts
model/devstateEventsPutRequest.ts
model/devstateExecCommandCommandNamePatchRequest.ts
model/devstateExecCommandPostRequest.ts
model/devstateImageImageNamePatchRequest.ts
model/devstateImagePostRequest.ts

View File

@@ -21,6 +21,8 @@ import { Observable } from 'rxjs';
// @ts-ignore
import { DevfileContent } from '../model/devfileContent';
// @ts-ignore
import { DevstateApplyCommandCommandNamePatchRequest } from '../model/devstateApplyCommandCommandNamePatchRequest';
// @ts-ignore
import { DevstateApplyCommandPostRequest } from '../model/devstateApplyCommandPostRequest';
// @ts-ignore
import { DevstateChartGet200Response } from '../model/devstateChartGet200Response';
@@ -29,6 +31,8 @@ import { DevstateCommandCommandNameMovePostRequest } from '../model/devstateComm
// @ts-ignore
import { DevstateCommandCommandNameSetDefaultPostRequest } from '../model/devstateCommandCommandNameSetDefaultPostRequest';
// @ts-ignore
import { DevstateCompositeCommandCommandNamePatchRequest } from '../model/devstateCompositeCommandCommandNamePatchRequest';
// @ts-ignore
import { DevstateCompositeCommandPostRequest } from '../model/devstateCompositeCommandPostRequest';
// @ts-ignore
import { DevstateContainerPostRequest } from '../model/devstateContainerPostRequest';
@@ -37,6 +41,8 @@ import { DevstateDevfilePutRequest } from '../model/devstateDevfilePutRequest';
// @ts-ignore
import { DevstateEventsPutRequest } from '../model/devstateEventsPutRequest';
// @ts-ignore
import { DevstateExecCommandCommandNamePatchRequest } from '../model/devstateExecCommandCommandNamePatchRequest';
// @ts-ignore
import { DevstateExecCommandPostRequest } from '../model/devstateExecCommandPostRequest';
// @ts-ignore
import { DevstateImageImageNamePatchRequest } from '../model/devstateImageImageNamePatchRequest';
@@ -129,6 +135,75 @@ export class DevstateService {
return httpParams;
}
/**
* Update an Apply Command
* @param commandName Command name to update
* @param devstateApplyCommandCommandNamePatchRequest
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public devstateApplyCommandCommandNamePatch(commandName: string, devstateApplyCommandCommandNamePatchRequest?: DevstateApplyCommandCommandNamePatchRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<DevfileContent>;
public devstateApplyCommandCommandNamePatch(commandName: string, devstateApplyCommandCommandNamePatchRequest?: DevstateApplyCommandCommandNamePatchRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpResponse<DevfileContent>>;
public devstateApplyCommandCommandNamePatch(commandName: string, devstateApplyCommandCommandNamePatchRequest?: DevstateApplyCommandCommandNamePatchRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpEvent<DevfileContent>>;
public devstateApplyCommandCommandNamePatch(commandName: string, devstateApplyCommandCommandNamePatchRequest?: DevstateApplyCommandCommandNamePatchRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<any> {
if (commandName === null || commandName === undefined) {
throw new Error('Required parameter commandName was null or undefined when calling devstateApplyCommandCommandNamePatch.');
}
let localVarHeaders = this.defaultHeaders;
let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept;
if (localVarHttpHeaderAcceptSelected === undefined) {
// to determine the Accept header
const httpHeaderAccepts: string[] = [
'application/json'
];
localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts);
}
if (localVarHttpHeaderAcceptSelected !== undefined) {
localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected);
}
let localVarHttpContext: HttpContext | undefined = options && options.context;
if (localVarHttpContext === undefined) {
localVarHttpContext = new HttpContext();
}
// to determine the Content-Type header
const consumes: string[] = [
'application/json'
];
const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes);
if (httpContentTypeSelected !== undefined) {
localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected);
}
let responseType_: 'text' | 'json' | 'blob' = 'json';
if (localVarHttpHeaderAcceptSelected) {
if (localVarHttpHeaderAcceptSelected.startsWith('text')) {
responseType_ = 'text';
} else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) {
responseType_ = 'json';
} else {
responseType_ = 'blob';
}
}
let localVarPath = `/devstate/applyCommand/${this.configuration.encodeParam({name: "commandName", value: commandName, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}`;
return this.httpClient.request<DevfileContent>('patch', `${this.configuration.basePath}${localVarPath}`,
{
context: localVarHttpContext,
body: devstateApplyCommandCommandNamePatchRequest,
responseType: <any>responseType_,
withCredentials: this.configuration.withCredentials,
headers: localVarHeaders,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
* Add a new Apply Command to the Devfile
* @param devstateApplyCommandPostRequest
@@ -502,6 +577,75 @@ export class DevstateService {
);
}
/**
* Update a Composite Command
* @param commandName Command name to update
* @param devstateCompositeCommandCommandNamePatchRequest
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public devstateCompositeCommandCommandNamePatch(commandName: string, devstateCompositeCommandCommandNamePatchRequest?: DevstateCompositeCommandCommandNamePatchRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<DevfileContent>;
public devstateCompositeCommandCommandNamePatch(commandName: string, devstateCompositeCommandCommandNamePatchRequest?: DevstateCompositeCommandCommandNamePatchRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpResponse<DevfileContent>>;
public devstateCompositeCommandCommandNamePatch(commandName: string, devstateCompositeCommandCommandNamePatchRequest?: DevstateCompositeCommandCommandNamePatchRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpEvent<DevfileContent>>;
public devstateCompositeCommandCommandNamePatch(commandName: string, devstateCompositeCommandCommandNamePatchRequest?: DevstateCompositeCommandCommandNamePatchRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<any> {
if (commandName === null || commandName === undefined) {
throw new Error('Required parameter commandName was null or undefined when calling devstateCompositeCommandCommandNamePatch.');
}
let localVarHeaders = this.defaultHeaders;
let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept;
if (localVarHttpHeaderAcceptSelected === undefined) {
// to determine the Accept header
const httpHeaderAccepts: string[] = [
'application/json'
];
localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts);
}
if (localVarHttpHeaderAcceptSelected !== undefined) {
localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected);
}
let localVarHttpContext: HttpContext | undefined = options && options.context;
if (localVarHttpContext === undefined) {
localVarHttpContext = new HttpContext();
}
// to determine the Content-Type header
const consumes: string[] = [
'application/json'
];
const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes);
if (httpContentTypeSelected !== undefined) {
localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected);
}
let responseType_: 'text' | 'json' | 'blob' = 'json';
if (localVarHttpHeaderAcceptSelected) {
if (localVarHttpHeaderAcceptSelected.startsWith('text')) {
responseType_ = 'text';
} else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) {
responseType_ = 'json';
} else {
responseType_ = 'blob';
}
}
let localVarPath = `/devstate/compositeCommand/${this.configuration.encodeParam({name: "commandName", value: commandName, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}`;
return this.httpClient.request<DevfileContent>('patch', `${this.configuration.basePath}${localVarPath}`,
{
context: localVarHttpContext,
body: devstateCompositeCommandCommandNamePatchRequest,
responseType: <any>responseType_,
withCredentials: this.configuration.withCredentials,
headers: localVarHeaders,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
* Add a new Composite Command to the Devfile
* @param devstateCompositeCommandPostRequest
@@ -928,6 +1072,75 @@ export class DevstateService {
);
}
/**
* Update an Exec Command
* @param commandName Command name to update
* @param devstateExecCommandCommandNamePatchRequest
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public devstateExecCommandCommandNamePatch(commandName: string, devstateExecCommandCommandNamePatchRequest?: DevstateExecCommandCommandNamePatchRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<DevfileContent>;
public devstateExecCommandCommandNamePatch(commandName: string, devstateExecCommandCommandNamePatchRequest?: DevstateExecCommandCommandNamePatchRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpResponse<DevfileContent>>;
public devstateExecCommandCommandNamePatch(commandName: string, devstateExecCommandCommandNamePatchRequest?: DevstateExecCommandCommandNamePatchRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpEvent<DevfileContent>>;
public devstateExecCommandCommandNamePatch(commandName: string, devstateExecCommandCommandNamePatchRequest?: DevstateExecCommandCommandNamePatchRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<any> {
if (commandName === null || commandName === undefined) {
throw new Error('Required parameter commandName was null or undefined when calling devstateExecCommandCommandNamePatch.');
}
let localVarHeaders = this.defaultHeaders;
let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept;
if (localVarHttpHeaderAcceptSelected === undefined) {
// to determine the Accept header
const httpHeaderAccepts: string[] = [
'application/json'
];
localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts);
}
if (localVarHttpHeaderAcceptSelected !== undefined) {
localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected);
}
let localVarHttpContext: HttpContext | undefined = options && options.context;
if (localVarHttpContext === undefined) {
localVarHttpContext = new HttpContext();
}
// to determine the Content-Type header
const consumes: string[] = [
'application/json'
];
const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes);
if (httpContentTypeSelected !== undefined) {
localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected);
}
let responseType_: 'text' | 'json' | 'blob' = 'json';
if (localVarHttpHeaderAcceptSelected) {
if (localVarHttpHeaderAcceptSelected.startsWith('text')) {
responseType_ = 'text';
} else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) {
responseType_ = 'json';
} else {
responseType_ = 'blob';
}
}
let localVarPath = `/devstate/execCommand/${this.configuration.encodeParam({name: "commandName", value: commandName, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}`;
return this.httpClient.request<DevfileContent>('patch', `${this.configuration.basePath}${localVarPath}`,
{
context: localVarHttpContext,
body: devstateExecCommandCommandNamePatchRequest,
responseType: <any>responseType_,
withCredentials: this.configuration.withCredentials,
headers: localVarHeaders,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
* Add a new Exec Command to the Devfile
* @param devstateExecCommandPostRequest

View File

@@ -0,0 +1,17 @@
/**
* 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 DevstateApplyCommandCommandNamePatchRequest {
component?: string;
}

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 DevstateCompositeCommandCommandNamePatchRequest {
parallel?: boolean;
commands?: Array<string>;
}

View File

@@ -0,0 +1,20 @@
/**
* 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 DevstateExecCommandCommandNamePatchRequest {
component?: string;
commandLine?: string;
workingDir?: string;
hotReloadCapable?: boolean;
}

View File

@@ -8,14 +8,17 @@ export * from './container';
export * from './devfileContent';
export * from './devfileGet200Response';
export * from './devfilePutRequest';
export * from './devstateApplyCommandCommandNamePatchRequest';
export * from './devstateApplyCommandPostRequest';
export * from './devstateChartGet200Response';
export * from './devstateCommandCommandNameMovePostRequest';
export * from './devstateCommandCommandNameSetDefaultPostRequest';
export * from './devstateCompositeCommandCommandNamePatchRequest';
export * from './devstateCompositeCommandPostRequest';
export * from './devstateContainerPostRequest';
export * from './devstateDevfilePutRequest';
export * from './devstateEventsPutRequest';
export * from './devstateExecCommandCommandNamePatchRequest';
export * from './devstateExecCommandPostRequest';
export * from './devstateImageImageNamePatchRequest';
export * from './devstateImagePostRequest';

View File

@@ -5,7 +5,7 @@ import {
FormArray, FormControl,
FormGroup, NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors, Validator, Validators
ValidationErrors, Validator, ValidatorFn, Validators
} from '@angular/forms';
@Component({
@@ -34,9 +34,19 @@ export class SelectContainerComponent implements ControlValueAccessor, Validator
formCtrl: FormControl;
onChange = (_: string) => {};
onValidatorChange = () => {};
constructor() {
this.formCtrl = new FormControl('', [Validators.required]);
this.formCtrl = new FormControl('', [Validators.required, this.validatorIsNotNew()]);
}
validatorIsNotNew(): ValidatorFn {
return (control:AbstractControl) : ValidationErrors | null => {
if (control.value == '!') {
return {'internal': true};
}
return null;
};
}
writeValue(value: string) {
@@ -50,6 +60,7 @@ export class SelectContainerComponent implements ControlValueAccessor, Validator
registerOnTouched(_: any) {}
onSelectChange(v: string) {
this.onValidatorChange();
if (v != "!") {
this.onChange(v);
}
@@ -57,6 +68,10 @@ export class SelectContainerComponent implements ControlValueAccessor, Validator
}
/* Validator implementation */
registerOnValidatorChange?(onValidatorChange: () => void): void {
this.onValidatorChange = onValidatorChange;
}
validate(control: AbstractControl): ValidationErrors | null {
if (!this.formCtrl.valid) {
return {'internal': true};

View File

@@ -1,6 +1,7 @@
<div class="main">
<h2>Add an Apply Command</h2>
<div class="description">An Apply command "applies" a resource to the cluster. Equivalent to <code>kubectl apply -f ...</code></div>
<h2 *ngIf="!command">Add an Apply Command</h2>
<h2 *ngIf="command">Edit apply command <i>{{command.name}}</i></h2>
<div class="description">An Apply command "applies" a resource to the cluster. Equivalent to <code>kubectl apply -f ...</code></div>
<form [formGroup]="form">
<mat-form-field appearance="outline" class="mid-width">
<mat-label><span>Name</span></mat-label>
@@ -19,6 +20,7 @@
(created)="onNewResourceCreated($event)"
></app-resource>
<button data-cy="command-apply-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new Apply Command" (click)="create()">Create</button>
<button *ngIf="!command" data-cy="command-apply-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new Apply Command" (click)="create()">Create</button>
<button *ngIf="command" data-cy="command-apply-save" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="save Apply Command" (click)="save()">Save</button>
<button mat-flat-button (click)="cancel()">Cancel</button>
</div>

View File

@@ -1,9 +1,9 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { Component, EventEmitter, Input, Output, SimpleChanges } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { StateService } from 'src/app/services/state.service';
import { DevstateService } from 'src/app/services/devstate.service';
import { PATTERN_COMMAND_ID } from '../patterns';
import { Resource } from 'src/app/api-gen';
import { Command, Resource } from 'src/app/api-gen';
import { TelemetryService } from 'src/app/services/telemetry.service';
@Component({
@@ -12,6 +12,8 @@ import { TelemetryService } from 'src/app/services/telemetry.service';
styleUrls: ['./command-apply.component.css']
})
export class CommandApplyComponent {
@Input() command: Command | undefined;
@Output() canceled = new EventEmitter<void>();
form: FormGroup;
@@ -83,4 +85,53 @@ export class CommandApplyComponent {
this.showNewResource = false;
this.resourceToCreate = resource;
}
ngOnChanges(changes: SimpleChanges) {
if (!changes['command']) {
return;
}
const cmd = changes['command'].currentValue;
if (cmd == undefined) {
this.form.get('name')?.enable();
} else {
this.form.reset();
this.form.patchValue(cmd);
this.form.patchValue(cmd.apply);
this.form.get('name')?.disable();
}
}
save() {
this.telemetry.track("[ui] update apply command");
const subcreate = () => {
if (this.command == undefined) {
return;
}
const result = this.devstate.updateApplyCommand(this.command.name, this.form.value);
result.subscribe({
next: (value) => {
this.state.changeDevfileYaml(value);
},
error: (error) => {
alert(error.error.message);
}
});
}
if (this.resourceToCreate != null &&
this.resourceToCreate?.name == this.form.controls["component"].value) {
const result = this.devstate.addResource(this.resourceToCreate);
result.subscribe({
next: (value) => {
this.state.changeDevfileYaml(value);
subcreate();
},
error: (error) => {
alert(error.error.message);
}
});
} else {
subcreate();
}
}
}

View File

@@ -1,5 +1,6 @@
<div class="main">
<h2>Add a Composite Command</h2>
<h2 *ngIf="!command">Add a Composite Command</h2>
<h2 *ngIf="command">Edit composite command <i>{{command.name}}</i></h2>
<div class="description">A Composite command executes several commands, either serially or in parallel.</div>
<form [formGroup]="form">
<mat-form-field appearance="outline" class="mid-width">
@@ -11,6 +12,7 @@
<app-multi-command formControlName="commands" title="Commands" addLabel="Add a command" [commandList]="commandList"></app-multi-command>
</form>
<button data-cy="command-composite-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new Composite Command" (click)="create()">Create</button>
<button *ngIf="!command" data-cy="command-composite-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new Composite Command" (click)="create()">Create</button>
<button *ngIf="command" data-cy="command-composite-save" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="save Composite Command" (click)="save()">Save</button>
<button mat-flat-button (click)="cancel()">Cancel</button>
</div>

View File

@@ -1,9 +1,10 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { Component, EventEmitter, Input, Output, SimpleChanges } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { StateService } from 'src/app/services/state.service';
import { DevstateService } from 'src/app/services/devstate.service';
import { PATTERN_COMMAND_ID } from '../patterns';
import { TelemetryService } from 'src/app/services/telemetry.service';
import { Command } from 'src/app/api-gen';
@Component({
selector: 'app-command-composite',
@@ -11,6 +12,8 @@ import { TelemetryService } from 'src/app/services/telemetry.service';
styleUrls: ['./command-composite.component.css']
})
export class CommandCompositeComponent {
@Input() command: Command | undefined;
@Output() canceled = new EventEmitter<void>();
form: FormGroup;
@@ -53,4 +56,36 @@ export class CommandCompositeComponent {
cancel() {
this.canceled.emit();
}
ngOnChanges(changes: SimpleChanges) {
if (!changes['command']) {
return;
}
const cmd = changes['command'].currentValue;
if (cmd == undefined) {
this.form.get('name')?.enable();
} else {
this.form.reset();
this.form.patchValue(cmd);
this.form.patchValue(cmd.composite);
this.form.get('name')?.disable();
}
}
save() {
this.telemetry.track("[ui] update composite command");
if (this.command == undefined) {
return;
}
const result = this.devstate.updateCompositeCommand(this.command.name, this.form.value);
result.subscribe({
next: (value) => {
this.state.changeDevfileYaml(value);
},
error: (error) => {
alert(error.error.message);
}
});
}
}

View File

@@ -1,5 +1,6 @@
<div class="main">
<h2>Add an Exec Command</h2>
<h2 *ngIf="!command">Add an Exec Command</h2>
<h2 *ngIf="command">Edit exec command <i>{{command.name}}</i></h2>
<div class="description">An Exec command is a shell command executed into a container.</div>
<form [formGroup]="form">
<div><mat-checkbox formControlName="hotReloadCapable">Hot Reload Capable</mat-checkbox></div>
@@ -32,6 +33,7 @@
(created)="onNewContainerCreated($event)"
></app-container>
<button data-cy="command-exec-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new Exec Command" (click)="create()">Create</button>
<button *ngIf="!command" data-cy="command-exec-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new Exec Command" (click)="create()">Create</button>
<button *ngIf="command" data-cy="command-exec-save" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="save command" (click)="save()">Save</button>
<button mat-flat-button (click)="cancel()">Cancel</button>
</div>

View File

@@ -1,9 +1,9 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { Component, EventEmitter, Input, Output, SimpleChanges } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { StateService } from 'src/app/services/state.service';
import { DevstateService } from 'src/app/services/devstate.service';
import { PATTERN_COMMAND_ID } from '../patterns';
import { Container, Volume } from 'src/app/api-gen';
import { Command, Container, Volume } from 'src/app/api-gen';
import { TelemetryService } from 'src/app/services/telemetry.service';
import { ToCreate } from '../container/container.component';
@@ -13,6 +13,8 @@ import { ToCreate } from '../container/container.component';
styleUrls: ['./command-exec.component.css']
})
export class CommandExecComponent {
@Input() command: Command | undefined;
@Output() canceled = new EventEmitter<void>();
form: FormGroup;
@@ -113,4 +115,54 @@ export class CommandExecComponent {
this.containerToCreate = container;
this.volumesToCreate.push(...toCreate.volumes);
}
ngOnChanges(changes: SimpleChanges) {
if (!changes['command']) {
return;
}
const cmd = changes['command'].currentValue;
if (cmd == undefined) {
this.form.get('name')?.enable();
} else {
this.form.reset();
this.form.patchValue(cmd);
this.form.patchValue(cmd.exec);
this.form.get('name')?.disable();
}
}
save() {
this.telemetry.track("[ui] update exec command");
const subcreate = () => {
if (this.command == undefined) {
return;
}
const result = this.devstate.updateExecCommand(this.command.name, this.form.value);
result.subscribe({
next: (value) => {
this.state.changeDevfileYaml(value);
},
error: (error) => {
alert(error.error.message);
}
});
}
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({
next: () => {
subcreate();
},
error: error => {
alert(error.error.message);
}
});
} else {
subcreate();
}
});
}
}

View File

@@ -1,5 +1,6 @@
<div class="main">
<h2>Add an Image Command</h2>
<h2 *ngIf="!command">Add an Image Command</h2>
<h2 *ngIf="command">Edit image command <i>{{command.name}}</i></h2>
<div class="description">An Image command builds a container image and pushes it to a container registry.</div>
<form [formGroup]="form">
<mat-form-field appearance="outline" class="mid-width">
@@ -19,6 +20,7 @@
(created)="onNewImageCreated($event)"
></app-image>
<button data-cy="command-image-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new Image Command" (click)="create()">Create</button>
<button *ngIf="!command" data-cy="command-image-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new Image Command" (click)="create()">Create</button>
<button *ngIf="command" data-cy="command-image-save" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="save Image Command" (click)="save()">Save</button>
<button mat-flat-button (click)="cancel()">Cancel</button>
</div>

View File

@@ -1,9 +1,9 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { Component, EventEmitter, Input, Output, SimpleChanges } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { StateService } from 'src/app/services/state.service';
import { DevstateService } from 'src/app/services/devstate.service';
import { PATTERN_COMMAND_ID } from '../patterns';
import { Image } from 'src/app/api-gen';
import { Command, Image } from 'src/app/api-gen';
import { TelemetryService } from 'src/app/services/telemetry.service';
@Component({
@@ -12,6 +12,8 @@ import { TelemetryService } from 'src/app/services/telemetry.service';
styleUrls: ['./command-image.component.css']
})
export class CommandImageComponent {
@Input() command: Command | undefined;
@Output() canceled = new EventEmitter<void>();
form: FormGroup;
@@ -82,4 +84,54 @@ export class CommandImageComponent {
this.showNewImage = false;
this.imageToCreate = image;
}
ngOnChanges(changes: SimpleChanges) {
if (!changes['command']) {
return;
}
const cmd = changes['command'].currentValue;
if (cmd == undefined) {
this.form.get('name')?.enable();
} else {
this.form.reset();
this.form.patchValue(cmd);
this.form.patchValue(cmd.image);
this.form.get('name')?.disable();
}
}
save() {
this.telemetry.track("[ui] update image command");
const subcreate = () => {
if (this.command == undefined) {
return;
}
const result = this.devstate.updateApplyCommand(this.command.name, this.form.value);
result.subscribe({
next: (value) => {
this.state.changeDevfileYaml(value);
},
error: (error) => {
alert(error.error.message);
}
});
}
if (this.imageToCreate != null &&
this.imageToCreate?.name == this.form.controls["component"].value) {
const result = this.devstate.addImage(this.imageToCreate);
result.subscribe({
next: (value) => {
this.state.changeDevfileYaml(value);
subcreate();
},
error: (error) => {
alert(error.error.message);
}
});
} else {
subcreate();
}
}
}

View File

@@ -103,6 +103,7 @@
<mat-card-actions>
<button mat-button color="warn" (click)="delete(command.name)">Delete</button>
<button data-cy="command-edit" mat-button (click)="edit(command)">Edit</button>
</mat-card-actions>
</mat-card>

View File

@@ -1,4 +1,4 @@
import { Component, Input } from '@angular/core';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { StateService } from 'src/app/services/state.service';
import { DevstateService } from 'src/app/services/devstate.service';
@@ -14,6 +14,8 @@ export class CommandsListComponent {
@Input() kind: string = "";
@Input() dragDisabled: boolean = true;
@Output() onEdit = new EventEmitter<Command>();
constructor(
private devstate: DevstateService,
private state: StateService,
@@ -68,4 +70,8 @@ export class CommandsListComponent {
});
}
}
edit(command: Command) {
this.onEdit.next(command);
}
}

View File

@@ -101,6 +101,15 @@ export class DevstateService {
});
}
updateExecCommand(name: string, cmd: ExecCommand): Observable<DevfileContent> {
return this.http.patch<DevfileContent>(this.base+"/execCommand/"+name, {
component: cmd.component,
commandLine: cmd.commandLine,
workingDir: cmd.workingDir,
hotReloadCapable: cmd.hotReloadCapable,
});
}
addApplyCommand(name: string, cmd: ApplyCommand): Observable<DevfileContent> {
return this.http.post<DevfileContent>(this.base+"/applyCommand", {
name: name,
@@ -108,6 +117,12 @@ export class DevstateService {
});
}
updateApplyCommand(name: string, cmd: ApplyCommand): Observable<DevfileContent> {
return this.http.patch<DevfileContent>(this.base+"/applyCommand/"+name, {
component: cmd.component,
});
}
addCompositeCommand(name: string, cmd: CompositeCommand): Observable<DevfileContent> {
return this.http.post<DevfileContent>(this.base+"/compositeCommand", {
name: name,
@@ -116,6 +131,13 @@ export class DevstateService {
});
}
updateCompositeCommand(name: string, cmd: CompositeCommand): Observable<DevfileContent> {
return this.http.patch<DevfileContent>(this.base+"/compositeCommand/"+name, {
parallel: cmd.parallel,
commands: cmd.commands,
});
}
// getFlowChart calls the wasm module to get the lifecycle of the Devfile in mermaid chart format
getFlowChart(): Observable<DevstateChartGet200Response> {
return this.http.get<DevstateChartGet200Response>(this.base+"/chart");

View File

@@ -7,7 +7,7 @@
(cdkDropListDropped)="drop($event)">
<h2>Build Commands</h2>
<div class="description">When using odo, a Build command is the first command executed during the inner loop. The command is expected to terminate after the build is completed.</div>
<app-commands-list kind="build" [dragDisabled]="!enableDragAndDrop" [commands]="commands"></app-commands-list>
<app-commands-list kind="build" [dragDisabled]="!enableDragAndDrop" [commands]="commands" (onEdit)="edit($event)"></app-commands-list>
</div>
<div
cdkDropList
@@ -15,14 +15,14 @@
(cdkDropListDropped)="drop($event)">
<h2>Run Commands</h2>
<div class="description">When using odo, a Run command is executed during the inner loop after the Build command terminates. The command is expected to not terminate.</div>
<app-commands-list kind="run" [dragDisabled]="!enableDragAndDrop" [commands]="commands"></app-commands-list>
<app-commands-list kind="run" [dragDisabled]="!enableDragAndDrop" [commands]="commands" (onEdit)="edit($event)"></app-commands-list>
</div>
<div
cdkDropList
cdkDropListData="test"
(cdkDropListDropped)="drop($event)">
<h2>Test Commands</h2>
<app-commands-list kind="test" [dragDisabled]="!enableDragAndDrop" [commands]="commands"></app-commands-list>
<app-commands-list kind="test" [dragDisabled]="!enableDragAndDrop" [commands]="commands" (onEdit)="edit($event)"></app-commands-list>
</div>
<div
cdkDropList
@@ -30,7 +30,7 @@
(cdkDropListDropped)="drop($event)">
<h2>Debug Commands</h2>
<div class="description">When using odo, a Debug command is executed during the inner loop after the Build command terminates. The command is expected to not terminate.</div>
<app-commands-list kind="debug" [dragDisabled]="!enableDragAndDrop" [commands]="commands"></app-commands-list>
<app-commands-list kind="debug" [dragDisabled]="!enableDragAndDrop" [commands]="commands" (onEdit)="edit($event)"></app-commands-list>
</div>
<div
cdkDropList
@@ -38,7 +38,7 @@
(cdkDropListDropped)="drop($event)">
<h2>Deploy Commands</h2>
<div class="description">When using odo, a Deploy command is executed with <code>odo deploy</code>.</div>
<app-commands-list kind="deploy" [dragDisabled]="!enableDragAndDrop" [commands]="commands"></app-commands-list>
<app-commands-list kind="deploy" [dragDisabled]="!enableDragAndDrop" [commands]="commands" (onEdit)="edit($event)"></app-commands-list>
</div>
<div
cdkDropList
@@ -46,13 +46,29 @@
(cdkDropListDropped)="drop($event)">
<h2>Generic Commands</h2>
<div class="description">Generic can be executed manually, or be part of composite commands and events.</div>
<app-commands-list kind="" [dragDisabled]="!enableDragAndDrop" [commands]="commands"></app-commands-list>
<app-commands-list kind="" [dragDisabled]="!enableDragAndDrop" [commands]="commands" (onEdit)="edit($event)"></app-commands-list>
</div>
</div>
<app-command-exec (canceled)="undisplayExecForm()" *ngIf="forceDisplayExecForm"></app-command-exec>
<app-command-apply (canceled)="undisplayApplyForm()" *ngIf="forceDisplayApplyForm"></app-command-apply>
<app-command-image (canceled)="undisplayImageForm()" *ngIf="forceDisplayImageForm"></app-command-image>
<app-command-composite (canceled)="undisplayCompositeForm()" *ngIf="forceDisplayCompositeForm"></app-command-composite>
<app-command-exec
(canceled)="undisplayExecForm()"
*ngIf="forceDisplayExecForm"
[command]="editingCommand"
></app-command-exec>
<app-command-apply
(canceled)="undisplayApplyForm()"
*ngIf="forceDisplayApplyForm"
[command]="editingCommand"
></app-command-apply>
<app-command-image
(canceled)="undisplayImageForm()"
*ngIf="forceDisplayImageForm"
[command]="editingCommand"
></app-command-image>
<app-command-composite
(canceled)="undisplayCompositeForm()"
*ngIf="forceDisplayCompositeForm"
[command]="editingCommand"
></app-command-composite>
</div>
<ng-container *ngIf="!forceDisplayExecForm && !forceDisplayApplyForm && !forceDisplayImageForm && !forceDisplayCompositeForm">
@@ -62,19 +78,19 @@
</ng-container>
<mat-menu #menu="matMenu" yPosition="above" xPosition="before">
<button data-cy="new-command-exec" mat-menu-item (click)="displayExecForm()">
<button data-cy="new-command-exec" mat-menu-item (click)="displayAddExecForm()">
<mat-icon class="tab-icon material-icons-outlined">width_normal</mat-icon>
<span>Exec command</span>
</button>
<button data-cy="new-command-image" mat-menu-item (click)="displayImageForm()">
<button data-cy="new-command-image" mat-menu-item (click)="displayAddImageForm()">
<mat-icon class="tab-icon material-icons-outlined">image</mat-icon>
<span>Image command</span>
</button>
<button data-cy="new-command-apply" mat-menu-item (click)="displayApplyForm()">
<button data-cy="new-command-apply" mat-menu-item (click)="displayAddApplyForm()">
<mat-icon class="tab-icon material-icons-outlined">description</mat-icon>
<span>Apply command</span>
</button>
<button data-cy="new-command-composite" mat-menu-item (click)="displayCompositeForm()">
<button data-cy="new-command-composite" mat-menu-item (click)="displayAddCompositeForm()">
<span>Composite command</span>
</button>
</mat-menu>

View File

@@ -19,6 +19,8 @@ export class CommandsComponent {
commands: Command[] | undefined = [];
editingCommand: Command | undefined;
constructor(
private state: StateService,
private devstate: DevstateService,
@@ -40,32 +42,52 @@ export class CommandsComponent {
});
}
displayExecForm() {
displayAddExecForm() {
this.telemetry.track("[ui] start create exec command");
this.editingCommand = undefined;
this.displayExecForm();
}
displayExecForm() {
this.forceDisplayExecForm = true;
setTimeout(() => {
this.scrollToBottom();
}, 0);
}
displayApplyForm() {
displayAddApplyForm() {
this.telemetry.track("[ui] start create apply command");
this.editingCommand = undefined;
this.displayApplyForm();
}
displayApplyForm() {
this.forceDisplayApplyForm = true;
setTimeout(() => {
this.scrollToBottom();
}, 0);
}
displayImageForm() {
displayAddImageForm() {
this.telemetry.track("[ui] start create image command");
this.editingCommand = undefined;
this.displayImageForm();
}
displayImageForm() {
this.forceDisplayImageForm = true;
setTimeout(() => {
this.scrollToBottom();
}, 0);
}
displayCompositeForm() {
displayAddCompositeForm() {
this.telemetry.track("[ui] start create composite command");
this.editingCommand = undefined;
this.displayCompositeForm();
}
displayCompositeForm() {
this.forceDisplayCompositeForm = true;
setTimeout(() => {
this.scrollToBottom();
@@ -117,4 +139,26 @@ export class CommandsComponent {
window.scrollTo(0,document.body.scrollHeight);
}
edit(command: Command) {
this.editingCommand = command;
this.undisplayExecForm();
this.undisplayApplyForm();
this.undisplayImageForm();
this.undisplayCompositeForm();
switch (command.type) {
case 'exec':
this.displayExecForm();
break;
case 'apply':
this.displayApplyForm();
break;
case 'image':
this.displayImageForm();
break;
case 'composite':
this.displayCompositeForm();
break;
}
}
}