[ui] Edit volumes (#7061)

* Update volumes

* Proxy use ipv4

* Static ui files

* e2e tests
This commit is contained in:
Philippe Martin
2023-08-31 10:49:27 +02:00
committed by GitHub
parent 9089f9a637
commit 7ff38b7965
21 changed files with 387 additions and 14 deletions

View File

@@ -939,6 +939,47 @@ paths:
example:
message: "Error deleting the volume"
patch:
tags:
- devstate
description: "Update a volume"
parameters:
- name: volumeName
in: path
description: Volume name to update
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
type: object
properties:
size:
description: Minimal size of the volume
type: string
ephemeral:
description: True if the Volume is Ephemeral
type: boolean
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralSuccess'
example:
message: "Volume has been updated"
description: "Volume has been updated"
'500':
description: Error updating the volume
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
example:
message: "Error updating the volume"
/devstate/applyCommand:
post:
tags:

View File

@@ -20,6 +20,7 @@ go/model__devstate_exec_command_post_request.go
go/model__devstate_image_post_request.go
go/model__devstate_quantity_valid_post_request.go
go/model__devstate_resource_post_request.go
go/model__devstate_volume__volume_name__patch_request.go
go/model__devstate_volume_post_request.go
go/model__instance_get_200_response.go
go/model_annotation.go

View File

@@ -53,6 +53,7 @@ type DevstateApiRouter interface {
DevstateResourceResourceNameDelete(http.ResponseWriter, *http.Request)
DevstateVolumePost(http.ResponseWriter, *http.Request)
DevstateVolumeVolumeNameDelete(http.ResponseWriter, *http.Request)
DevstateVolumeVolumeNamePatch(http.ResponseWriter, *http.Request)
}
// DefaultApiServicer defines the api actions for the DefaultApi service
@@ -96,4 +97,5 @@ type DevstateApiServicer interface {
DevstateResourceResourceNameDelete(context.Context, string) (ImplResponse, error)
DevstateVolumePost(context.Context, DevstateVolumePostRequest) (ImplResponse, error)
DevstateVolumeVolumeNameDelete(context.Context, string) (ImplResponse, error)
DevstateVolumeVolumeNamePatch(context.Context, string, DevstateVolumeVolumeNamePatchRequest) (ImplResponse, error)
}

View File

@@ -182,6 +182,12 @@ func (c *DevstateApiController) Routes() Routes {
"/api/v1/devstate/volume/{volumeName}",
c.DevstateVolumeVolumeNameDelete,
},
{
"DevstateVolumeVolumeNamePatch",
strings.ToUpper("Patch"),
"/api/v1/devstate/volume/{volumeName}",
c.DevstateVolumeVolumeNamePatch,
},
}
}
@@ -629,3 +635,29 @@ func (c *DevstateApiController) DevstateVolumeVolumeNameDelete(w http.ResponseWr
EncodeJSONResponse(result.Body, &result.Code, w)
}
// DevstateVolumeVolumeNamePatch -
func (c *DevstateApiController) DevstateVolumeVolumeNamePatch(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
volumeNameParam := params["volumeName"]
devstateVolumeVolumeNamePatchRequestParam := DevstateVolumeVolumeNamePatchRequest{}
d := json.NewDecoder(r.Body)
d.DisallowUnknownFields()
if err := d.Decode(&devstateVolumeVolumeNamePatchRequestParam); err != nil {
c.errorHandler(w, r, &ParsingError{Err: err}, nil)
return
}
if err := AssertDevstateVolumeVolumeNamePatchRequestRequired(devstateVolumeVolumeNamePatchRequestParam); err != nil {
c.errorHandler(w, r, err, nil)
return
}
result, err := c.service.DevstateVolumeVolumeNamePatch(r.Context(), volumeNameParam, devstateVolumeVolumeNamePatchRequestParam)
// 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)
}

View File

