mirror of
https://github.com/ceph/ceph
synced 2025-02-23 19:17:37 +00:00
Merge pull request #43903 from rhcs-dashboard/edit-service-feature
mgr/dashboard: Edit a service feature Reviewed-by: Avan Thakkar <athakkar@redhat.com> Reviewed-by: Ernesto Puerta <epuertat@redhat.com> Reviewed-by: Nizamudeen A <nia@redhat.com> Reviewed-by: Pere Diaz Bou <pdiazbou@redhat.com>
This commit is contained in:
commit
689c213d1a
@ -70,6 +70,16 @@ export class ServicesPageHelper extends PageHelper {
|
||||
}
|
||||
}
|
||||
|
||||
editService(name: string, count: string) {
|
||||
this.navigateEdit(name, true, false);
|
||||
cy.get(`${this.pages.create.id}`).within(() => {
|
||||
cy.get('#service_type').should('be.disabled');
|
||||
cy.get('#service_id').should('be.disabled');
|
||||
cy.get('#count').clear().type(count);
|
||||
cy.get('cd-submit-button').click();
|
||||
});
|
||||
}
|
||||
|
||||
checkServiceStatus(daemon: string) {
|
||||
this.getTableCell(this.serviceDetailColumnIndex.daemonType, daemon)
|
||||
.parent()
|
||||
@ -80,6 +90,16 @@ export class ServicesPageHelper extends PageHelper {
|
||||
});
|
||||
}
|
||||
|
||||
expectPlacementCount(serviceName: string, expectedCount: string) {
|
||||
this.getTableCell(this.columnIndex.service_name, serviceName)
|
||||
.parent()
|
||||
.find(`datatable-body-cell:nth-child(${this.columnIndex.placement})`)
|
||||
.should(($ele) => {
|
||||
const running = $ele.text().split(';');
|
||||
expect(running).to.include(`count:${expectedCount}`);
|
||||
});
|
||||
}
|
||||
|
||||
checkExist(serviceName: string, exist: boolean) {
|
||||
this.getTableCell(this.columnIndex.service_name, serviceName).should(($elements) => {
|
||||
const services = $elements.map((_, el) => el.textContent).get();
|
||||
|
@ -2,6 +2,7 @@ import { ServicesPageHelper } from '../cluster/services.po';
|
||||
|
||||
describe('Services page', () => {
|
||||
const services = new ServicesPageHelper();
|
||||
const serviceName = 'rgw.foo';
|
||||
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
@ -14,7 +15,13 @@ describe('Services page', () => {
|
||||
services.navigateTo('create');
|
||||
services.addService('rgw');
|
||||
|
||||
services.checkExist('rgw.foo', true);
|
||||
services.checkExist(serviceName, true);
|
||||
});
|
||||
|
||||
it('should edit a service', () => {
|
||||
const count = '2';
|
||||
services.editService(serviceName, count);
|
||||
services.expectPlacementCount(serviceName, count);
|
||||
});
|
||||
|
||||
it('should create and delete an ingress service', () => {
|
||||
|
@ -20,11 +20,19 @@ describe('Create cluster create services page', () => {
|
||||
});
|
||||
|
||||
describe('when Orchestrator is available', () => {
|
||||
const serviceName = 'rgw.foo';
|
||||
|
||||
it('should create an rgw service', () => {
|
||||
cy.get('.btn.btn-accent').first().click({ force: true });
|
||||
|
||||
createClusterServicePage.addService('rgw', false, '3');
|
||||
createClusterServicePage.checkExist('rgw.foo', true);
|
||||
createClusterServicePage.addService('rgw', false, '2');
|
||||
createClusterServicePage.checkExist(serviceName, true);
|
||||
});
|
||||
|
||||
it('should edit a service', () => {
|
||||
const count = '3';
|
||||
createClusterServicePage.editService(serviceName, count);
|
||||
createClusterServicePage.expectPlacementCount(serviceName, count);
|
||||
});
|
||||
|
||||
it('should create and delete an ingress service', () => {
|
||||
|
@ -52,14 +52,16 @@ export abstract class PageHelper {
|
||||
/**
|
||||
* Navigates to the edit page
|
||||
*/
|
||||
navigateEdit(name: string, select = true) {
|
||||
navigateEdit(name: string, select = true, breadcrumb = true) {
|
||||
if (select) {
|
||||
this.navigateTo();
|
||||
this.getFirstTableCell(name).click();
|
||||
}
|
||||
cy.contains('Creating...').should('not.exist');
|
||||
cy.contains('button', 'Edit').click();
|
||||
this.expectBreadcrumbText('Edit');
|
||||
if (breadcrumb) {
|
||||
this.expectBreadcrumbText('Edit');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -139,6 +139,11 @@ const routes: Routes = [
|
||||
path: URLVerbs.CREATE,
|
||||
component: ServiceFormComponent,
|
||||
outlet: 'modal'
|
||||
},
|
||||
{
|
||||
path: `${URLVerbs.EDIT}/:type/:name`,
|
||||
component: ServiceFormComponent,
|
||||
outlet: 'modal'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -65,7 +65,7 @@
|
||||
<cd-services [hasDetails]="false"
|
||||
[hiddenServices]="['mon', 'mgr', 'crash', 'agent']"
|
||||
[hiddenColumns]="['status.running', 'status.size', 'status.last_refresh']"
|
||||
[modal]="false"></cd-services>
|
||||
[routedModal]="false"></cd-services>
|
||||
</div>
|
||||
<div *ngSwitchCase="'4'"
|
||||
class="ml-5">
|
||||
|
@ -215,10 +215,10 @@
|
||||
<option *ngIf="pools === null"
|
||||
[ngValue]="null"
|
||||
i18n>Loading...</option>
|
||||
<option *ngIf="pools !== null && pools.length === 0"
|
||||
<option *ngIf="pools && pools.length === 0"
|
||||
[ngValue]="null"
|
||||
i18n>-- No pools available --</option>
|
||||
<option *ngIf="pools !== null && pools.length > 0"
|
||||
<option *ngIf="pools && pools.length > 0"
|
||||
[ngValue]="null"
|
||||
i18n>-- Select a pool --</option>
|
||||
<option *ngFor="let pool of pools"
|
||||
|
@ -433,5 +433,22 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
|
||||
formHelper.expectError('monitor_port', 'pattern');
|
||||
});
|
||||
});
|
||||
|
||||
describe('check edit fields', () => {
|
||||
beforeEach(() => {
|
||||
component.editing = true;
|
||||
});
|
||||
|
||||
it('should check whether edit field is correctly loaded', () => {
|
||||
const cephServiceSpy = spyOn(cephServiceService, 'list').and.callThrough();
|
||||
component.ngOnInit();
|
||||
expect(cephServiceSpy).toBeCalledTimes(2);
|
||||
expect(component.action).toBe('Edit');
|
||||
const serviceType = fixture.debugElement.query(By.css('#service_type')).nativeElement;
|
||||
const serviceId = fixture.debugElement.query(By.css('#service_id')).nativeElement;
|
||||
expect(serviceType.disabled).toBeTruthy();
|
||||
expect(serviceId.disabled).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Component, Input, OnInit, ViewChild } from '@angular/core';
|
||||
import { AbstractControl, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { NgbActiveModal, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
|
||||
import _ from 'lodash';
|
||||
@ -31,7 +31,13 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
@ViewChild(NgbTypeahead, { static: false })
|
||||
typeahead: NgbTypeahead;
|
||||
|
||||
@Input() public hiddenServices: string[] = [];
|
||||
@Input() hiddenServices: string[] = [];
|
||||
|
||||
@Input() editing = false;
|
||||
|
||||
@Input() serviceName: string;
|
||||
|
||||
@Input() serviceType: string;
|
||||
|
||||
serviceForm: CdFormGroup;
|
||||
action: string;
|
||||
@ -53,6 +59,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
private poolService: PoolService,
|
||||
private router: Router,
|
||||
private taskWrapperService: TaskWrapperService,
|
||||
private route: ActivatedRoute,
|
||||
public activeModal: NgbActiveModal
|
||||
) {
|
||||
super();
|
||||
@ -213,10 +220,17 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.router.url.includes('services')) {
|
||||
this.pageURL = 'services';
|
||||
}
|
||||
this.action = this.actionLabels.CREATE;
|
||||
if (this.router.url.includes('services/(modal:create')) {
|
||||
this.pageURL = 'services';
|
||||
} else if (this.router.url.includes('services/(modal:edit')) {
|
||||
this.editing = true;
|
||||
this.pageURL = 'services';
|
||||
this.route.params.subscribe((params: { type: string; name: string }) => {
|
||||
this.serviceName = params.name;
|
||||
this.serviceType = params.type;
|
||||
});
|
||||
}
|
||||
this.cephServiceService.getKnownTypes().subscribe((resp: Array<string>) => {
|
||||
// Remove service types:
|
||||
// osd - This is deployed a different way.
|
||||
@ -244,6 +258,79 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
this.cephServiceService.list().subscribe((services: CephServiceSpec[]) => {
|
||||
this.services = services.filter((service: any) => service.service_type === 'rgw');
|
||||
});
|
||||
|
||||
if (this.editing) {
|
||||
this.action = this.actionLabels.EDIT;
|
||||
this.disableForEditing(this.serviceType);
|
||||
this.cephServiceService.list(this.serviceName).subscribe((response: CephServiceSpec[]) => {
|
||||
const formKeys = ['service_type', 'service_id', 'unmanaged'];
|
||||
formKeys.forEach((keys) => {
|
||||
this.serviceForm.get(keys).setValue(response[0][keys]);
|
||||
});
|
||||
if (!response[0]['unmanaged']) {
|
||||
const placementKey = Object.keys(response[0]['placement'])[0];
|
||||
let placementValue: string;
|
||||
['hosts', 'label'].indexOf(placementKey) >= 0
|
||||
? (placementValue = placementKey)
|
||||
: (placementValue = 'hosts');
|
||||
this.serviceForm.get('placement').setValue(placementValue);
|
||||
this.serviceForm.get('count').setValue(response[0]['placement']['count']);
|
||||
if (response[0]?.placement[placementValue]) {
|
||||
this.serviceForm.get(placementValue).setValue(response[0]?.placement[placementValue]);
|
||||
}
|
||||
}
|
||||
switch (this.serviceType) {
|
||||
case 'iscsi':
|
||||
const specKeys = ['pool', 'api_password', 'api_user', 'trusted_ip_list', 'api_port'];
|
||||
specKeys.forEach((key) => {
|
||||
this.serviceForm.get(key).setValue(response[0].spec[key]);
|
||||
});
|
||||
this.serviceForm.get('ssl').setValue(response[0].spec?.api_secure);
|
||||
if (response[0].spec?.api_secure) {
|
||||
this.serviceForm.get('ssl_cert').setValue(response[0].spec?.ssl_cert);
|
||||
this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key);
|
||||
}
|
||||
break;
|
||||
case 'rgw':
|
||||
this.serviceForm.get('rgw_frontend_port').setValue(response[0].spec?.rgw_frontend_port);
|
||||
this.serviceForm.get('ssl').setValue(response[0].spec?.ssl);
|
||||
if (response[0].spec?.ssl) {
|
||||
this.serviceForm
|
||||
.get('ssl_cert')
|
||||
.setValue(response[0].spec?.rgw_frontend_ssl_certificate);
|
||||
}
|
||||
break;
|
||||
case 'ingress':
|
||||
const ingressSpecKeys = [
|
||||
'backend_service',
|
||||
'virtual_ip',
|
||||
'frontend_port',
|
||||
'monitor_port',
|
||||
'virtual_interface_networks',
|
||||
'ssl'
|
||||
];
|
||||
ingressSpecKeys.forEach((key) => {
|
||||
this.serviceForm.get(key).setValue(response[0].spec[key]);
|
||||
});
|
||||
if (response[0].spec?.ssl) {
|
||||
this.serviceForm.get('ssl_cert').setValue(response[0].spec?.ssl_cert);
|
||||
this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
disableForEditing(serviceType: string) {
|
||||
const disableForEditKeys = ['service_type', 'service_id'];
|
||||
disableForEditKeys.forEach((key) => {
|
||||
this.serviceForm.get(key).disable();
|
||||
});
|
||||
switch (serviceType) {
|
||||
case 'ingress':
|
||||
this.serviceForm.get('backend_service').disable();
|
||||
}
|
||||
}
|
||||
|
||||
searchLabels = (text$: Observable<string>) => {
|
||||
@ -282,8 +369,12 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
|
||||
onSubmit() {
|
||||
const self = this;
|
||||
const values: object = this.serviceForm.value;
|
||||
const values: object = this.serviceForm.getRawValue();
|
||||
const serviceType: string = values['service_type'];
|
||||
let taskUrl = `service/${URLVerbs.CREATE}`;
|
||||
if (this.editing) {
|
||||
taskUrl = `service/${URLVerbs.EDIT}`;
|
||||
}
|
||||
const serviceSpec: object = {
|
||||
service_type: serviceType,
|
||||
placement: {},
|
||||
@ -327,7 +418,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
}
|
||||
serviceSpec['ssl'] = values['ssl'];
|
||||
if (values['ssl']) {
|
||||
serviceSpec['rgw_frontend_ssl_certificate'] = values['ssl_cert'].trim();
|
||||
serviceSpec['rgw_frontend_ssl_certificate'] = values['ssl_cert']?.trim();
|
||||
}
|
||||
break;
|
||||
case 'iscsi':
|
||||
@ -342,8 +433,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
serviceSpec['api_password'] = values['api_password'];
|
||||
serviceSpec['api_secure'] = values['ssl'];
|
||||
if (values['ssl']) {
|
||||
serviceSpec['ssl_cert'] = values['ssl_cert'].trim();
|
||||
serviceSpec['ssl_key'] = values['ssl_key'].trim();
|
||||
serviceSpec['ssl_cert'] = values['ssl_cert']?.trim();
|
||||
serviceSpec['ssl_key'] = values['ssl_key']?.trim();
|
||||
}
|
||||
break;
|
||||
case 'ingress':
|
||||
@ -360,16 +451,17 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
}
|
||||
serviceSpec['ssl'] = values['ssl'];
|
||||
if (values['ssl']) {
|
||||
serviceSpec['ssl_cert'] = values['ssl_cert'].trim();
|
||||
serviceSpec['ssl_key'] = values['ssl_key'].trim();
|
||||
serviceSpec['ssl_cert'] = values['ssl_cert']?.trim();
|
||||
serviceSpec['ssl_key'] = values['ssl_key']?.trim();
|
||||
}
|
||||
serviceSpec['virtual_interface_networks'] = values['virtual_interface_networks'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.taskWrapperService
|
||||
.wrapTaskAroundCall({
|
||||
task: new FinishedTask(`service/${URLVerbs.CREATE}`, {
|
||||
task: new FinishedTask(taskUrl, {
|
||||
service_name: serviceName
|
||||
}),
|
||||
call: this.cephServiceService.create(serviceSpec)
|
||||
|
@ -49,7 +49,7 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
|
||||
|
||||
@Input() hasDetails = true;
|
||||
|
||||
@Input() modal = true;
|
||||
@Input() routedModal = true;
|
||||
|
||||
permissions: Permissions;
|
||||
tableActions: CdTableAction[];
|
||||
@ -59,6 +59,7 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
|
||||
orchStatus: OrchestratorStatus;
|
||||
actionOrchFeatures = {
|
||||
create: [OrchestratorFeature.SERVICE_CREATE],
|
||||
update: [OrchestratorFeature.SERVICE_EDIT],
|
||||
delete: [OrchestratorFeature.SERVICE_DELETE]
|
||||
};
|
||||
|
||||
@ -88,6 +89,13 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
|
||||
canBePrimary: (selection: CdTableSelection) => !selection.hasSelection,
|
||||
disable: (selection: CdTableSelection) => this.getDisable('create', selection)
|
||||
},
|
||||
{
|
||||
permission: 'update',
|
||||
icon: Icons.edit,
|
||||
click: () => this.openModal(true),
|
||||
name: this.actionLabels.EDIT,
|
||||
disable: (selection: CdTableSelection) => this.getDisable('update', selection)
|
||||
},
|
||||
{
|
||||
permission: 'delete',
|
||||
icon: Icons.destroy,
|
||||
@ -98,12 +106,36 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
|
||||
];
|
||||
}
|
||||
|
||||
openModal() {
|
||||
if (this.modal) {
|
||||
this.router.navigate([BASE_URL, { outlets: { modal: [URLVerbs.CREATE] } }]);
|
||||
openModal(edit = false) {
|
||||
if (this.routedModal) {
|
||||
edit
|
||||
? this.router.navigate([
|
||||
BASE_URL,
|
||||
{
|
||||
outlets: {
|
||||
modal: [
|
||||
URLVerbs.EDIT,
|
||||
this.selection.first().service_type,
|
||||
this.selection.first().service_name
|
||||
]
|
||||
}
|
||||
}
|
||||
])
|
||||
: this.router.navigate([BASE_URL, { outlets: { modal: [URLVerbs.CREATE] } }]);
|
||||
} else {
|
||||
this.bsModalRef = this.modalService.show(ServiceFormComponent);
|
||||
this.bsModalRef.componentInstance.hiddenServices = this.hiddenServices;
|
||||
let initialState = {};
|
||||
edit
|
||||
? (initialState = {
|
||||
serviceName: this.selection.first()?.service_name,
|
||||
serviceType: this.selection?.first()?.service_type,
|
||||
hiddenServices: this.hiddenServices,
|
||||
editing: edit
|
||||
})
|
||||
: (initialState = {
|
||||
hiddenServices: this.hiddenServices,
|
||||
editing: edit
|
||||
});
|
||||
this.bsModalRef = this.modalService.show(ServiceFormComponent, initialState, { size: 'lg' });
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,12 +187,21 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
|
||||
}
|
||||
}
|
||||
|
||||
getDisable(action: 'create' | 'delete', selection: CdTableSelection): boolean | string {
|
||||
getDisable(
|
||||
action: 'create' | 'update' | 'delete',
|
||||
selection: CdTableSelection
|
||||
): boolean | string {
|
||||
if (action === 'delete') {
|
||||
if (!selection?.hasSingleSelection) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (action === 'update') {
|
||||
const disableEditServices = ['osd', 'container'];
|
||||
if (disableEditServices.indexOf(this.selection.first()?.service_type) >= 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return this.orchService.getTableActionDisableDesc(
|
||||
this.orchStatus,
|
||||
this.actionOrchFeatures[action]
|
||||
|
@ -10,6 +10,7 @@ export enum OrchestratorFeature {
|
||||
|
||||
SERVICE_LIST = 'describe_service',
|
||||
SERVICE_CREATE = 'apply',
|
||||
SERVICE_EDIT = 'apply',
|
||||
SERVICE_DELETE = 'remove_service',
|
||||
SERVICE_RELOAD = 'service_action',
|
||||
DAEMON_LIST = 'list_daemons',
|
||||
|
@ -14,4 +14,32 @@ export interface CephServiceSpec {
|
||||
service_id: string;
|
||||
unmanaged: boolean;
|
||||
status: CephServiceStatus;
|
||||
spec: CephServiceAdditionalSpec;
|
||||
placement: CephServicePlacement;
|
||||
}
|
||||
|
||||
export interface CephServiceAdditionalSpec {
|
||||
backend_service: string;
|
||||
api_user: string;
|
||||
api_password: string;
|
||||
api_port: number;
|
||||
api_secure: boolean;
|
||||
rgw_frontend_port: number;
|
||||
trusted_ip_list: string[];
|
||||
virtual_ip: string;
|
||||
frontend_port: number;
|
||||
monitor_port: number;
|
||||
virtual_interface_networks: string[];
|
||||
pool: string;
|
||||
rgw_frontend_ssl_certificate: string;
|
||||
ssl: boolean;
|
||||
ssl_cert: string;
|
||||
ssl_key: string;
|
||||
}
|
||||
|
||||
export interface CephServicePlacement {
|
||||
count: number;
|
||||
placement: string;
|
||||
hosts: string[];
|
||||
label: string;
|
||||
}
|
||||
|
@ -334,6 +334,9 @@ export class TaskMessageService {
|
||||
'service/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
|
||||
this.service(metadata)
|
||||
),
|
||||
'service/edit': this.newTaskMessage(this.commonOperations.update, (metadata) =>
|
||||
this.service(metadata)
|
||||
),
|
||||
'service/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) =>
|
||||
this.service(metadata)
|
||||
)
|
||||
|
@ -201,6 +201,7 @@ class OrchFeature(object):
|
||||
|
||||
SERVICE_LIST = 'describe_service'
|
||||
SERVICE_CREATE = 'apply'
|
||||
SERVICE_EDIT = 'apply'
|
||||
SERVICE_DELETE = 'remove_service'
|
||||
SERVICE_RELOAD = 'service_action'
|
||||
DAEMON_LIST = 'list_daemons'
|
||||
|
Loading…
Reference in New Issue
Block a user