}
}
+ 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()
});
}
+ 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();
describe('Services page', () => {
const services = new ServicesPageHelper();
+ const serviceName = 'rgw.foo';
beforeEach(() => {
cy.login();
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', () => {
});
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', () => {
/**
* 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');
+ }
}
/**
path: URLVerbs.CREATE,
component: ServiceFormComponent,
outlet: 'modal'
+ },
+ {
+ path: `${URLVerbs.EDIT}/:type/:name`,
+ component: ServiceFormComponent,
+ outlet: 'modal'
}
]
},
<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">
<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"
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();
+ });
+ });
});
});
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';
@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;
private poolService: PoolService,
private router: Router,
private taskWrapperService: TaskWrapperService,
+ private route: ActivatedRoute,
public activeModal: NgbActiveModal
) {
super();
}
ngOnInit(): void {
- if (this.router.url.includes('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.action = this.actionLabels.CREATE;
this.cephServiceService.getKnownTypes().subscribe((resp: Array<string>) => {
// Remove service types:
// osd - This is deployed a different way.
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>) => {
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: {},
}
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':
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':
}
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)
@Input() hasDetails = true;
- @Input() modal = true;
+ @Input() routedModal = true;
permissions: Permissions;
tableActions: CdTableAction[];
orchStatus: OrchestratorStatus;
actionOrchFeatures = {
create: [OrchestratorFeature.SERVICE_CREATE],
+ update: [OrchestratorFeature.SERVICE_EDIT],
delete: [OrchestratorFeature.SERVICE_DELETE]
};
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,
];
}
- 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' });
}
}
}
}
- 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]
SERVICE_LIST = 'describe_service',
SERVICE_CREATE = 'apply',
+ SERVICE_EDIT = 'apply',
SERVICE_DELETE = 'remove_service',
SERVICE_RELOAD = 'service_action',
DAEMON_LIST = 'list_daemons',
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;
}
'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)
)
SERVICE_LIST = 'describe_service'
SERVICE_CREATE = 'apply'
+ SERVICE_EDIT = 'apply'
SERVICE_DELETE = 'remove_service'
SERVICE_RELOAD = 'service_action'
DAEMON_LIST = 'list_daemons'