@@ -0,0 +1,36 @@
/*
* odo dev
*
* API interface for 'odo dev'
*
* API version: 0.1
* Generated by: OpenAPI Generator (https://openapi-generator.tech)
*/
package openapi
type DevstateVolumeVolumeNamePatchRequest struct {
// Minimal size of the volume
Size string `json:"size,omitempty"`
// True if the Volume is Ephemeral
Ephemeral bool `json:"ephemeral,omitempty"`
}
// AssertDevstateVolumeVolumeNamePatchRequestRequired checks if the required fields are not zero-ed
func AssertDevstateVolumeVolumeNamePatchRequestRequired(obj DevstateVolumeVolumeNamePatchRequest) error {
return nil
}
// AssertRecurseDevstateVolumeVolumeNamePatchRequestRequired recursively checks if required fields are not zero-ed in a nested slice.
// Accepts only nested slice of DevstateVolumeVolumeNamePatchRequest (e.g. [][]DevstateVolumeVolumeNamePatchRequest), otherwise ErrTypeAssertionError is thrown.
func AssertRecurseDevstateVolumeVolumeNamePatchRequestRequired(objSlice interface{}) error {
return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error {
aDevstateVolumeVolumeNamePatchRequest, ok := obj.(DevstateVolumeVolumeNamePatchRequest)
if !ok {
return ErrTypeAssertionError
}
return AssertDevstateVolumeVolumeNamePatchRequestRequired(aDevstateVolumeVolumeNamePatchRequest)
})
}

View File

@@ -297,3 +297,18 @@ func (s *DevstateApiService) DevstateDevfileDelete(context.Context) (openapi.Imp
}
return openapi.Response(http.StatusOK, newContent), nil
}
func (s *DevstateApiService) DevstateVolumeVolumeNamePatch(ctx context.Context, name string, patch openapi.DevstateVolumeVolumeNamePatchRequest) (openapi.ImplResponse, error) {
newContent, err := s.devfileState.PatchVolume(
name,
patch.Ephemeral,
patch.Size,
)
if err != nil {
return openapi.Response(http.StatusInternalServerError, openapi.GeneralError{
Message: fmt.Sprintf("Error updating the volume: %s", err),
}), nil
}
return openapi.Response(http.StatusOK, newContent), nil
}

View File

