[ui] Edit images (#7068)

* [api] Update Image

* [ui] edit images

* static ui files
This commit is contained in:
Philippe Martin
2023-09-04 15:48:06 +02:00
committed by GitHub
parent e65b4f6dba
commit 8051843d89
21 changed files with 514 additions and 12 deletions

View File

@@ -757,6 +757,86 @@ paths:
example:
message: "Error deleting the image"
patch:
tags:
- devstate
description: Update an image
parameters:
- name: imageName
in: path
description: Image name to update
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
type: object
properties:
imageName:
type: string
args:
type: array
items:
type: string
buildContext:
type: string
rootRequired:
type: boolean
uri:
type: string
autoBuild:
type: string
enum:
- never
- undefined
- always
responses:
'200':
description: image was successfully updated
content:
application/json:
schema:
$ref: '#/components/schemas/DevfileContent'
example:
{
"content": "schemaVersion: 2.2.0\n",
"commands": [],
"containers": [],
"images": [],
"resources": [],
"events": {
"preStart": null,
"postStart": null,
"preStop": null,
"postStop": null
},
"metadata": {
"name": "",
"version": "",
"displayName": "",
description": "",
"tags": "",
"architectures": "",
"icon": "",
"globalMemoryLimit": "",
"projectType": "",
"language": "",
"website": "",
"provider": "",
"supportUrl": ""
}
}
'500':
description: Error updating the image
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
example:
message: "Error updating the image"
/devstate/resource:
post:
tags:

View File

@@ -17,6 +17,7 @@ go/model__devstate_composite_command_post_request.go
go/model__devstate_container_post_request.go
go/model__devstate_events_put_request.go
go/model__devstate_exec_command_post_request.go
go/model__devstate_image__image_name__patch_request.go
go/model__devstate_image_post_request.go
go/model__devstate_quantity_valid_post_request.go
go/model__devstate_resource__resource_name__patch_request.go

View File

@@ -46,6 +46,7 @@ type DevstateApiRouter interface {
DevstateEventsPut(http.ResponseWriter, *http.Request)
DevstateExecCommandPost(http.ResponseWriter, *http.Request)
DevstateImageImageNameDelete(http.ResponseWriter, *http.Request)
DevstateImageImageNamePatch(http.ResponseWriter, *http.Request)
DevstateImagePost(http.ResponseWriter, *http.Request)
DevstateMetadataPut(http.ResponseWriter, *http.Request)
DevstateQuantityValidPost(http.ResponseWriter, *http.Request)
@@ -91,6 +92,7 @@ type DevstateApiServicer interface {
DevstateEventsPut(context.Context, DevstateEventsPutRequest) (ImplResponse, error)
DevstateExecCommandPost(context.Context, DevstateExecCommandPostRequest) (ImplResponse, error)
DevstateImageImageNameDelete(context.Context, string) (ImplResponse, error)
DevstateImageImageNamePatch(context.Context, string, DevstateImageImageNamePatchRequest) (ImplResponse, error)
DevstateImagePost(context.Context, DevstateImagePostRequest) (ImplResponse, error)
DevstateMetadataPut(context.Context, MetadataRequest) (ImplResponse, error)
DevstateQuantityValidPost(context.Context, DevstateQuantityValidPostRequest) (ImplResponse, error)

View File

@@ -140,6 +140,12 @@ func (c *DevstateApiController) Routes() Routes {
"/api/v1/devstate/image/{imageName}",
c.DevstateImageImageNameDelete,
},
{
"DevstateImageImageNamePatch",
strings.ToUpper("Patch"),
"/api/v1/devstate/image/{imageName}",
c.DevstateImageImageNamePatch,
},
{
"DevstateImagePost",
strings.ToUpper("Post"),
@@ -492,6 +498,32 @@ func (c *DevstateApiController) DevstateImageImageNameDelete(w http.ResponseWrit
}
// DevstateImageImageNamePatch -
func (c *DevstateApiController) DevstateImageImageNamePatch(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
imageNameParam := params["imageName"]
devstateImageImageNamePatchRequestParam := DevstateImageImageNamePatchRequest{}
d := json.NewDecoder(r.Body)
d.DisallowUnknownFields()
if err := d.Decode(&devstateImageImageNamePatchRequestParam); err != nil {
c.errorHandler(w, r, &ParsingError{Err: err}, nil)
return
}
if err := AssertDevstateImageImageNamePatchRequestRequired(devstateImageImageNamePatchRequestParam); err != nil {
c.errorHandler(w, r, err, nil)
return
}
result, err := c.service.DevstateImageImageNamePatch(r.Context(), imageNameParam, devstateImageImageNamePatchRequestParam)
// If an error occurred, encode the error with the status code
if err != nil {
c.errorHandler(w, r, err, &result)
return
}
// If no error, encode the body and the result code
EncodeJSONResponse(result.Body, &result.Code, w)
}
// DevstateImagePost -
func (c *DevstateApiController) DevstateImagePost(w http.ResponseWriter, r *http.Request) {
devstateImagePostRequestParam := DevstateImagePostRequest{}

View File

@@ -0,0 +1,41 @@
/*
* odo dev
*
* API interface for 'odo dev'
*
* API version: 0.1
* Generated by: OpenAPI Generator (https://openapi-generator.tech)
*/
package openapi
type DevstateImageImageNamePatchRequest struct {
ImageName string `json:"imageName,omitempty"`
Args []string `json:"args,omitempty"`
BuildContext string `json:"buildContext,omitempty"`
RootRequired bool `json:"rootRequired,omitempty"`
Uri string `json:"uri,omitempty"`
AutoBuild string `json:"autoBuild,omitempty"`
}
// AssertDevstateImageImageNamePatchRequestRequired checks if the required fields are not zero-ed
func AssertDevstateImageImageNamePatchRequestRequired(obj DevstateImageImageNamePatchRequest) error {
return nil
}
// AssertRecurseDevstateImageImageNamePatchRequestRequired recursively checks if required fields are not zero-ed in a nested slice.
// Accepts only nested slice of DevstateImageImageNamePatchRequest (e.g. [][]DevstateImageImageNamePatchRequest), otherwise ErrTypeAssertionError is thrown.
func AssertRecurseDevstateImageImageNamePatchRequestRequired(objSlice interface{}) error {
return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error {
aDevstateImageImageNamePatchRequest, ok := obj.(DevstateImageImageNamePatchRequest)
if !ok {
return ErrTypeAssertionError
}
return AssertDevstateImageImageNamePatchRequestRequired(aDevstateImageImageNamePatchRequest)
})
}

View File

@@ -328,3 +328,22 @@ func (s *DevstateApiService) DevstateResourceResourceNamePatch(ctx context.Conte
}
return openapi.Response(http.StatusOK, newContent), nil
}
func (s *DevstateApiService) DevstateImageImageNamePatch(ctx context.Context, name string, patch openapi.DevstateImageImageNamePatchRequest) (openapi.ImplResponse, error) {
newContent, err := s.devfileState.PatchImage(
name,
patch.ImageName,
patch.Args,
patch.BuildContext,
patch.RootRequired,
patch.Uri,
patch.AutoBuild,
)
if err != nil {
return openapi.Response(http.StatusInternalServerError, openapi.GeneralError{
Message: fmt.Sprintf("Error updating the image: %s", err),
}), nil
}
return openapi.Response(http.StatusOK, newContent), nil
}

View File

@@ -165,6 +165,45 @@ func (o *DevfileState) AddImage(name string, imageName string, args []string, bu
return o.GetContent()
}
func (o *DevfileState) PatchImage(name string, imageName string, args []string, buildContext string, rootRequired bool, uri string, autoBuild string) (DevfileContent, error) {
found, err := o.Devfile.Data.GetComponents(common.DevfileOptions{
ComponentOptions: common.ComponentOptions{
ComponentType: v1alpha2.ImageComponentType,
},
FilterByName: name,
})
if err != nil {
return DevfileContent{}, err
}
if len(found) != 1 {
return DevfileContent{}, fmt.Errorf("%d Image found with name %q", len(found), name)
}
image := found[0]
if image.Image == nil {
image.Image = &v1alpha2.ImageComponent{}
}
image.Image.ImageName = imageName
if image.Image.Dockerfile == nil {
image.Image.Dockerfile = &v1alpha2.DockerfileImage{}
}
image.Image.Dockerfile.Args = args
image.Image.Dockerfile.BuildContext = buildContext
image.Image.Dockerfile.RootRequired = &rootRequired
image.Image.Dockerfile.DockerfileSrc.Uri = uri
image.Image.AutoBuild = nil
if autoBuild == "never" {
image.Image.AutoBuild = pointer.Bool(false)
} else if autoBuild == "always" {
image.Image.AutoBuild = pointer.Bool(true)
}
err = o.Devfile.Data.UpdateComponent(image)
if err != nil {
return DevfileContent{}, err
}
return o.GetContent()
}
func (o *DevfileState) DeleteImage(name string) (DevfileContent, error) {
err := o.checkImageUsed(name)

View File

@@ -757,6 +757,86 @@ paths:
example:
message: "Error deleting the image"
patch:
tags:
- devstate
description: Update an image
parameters:
- name: imageName
in: path
description: Image name to update
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
type: object
properties:
imageName:
type: string
args:
type: array
items:
type: string
buildContext:
type: string
rootRequired:
type: boolean
uri:
type: string
autoBuild:
type: string
enum:
- never
- undefined
- always
responses:
'200':
description: image was successfully updated
content:
application/json:
schema:
$ref: '#/components/schemas/DevfileContent'
example:
{
"content": "schemaVersion: 2.2.0\n",
"commands": [],
"containers": [],
"images": [],
"resources": [],
"events": {
"preStart": null,
"postStart": null,
"preStop": null,
"postStop": null
},
"metadata": {
"name": "",
"version": "",
"displayName": "",
description": "",
"tags": "",
"architectures": "",
"icon": "",
"globalMemoryLimit": "",
"projectType": "",
"language": "",
"website": "",
"provider": "",
"supportUrl": ""
}
}
'500':
description: Error updating the image
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
example:
message: "Error updating the image"
/devstate/resource:
post:
tags:

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -180,6 +180,43 @@ describe('devfile editor spec', () => {
.should('contain.text', 'Yes, the image is not referenced by any command');
});
it('displays a modified image', () => {
cy.init();
cy.selectTab(TAB_IMAGES);
cy.getByDataCy('image-name').type('created-image');
cy.getByDataCy('image-image-name').type('an-image-name');
cy.getByDataCy('image-build-context').type('/path/to/build/context');
cy.getByDataCy('image-dockerfile-uri').type('/path/to/dockerfile');
cy.getByDataCy('image-create').click();
cy.getByDataCy('image-info').first()
.should('contain.text', 'created-image')
.should('contain.text', 'an-image-name')
.should('contain.text', '/path/to/build/context')
.should('contain.text', '/path/to/dockerfile');
cy.getByDataCy('image-build-startup').first()
.should('contain.text', 'Yes, the image is not referenced by any command');
cy.getByDataCy('image-edit').click();
cy.getByDataCy('image-auto-build-always').click();
cy.getByDataCy('image-image-name').type('{selectAll}{del}another-image-name');
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')
.should('contain.text', '/new/path/to/build/context')
.should('contain.text', '/new/path/to/dockerfile');
cy.getByDataCy('image-build-startup').first()
.should('contain.text', 'Yes, forced');
});
it('displays a created image with forced build', () => {
cy.init();

View File

@@ -27,6 +27,7 @@ model/devstateContainerPostRequest.ts
model/devstateDevfilePutRequest.ts
model/devstateEventsPutRequest.ts
model/devstateExecCommandPostRequest.ts
model/devstateImageImageNamePatchRequest.ts
model/devstateImagePostRequest.ts
model/devstateQuantityValidPostRequest.ts
model/devstateResourcePostRequest.ts

View File

@@ -39,6 +39,8 @@ import { DevstateEventsPutRequest } from '../model/devstateEventsPutRequest';
// @ts-ignore
import { DevstateExecCommandPostRequest } from '../model/devstateExecCommandPostRequest';
// @ts-ignore
import { DevstateImageImageNamePatchRequest } from '../model/devstateImageImageNamePatchRequest';
// @ts-ignore
import { DevstateImagePostRequest } from '../model/devstateImagePostRequest';
// @ts-ignore
import { DevstateQuantityValidPostRequest } from '../model/devstateQuantityValidPostRequest';
@@ -1049,6 +1051,75 @@ export class DevstateService {
);
}
/**
* Update an image
* @param imageName Image name to update
* @param devstateImageImageNamePatchRequest
* @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 devstateImageImageNamePatch(imageName: string, devstateImageImageNamePatchRequest?: DevstateImageImageNamePatchRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<DevfileContent>;
public devstateImageImageNamePatch(imageName: string, devstateImageImageNamePatchRequest?: DevstateImageImageNamePatchRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpResponse<DevfileContent>>;
public devstateImageImageNamePatch(imageName: string, devstateImageImageNamePatchRequest?: DevstateImageImageNamePatchRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpEvent<DevfileContent>>;
public devstateImageImageNamePatch(imageName: string, devstateImageImageNamePatchRequest?: DevstateImageImageNamePatchRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<any> {
if (imageName === null || imageName === undefined) {
throw new Error('Required parameter imageName was null or undefined when calling devstateImageImageNamePatch.');
}
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/image/${this.configuration.encodeParam({name: "imageName", value: imageName, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}`;
return this.httpClient.request<DevfileContent>('patch', `${this.configuration.basePath}${localVarPath}`,
{
context: localVarHttpContext,
body: devstateImageImageNamePatchRequest,
responseType: <any>responseType_,
withCredentials: this.configuration.withCredentials,
headers: localVarHeaders,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
* Add a new image to the Devfile
* @param devstateImagePostRequest

View File

@@ -0,0 +1,31 @@
/**
* 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 DevstateImageImageNamePatchRequest {
imageName?: string;
args?: Array<string>;
buildContext?: string;
rootRequired?: boolean;
uri?: string;
autoBuild?: DevstateImageImageNamePatchRequest.AutoBuildEnum;
}
export namespace DevstateImageImageNamePatchRequest {
export type AutoBuildEnum = 'never' | 'undefined' | 'always';
export const AutoBuildEnum = {
Never: 'never' as AutoBuildEnum,
Undefined: 'undefined' as AutoBuildEnum,
Always: 'always' as AutoBuildEnum
};
}

View File

@@ -17,6 +17,7 @@ export * from './devstateContainerPostRequest';
export * from './devstateDevfilePutRequest';
export * from './devstateEventsPutRequest';
export * from './devstateExecCommandPostRequest';
export * from './devstateImageImageNamePatchRequest';
export * from './devstateImagePostRequest';
export * from './devstateQuantityValidPostRequest';
export * from './devstateResourcePostRequest';

View File

@@ -24,6 +24,9 @@ export class MultiTextComponent implements ControlValueAccessor {
texts: string[] = [];
writeValue(value: any) {
if (value == null) {
value = [];
}
this.texts = value;
}

View File

@@ -1,5 +1,6 @@
<div class="main">
<h2>Add a new image</h2>
<h2 *ngIf="!image">Add a new image</h2>
<h2 *ngIf="image">Edit image <i>{{image.name}}</i></h2>
<div class="description">An Image defines how to build a container image.</div>
<form [formGroup]="form">
<div class="toggle-group-div">
@@ -31,6 +32,7 @@
</form>
<button data-cy="image-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new image" (click)="create()">Create</button>
<button *ngIf="!image" data-cy="image-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new image" (click)="create()">Create</button>
<button *ngIf="image" data-cy="image-save" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="save image" (click)="save()">Save</button>
<button *ngIf="cancelable" mat-flat-button (click)="cancel()">Cancel</button>
</div>

View File

@@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Component, EventEmitter, Input, Output, SimpleChanges } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { PATTERN_COMPONENT_ID } from '../patterns';
import { Image } from 'src/app/api-gen';
@@ -11,8 +11,11 @@ import { TelemetryService } from 'src/app/services/telemetry.service';
})
export class ImageComponent {
@Input() cancelable: boolean = false;
@Input() image: Image | undefined;
@Output() canceled = new EventEmitter<void>();
@Output() created = new EventEmitter<Image>();
@Output() saved = new EventEmitter<Image>();
form: FormGroup;
@@ -35,7 +38,29 @@ export class ImageComponent {
this.created.emit(this.form.value);
}
save() {
const newValue = this.form.value;
newValue.name = this.image?.name;
this.telemetry.track("[ui] edit volume");
this.saved.emit(this.form.value);
}
cancel() {
this.canceled.emit();
}
ngOnChanges(changes: SimpleChanges) {
console.log("changes", changes);
if (!changes['image']) {
return;
}
const img = changes['image'].currentValue;
if (img == undefined) {
this.form.get('name')?.enable();
} else {
this.form.reset();
this.form.patchValue(img);
this.form.get('name')?.disable();
}
}
}

View File

@@ -48,6 +48,17 @@ export class DevstateService {
});
}
saveImage(image: Image): Observable<DevfileContent> {
return this.http.patch<DevfileContent>(this.base+"/image/"+image.name, {
imageName: image.imageName,
args: image.args,
buildContext: image.buildContext,
rootRequired: image.rootRequired,
uri: image.uri,
autoBuild: image.autoBuild,
});
}
addResource(resource: Resource): Observable<DevfileContent> {
return this.http.post<DevfileContent>(this.base+"/resource", {
name: resource.name,

View File

@@ -38,19 +38,22 @@
<mat-card-actions>
<button mat-button color="warn" (click)="delete(image.name)">Delete</button>
<button data-cy="image-edit" mat-button (click)="edit(image)">Edit</button>
</mat-card-actions>
</mat-card>
<app-image
*ngIf="forceDisplayAdd || images == undefined || images.length == 0"
[cancelable]="forceDisplayAdd"
*ngIf="forceDisplayForm || images == undefined || images.length == 0"
[cancelable]="forceDisplayForm"
(canceled)="undisplayAddForm()"
(created)="onCreated($event)"
[image]="editingImage"
(saved)="onSaved($event)"
></app-image>
</div>
<ng-container *ngIf="!forceDisplayAdd && images != undefined && images.length > 0">
<ng-container *ngIf="!forceDisplayForm && images != undefined && images.length > 0">
<button class="fab" mat-fab color="primary" (click)="displayAddForm()">
<mat-icon class="material-icons-outlined">add</mat-icon>
</button>

View File

@@ -10,8 +10,9 @@ import { Image } from 'src/app/api-gen';
})
export class ImagesComponent implements OnInit {
forceDisplayAdd: boolean = false;
forceDisplayForm: boolean = false;
images: Image[] | undefined = [];
editingImage: Image | undefined;
constructor(
private state: StateService,
@@ -25,19 +26,24 @@ export class ImagesComponent implements OnInit {
if (this.images == null) {
return
}
that.forceDisplayAdd = false;
that.forceDisplayForm = false;
});
}
displayAddForm() {
this.forceDisplayAdd = true;
this.editingImage = undefined;
this.displayForm();
}
displayForm() {
this.forceDisplayForm = true;
setTimeout(() => {
this.scrollToBottom();
}, 0);
}
undisplayAddForm() {
this.forceDisplayAdd = false;
this.forceDisplayForm = false;
}
delete(name: string) {
@@ -54,6 +60,11 @@ export class ImagesComponent implements OnInit {
}
}
edit(image: Image) {
this.editingImage = image;
this.displayForm();
}
onCreated(image: Image) {
const result = this.devstate.addImage(image);
result.subscribe({
@@ -66,6 +77,18 @@ export class ImagesComponent implements OnInit {
});
}
onSaved(image: Image) {
const result = this.devstate.saveImage(image);
result.subscribe({
next: value => {
this.state.changeDevfileYaml(value);
},
error: error => {
alert(error.error.message);
}
});
}
scrollToBottom() {
window.scrollTo(0,document.body.scrollHeight);
}