@@ -267,8 +267,32 @@ func (o *DevfileState) AddVolume(name string, ephemeral bool, size string) (Devf
return o.GetContent()
}
func (o *DevfileState) DeleteVolume(name string) (DevfileContent, error) {
func (o *DevfileState) PatchVolume(name string, ephemeral bool, size string) (DevfileContent, error) {
found, err := o.Devfile.Data.GetComponents(common.DevfileOptions{
ComponentOptions: common.ComponentOptions{
ComponentType: v1alpha2.VolumeComponentType,
},
FilterByName: name,
})
if err != nil {
return DevfileContent{}, err
}
if len(found) != 1 {
return DevfileContent{}, fmt.Errorf("%d Volume found with name %q", len(found), name)
}
volume := found[0]
volume.Volume.Ephemeral = &ephemeral
volume.Volume.Size = size
err = o.Devfile.Data.UpdateComponent(volume)
if err != nil {
return DevfileContent{}, err
}
return o.GetContent()
}
func (o *DevfileState) DeleteVolume(name string) (DevfileContent, error) {
err := o.checkVolumeUsed(name)
if err != nil {
return DevfileContent{}, fmt.Errorf("error deleting volume %q: %w", name, err)

View File

@@ -939,6 +939,47 @@ paths:
example:
message: "Error deleting the volume"
patch:
tags:
- devstate
description: "Update a volume"
parameters:
- name: volumeName
in: path
description: Volume name to update
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
type: object
properties:
size:
description: Minimal size of the volume
type: string
ephemeral:
description: True if the Volume is Ephemeral
type: boolean
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralSuccess'
example:
message: "Volume has been updated"
description: "Volume has been updated"
'500':
description: Error updating the volume
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
example:
message: "Error updating the volume"
/devstate/applyCommand:
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.fcdb01b1f229861f.js" type="module"></script>
<script src="runtime.1289ea0acffcdc5e.js" type="module"></script><script src="polyfills.8b3b37cedaf377c3.js" type="module"></script><script src="main.74249c789bca5336.js" type="module"></script>
</body></html>

File diff suppressed because one or more lines are too long

View File

@@ -220,6 +220,31 @@ describe('devfile editor spec', () => {
.should('contain.text', 'Yes')
});
it('displays a modified volume', () => {
cy.init();
cy.selectTab(TAB_VOLUMES);
cy.getByDataCy('volume-name').type('created-volume');
cy.getByDataCy('volume-size').type('512Mi');
cy.getByDataCy('volume-ephemeral').click();
cy.getByDataCy('volume-create').click();
cy.getByDataCy('volume-info').first()
.should('contain.text', 'created-volume')
.should('contain.text', '512Mi')
.should('contain.text', 'Yes');
cy.getByDataCy('volume-edit').click();
cy.getByDataCy('volume-size').type('{selectAll}{del}1Gi');
cy.getByDataCy('volume-ephemeral').click();
cy.getByDataCy('volume-save').click();
cy.getByDataCy('volume-info').first()
.should('contain.text', 'created-volume')
.should('contain.text', '1Gi')
.should('contain.text', 'No');
});
it('creates an exec command with a new container', () => {
cy.init();

View File

@@ -31,6 +31,7 @@ model/devstateImagePostRequest.ts
model/devstateQuantityValidPostRequest.ts
model/devstateResourcePostRequest.ts
model/devstateVolumePostRequest.ts
model/devstateVolumeVolumeNamePatchRequest.ts
model/endpoint.ts
model/env.ts
model/events.ts

View File

@@ -47,6 +47,8 @@ import { DevstateResourcePostRequest } from '../model/devstateResourcePostReques
// @ts-ignore
import { DevstateVolumePostRequest } from '../model/devstateVolumePostRequest';
// @ts-ignore
import { DevstateVolumeVolumeNamePatchRequest } from '../model/devstateVolumeVolumeNamePatchRequest';
// @ts-ignore
import { GeneralError } from '../model/generalError';
// @ts-ignore
import { GeneralSuccess } from '../model/generalSuccess';
@@ -1486,4 +1488,73 @@ export class DevstateService {
);
}
/**
* Update a volume
* @param volumeName Volume name to update
* @param devstateVolumeVolumeNamePatchRequest
* @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 devstateVolumeVolumeNamePatch(volumeName: string, devstateVolumeVolumeNamePatchRequest?: DevstateVolumeVolumeNamePatchRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<GeneralSuccess>;
public devstateVolumeVolumeNamePatch(volumeName: string, devstateVolumeVolumeNamePatchRequest?: DevstateVolumeVolumeNamePatchRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpResponse<GeneralSuccess>>;
public devstateVolumeVolumeNamePatch(volumeName: string, devstateVolumeVolumeNamePatchRequest?: DevstateVolumeVolumeNamePatchRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<HttpEvent<GeneralSuccess>>;
public devstateVolumeVolumeNamePatch(volumeName: string, devstateVolumeVolumeNamePatchRequest?: DevstateVolumeVolumeNamePatchRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable<any> {
if (volumeName === null || volumeName === undefined) {
throw new Error('Required parameter volumeName was null or undefined when calling devstateVolumeVolumeNamePatch.');
}
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/volume/${this.configuration.encodeParam({name: "volumeName", value: volumeName, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}`;
return this.httpClient.request<GeneralSuccess>('patch', `${this.configuration.basePath}${localVarPath}`,
{
context: localVarHttpContext,
body: devstateVolumeVolumeNamePatchRequest,
responseType: <any>responseType_,
withCredentials: this.configuration.withCredentials,
headers: localVarHeaders,
observe: observe,
reportProgress: reportProgress
}
);
}
}

View File

@@ -0,0 +1,24 @@
/**
* 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 DevstateVolumeVolumeNamePatchRequest {
/**
* Minimal size of the volume
*/
size?: string;
/**
* True if the Volume is Ephemeral
*/
ephemeral?: boolean;
}

View File

@@ -21,6 +21,7 @@ export * from './devstateImagePostRequest';
export * from './devstateQuantityValidPostRequest';
export * from './devstateResourcePostRequest';
export * from './devstateVolumePostRequest';
export * from './devstateVolumeVolumeNamePatchRequest';
export * from './endpoint';
export * from './env';
export * from './events';

View File

@@ -1,5 +1,6 @@
<div class="main">
<h2>Add a new volume</h2>
<h2 *ngIf="!volume">Add a new volume</h2>
<h2 *ngIf="volume">Edit volume <i>{{volume.name}}</i></h2>
<div class="description">A volume can be mounted and shared by several containers.</div>
<form [formGroup]="form">
<mat-form-field appearance="outline" class="mid-width">
@@ -15,6 +16,7 @@
</form>
<button data-cy="volume-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new volume" (click)="create()">Create</button>
<button *ngIf="!volume" data-cy="volume-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new volume" (click)="create()">Create</button>
<button *ngIf="volume" data-cy="volume-save" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="save volume" (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 { Volume } from 'src/app/api-gen';
import { TelemetryService } from 'src/app/services/telemetry.service';
@@ -12,8 +12,11 @@ import { DevstateService } from 'src/app/services/devstate.service';
})
export class VolumeComponent {
@Input() cancelable: boolean = false;
@Input() volume: Volume | undefined;
@Output() canceled = new EventEmitter<void>();
@Output() created = new EventEmitter<Volume>();
@Output() saved = new EventEmitter<Volume>();
form: FormGroup;
@@ -33,7 +36,28 @@ export class VolumeComponent {
this.created.emit(this.form.value);
}
save() {
const newValue = this.form.value;
newValue.name = this.volume?.name;
this.telemetry.track("[ui] edit volume");
this.saved.emit(this.form.value);
}
cancel() {
this.canceled.emit();
}
ngOnChanges(changes: SimpleChanges) {
if (!changes['volume']) {
return;
}
const vol = changes['volume'].currentValue;
if (vol == undefined) {
this.form.get('name')?.enable();
} else {
this.form.reset();
this.form.patchValue(vol);
this.form.get('name')?.disable();
}
}
}

View File

@@ -63,6 +63,13 @@ export class DevstateService {
});
}
saveVolume(volume: Volume): Observable<DevfileContent> {
return this.http.patch<DevfileContent>(this.base+"/volume/"+volume.name, {
ephemeral: volume.ephemeral,
size: volume.size,
});
}
addExecCommand(name: string, cmd: ExecCommand): Observable<DevfileContent> {
return this.http.post<DevfileContent>(this.base+"/execCommand", {
name: name,

View File

@@ -19,19 +19,22 @@
<mat-card-actions>
<button mat-button color="warn" (click)="delete(volume.name)">Delete</button>
<button data-cy="volume-edit" mat-button (click)="edit(volume)">Edit</button>
</mat-card-actions>
</mat-card>
<app-volume
*ngIf="forceDisplayAdd || volumes == undefined || volumes.length == 0"
[cancelable]="forceDisplayAdd"
*ngIf="forceDisplayForm || volumes == undefined || volumes.length == 0"
[cancelable]="forceDisplayForm"
(canceled)="undisplayAddForm()"
(created)="onCreated($event)"
[volume]="editingVolume"
(saved)="onSaved($event)"
></app-volume>
</div>
<ng-container *ngIf="!forceDisplayAdd && volumes != undefined && volumes.length > 0">
<ng-container *ngIf="!forceDisplayForm && volumes != undefined && volumes.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 { StateService } from 'src/app/services/state.service';
})
export class VolumesComponent {
forceDisplayAdd: boolean = false;
forceDisplayForm: boolean = false;
volumes: Volume[] | undefined = [];
editingVolume: Volume | undefined;
constructor(
private state: StateService,
@@ -25,19 +26,24 @@ export class VolumesComponent {
if (this.volumes == null) {
return
}
that.forceDisplayAdd = false;
that.forceDisplayForm = false;
});
}
displayAddForm() {
this.forceDisplayAdd = true;
this.editingVolume = 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 VolumesComponent {
}
}
edit(volume: Volume) {
this.editingVolume = volume;
this.displayForm();
}
onCreated(volume: Volume) {
const result = this.devstate.addVolume(volume);
result.subscribe({
@@ -66,6 +77,18 @@ export class VolumesComponent {
});
}
onSaved(volume: Volume) {
const result = this.devstate.saveVolume(volume);
result.subscribe({
next: value => {
this.state.changeDevfileYaml(value);
},
error: error => {
alert(error.error.message);
}
});
}
scrollToBottom() {
window.scrollTo(0,document.body.scrollHeight);
}

View File

@@ -1,6 +1,6 @@
{
"/api/v1": {
"target": "http://localhost:20000",
"target": "http://127.0.0.1:20000",
"secure": false
}
}