From: Tiago Melo Date: Tue, 10 Mar 2020 13:22:49 +0000 (-0100) Subject: mgr/dashboard: Append "Modal" to all modal components names X-Git-Tag: v15.1.1~16^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=refs%2Fpull%2F33858%2Fhead;p=ceph.git mgr/dashboard: Append "Modal" to all modal components names Fixes: https://tracker.ceph.com/issues/44547 Signed-off-by: Tiago Melo --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts index 217bd98e5575..c592e29386b7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts @@ -32,9 +32,9 @@ import { RbdDetailsComponent } from './rbd-details/rbd-details.component'; import { RbdFormComponent } from './rbd-form/rbd-form.component'; import { RbdImagesComponent } from './rbd-images/rbd-images.component'; import { RbdListComponent } from './rbd-list/rbd-list.component'; -import { RbdNamespaceFormComponent } from './rbd-namespace-form/rbd-namespace-form.component'; +import { RbdNamespaceFormModalComponent } from './rbd-namespace-form/rbd-namespace-form-modal.component'; import { RbdNamespaceListComponent } from './rbd-namespace-list/rbd-namespace-list.component'; -import { RbdSnapshotFormComponent } from './rbd-snapshot-form/rbd-snapshot-form.component'; +import { RbdSnapshotFormModalComponent } from './rbd-snapshot-form/rbd-snapshot-form-modal.component'; import { RbdSnapshotListComponent } from './rbd-snapshot-list/rbd-snapshot-list.component'; import { RbdTrashListComponent } from './rbd-trash-list/rbd-trash-list.component'; import { RbdTrashMoveModalComponent } from './rbd-trash-move-modal/rbd-trash-move-modal.component'; @@ -44,8 +44,8 @@ import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal/rbd-tra @NgModule({ entryComponents: [ RbdDetailsComponent, - RbdNamespaceFormComponent, - RbdSnapshotFormComponent, + RbdNamespaceFormModalComponent, + RbdSnapshotFormModalComponent, RbdTrashMoveModalComponent, RbdTrashRestoreModalComponent, RbdTrashPurgeModalComponent, @@ -78,10 +78,10 @@ import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal/rbd-tra IscsiTargetListComponent, RbdDetailsComponent, RbdFormComponent, - RbdNamespaceFormComponent, + RbdNamespaceFormModalComponent, RbdNamespaceListComponent, RbdSnapshotListComponent, - RbdSnapshotFormComponent, + RbdSnapshotFormModalComponent, RbdTrashListComponent, RbdTrashMoveModalComponent, RbdImagesComponent, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.html new file mode 100644 index 000000000000..fb0d3650d0d0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.html @@ -0,0 +1,85 @@ + + Create Namespace + + +
+ + + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.spec.ts new file mode 100644 index 000000000000..f7760157c05f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.spec.ts @@ -0,0 +1,41 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; +import { ToastrModule } from 'ngx-toastr'; + +import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper'; +import { ApiModule } from '../../../shared/api/api.module'; +import { ComponentsModule } from '../../../shared/components/components.module'; +import { AuthStorageService } from '../../../shared/services/auth-storage.service'; +import { RbdNamespaceFormModalComponent } from './rbd-namespace-form-modal.component'; + +describe('RbdNamespaceFormModalComponent', () => { + let component: RbdNamespaceFormModalComponent; + let fixture: ComponentFixture; + + configureTestBed({ + imports: [ + ReactiveFormsModule, + ComponentsModule, + HttpClientTestingModule, + ApiModule, + ToastrModule.forRoot(), + RouterTestingModule + ], + declarations: [RbdNamespaceFormModalComponent], + providers: [BsModalRef, BsModalService, AuthStorageService, i18nProviders] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RbdNamespaceFormModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.ts new file mode 100644 index 000000000000..e25f00667154 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.ts @@ -0,0 +1,147 @@ +import { Component, OnInit } from '@angular/core'; +import { + AbstractControl, + AsyncValidatorFn, + FormControl, + ValidationErrors, + ValidatorFn +} from '@angular/forms'; + +import { I18n } from '@ngx-translate/i18n-polyfill'; +import { BsModalRef } from 'ngx-bootstrap/modal'; +import { Subject } from 'rxjs'; + +import { PoolService } from '../../../shared/api/pool.service'; +import { RbdService } from '../../../shared/api/rbd.service'; +import { NotificationType } from '../../../shared/enum/notification-type.enum'; +import { CdFormGroup } from '../../../shared/forms/cd-form-group'; +import { FinishedTask } from '../../../shared/models/finished-task'; +import { Permission } from '../../../shared/models/permissions'; +import { AuthStorageService } from '../../../shared/services/auth-storage.service'; +import { NotificationService } from '../../../shared/services/notification.service'; +import { Pool } from '../../pool/pool'; + +@Component({ + selector: 'cd-rbd-namespace-form-modal', + templateUrl: './rbd-namespace-form-modal.component.html', + styleUrls: ['./rbd-namespace-form-modal.component.scss'] +}) +export class RbdNamespaceFormModalComponent implements OnInit { + poolPermission: Permission; + pools: Array = null; + pool: string; + namespace: string; + + namespaceForm: CdFormGroup; + + editing = false; + + public onSubmit: Subject; + + constructor( + public modalRef: BsModalRef, + private authStorageService: AuthStorageService, + private notificationService: NotificationService, + private poolService: PoolService, + private rbdService: RbdService, + private i18n: I18n + ) { + this.poolPermission = this.authStorageService.getPermissions().pool; + this.createForm(); + } + + createForm() { + this.namespaceForm = new CdFormGroup( + { + pool: new FormControl(''), + namespace: new FormControl('') + }, + this.validator(), + this.asyncValidator() + ); + } + + validator(): ValidatorFn { + return (control: AbstractControl) => { + const poolCtrl = control.get('pool'); + const namespaceCtrl = control.get('namespace'); + let poolErrors = null; + if (!poolCtrl.value) { + poolErrors = { required: true }; + } + poolCtrl.setErrors(poolErrors); + let namespaceErrors = null; + if (!namespaceCtrl.value) { + namespaceErrors = { required: true }; + } + namespaceCtrl.setErrors(namespaceErrors); + return null; + }; + } + + asyncValidator(): AsyncValidatorFn { + return (control: AbstractControl): Promise => { + return new Promise((resolve) => { + const poolCtrl = control.get('pool'); + const namespaceCtrl = control.get('namespace'); + this.rbdService.listNamespaces(poolCtrl.value).subscribe((namespaces: any[]) => { + if (namespaces.some((ns) => ns.namespace === namespaceCtrl.value)) { + const error = { namespaceExists: true }; + namespaceCtrl.setErrors(error); + resolve(error); + } else { + resolve(null); + } + }); + }); + }; + } + + ngOnInit() { + this.onSubmit = new Subject(); + + if (this.poolPermission.read) { + this.poolService.list(['pool_name', 'type', 'application_metadata']).then((resp) => { + const pools: Pool[] = []; + for (const pool of resp) { + if (this.rbdService.isRBDPool(pool) && pool.type === 'replicated') { + pools.push(pool); + } + } + this.pools = pools; + if (this.pools.length === 1) { + const poolName = this.pools[0]['pool_name']; + this.namespaceForm.get('pool').setValue(poolName); + } + }); + } + } + + submit() { + const pool = this.namespaceForm.getValue('pool'); + const namespace = this.namespaceForm.getValue('namespace'); + const finishedTask = new FinishedTask(); + finishedTask.name = 'rbd/namespace/create'; + finishedTask.metadata = { + pool: pool, + namespace: namespace + }; + this.rbdService + .createNamespace(pool, namespace) + .toPromise() + .then(() => { + this.notificationService.show( + NotificationType.success, + this.i18n(`Created namespace '{{pool}}/{{namespace}}'`, { + pool: pool, + namespace: namespace + }) + ); + this.modalRef.hide(); + this.onSubmit.next(); + }) + .catch(() => { + this.namespaceForm.setErrors({ cdSubmitButton: true }); + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.html deleted file mode 100644 index fb0d3650d0d0..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.html +++ /dev/null @@ -1,85 +0,0 @@ - - Create Namespace - - -
- - - -
-
-
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.spec.ts deleted file mode 100644 index c24381017c00..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; -import { ToastrModule } from 'ngx-toastr'; - -import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper'; -import { ApiModule } from '../../../shared/api/api.module'; -import { ComponentsModule } from '../../../shared/components/components.module'; -import { AuthStorageService } from '../../../shared/services/auth-storage.service'; -import { RbdNamespaceFormComponent } from './rbd-namespace-form.component'; - -describe('RbdNamespaceFormComponent', () => { - let component: RbdNamespaceFormComponent; - let fixture: ComponentFixture; - - configureTestBed({ - imports: [ - ReactiveFormsModule, - ComponentsModule, - HttpClientTestingModule, - ApiModule, - ToastrModule.forRoot(), - RouterTestingModule - ], - declarations: [RbdNamespaceFormComponent], - providers: [BsModalRef, BsModalService, AuthStorageService, i18nProviders] - }); - - beforeEach(() => { - fixture = TestBed.createComponent(RbdNamespaceFormComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.ts deleted file mode 100644 index acaf334a203d..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { - AbstractControl, - AsyncValidatorFn, - FormControl, - ValidationErrors, - ValidatorFn -} from '@angular/forms'; - -import { I18n } from '@ngx-translate/i18n-polyfill'; -import { BsModalRef } from 'ngx-bootstrap/modal'; -import { Subject } from 'rxjs'; - -import { PoolService } from '../../../shared/api/pool.service'; -import { RbdService } from '../../../shared/api/rbd.service'; -import { NotificationType } from '../../../shared/enum/notification-type.enum'; -import { CdFormGroup } from '../../../shared/forms/cd-form-group'; -import { FinishedTask } from '../../../shared/models/finished-task'; -import { Permission } from '../../../shared/models/permissions'; -import { AuthStorageService } from '../../../shared/services/auth-storage.service'; -import { NotificationService } from '../../../shared/services/notification.service'; -import { Pool } from '../../pool/pool'; - -@Component({ - selector: 'cd-rbd-namespace-form', - templateUrl: './rbd-namespace-form.component.html', - styleUrls: ['./rbd-namespace-form.component.scss'] -}) -export class RbdNamespaceFormComponent implements OnInit { - poolPermission: Permission; - pools: Array = null; - pool: string; - namespace: string; - - namespaceForm: CdFormGroup; - - editing = false; - - public onSubmit: Subject; - - constructor( - public modalRef: BsModalRef, - private authStorageService: AuthStorageService, - private notificationService: NotificationService, - private poolService: PoolService, - private rbdService: RbdService, - private i18n: I18n - ) { - this.poolPermission = this.authStorageService.getPermissions().pool; - this.createForm(); - } - - createForm() { - this.namespaceForm = new CdFormGroup( - { - pool: new FormControl(''), - namespace: new FormControl('') - }, - this.validator(), - this.asyncValidator() - ); - } - - validator(): ValidatorFn { - return (control: AbstractControl) => { - const poolCtrl = control.get('pool'); - const namespaceCtrl = control.get('namespace'); - let poolErrors = null; - if (!poolCtrl.value) { - poolErrors = { required: true }; - } - poolCtrl.setErrors(poolErrors); - let namespaceErrors = null; - if (!namespaceCtrl.value) { - namespaceErrors = { required: true }; - } - namespaceCtrl.setErrors(namespaceErrors); - return null; - }; - } - - asyncValidator(): AsyncValidatorFn { - return (control: AbstractControl): Promise => { - return new Promise((resolve) => { - const poolCtrl = control.get('pool'); - const namespaceCtrl = control.get('namespace'); - this.rbdService.listNamespaces(poolCtrl.value).subscribe((namespaces: any[]) => { - if (namespaces.some((ns) => ns.namespace === namespaceCtrl.value)) { - const error = { namespaceExists: true }; - namespaceCtrl.setErrors(error); - resolve(error); - } else { - resolve(null); - } - }); - }); - }; - } - - ngOnInit() { - this.onSubmit = new Subject(); - - if (this.poolPermission.read) { - this.poolService.list(['pool_name', 'type', 'application_metadata']).then((resp) => { - const pools: Pool[] = []; - for (const pool of resp) { - if (this.rbdService.isRBDPool(pool) && pool.type === 'replicated') { - pools.push(pool); - } - } - this.pools = pools; - if (this.pools.length === 1) { - const poolName = this.pools[0]['pool_name']; - this.namespaceForm.get('pool').setValue(poolName); - } - }); - } - } - - submit() { - const pool = this.namespaceForm.getValue('pool'); - const namespace = this.namespaceForm.getValue('namespace'); - const finishedTask = new FinishedTask(); - finishedTask.name = 'rbd/namespace/create'; - finishedTask.metadata = { - pool: pool, - namespace: namespace - }; - this.rbdService - .createNamespace(pool, namespace) - .toPromise() - .then(() => { - this.notificationService.show( - NotificationType.success, - this.i18n(`Created namespace '{{pool}}/{{namespace}}'`, { - pool: pool, - namespace: namespace - }) - ); - this.modalRef.hide(); - this.onSubmit.next(); - }) - .catch(() => { - this.namespaceForm.setErrors({ cdSubmitButton: true }); - }); - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.ts index 3dd514ee4616..80b0945f65d5 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.ts @@ -18,7 +18,7 @@ import { Permission } from '../../../shared/models/permissions'; import { AuthStorageService } from '../../../shared/services/auth-storage.service'; import { NotificationService } from '../../../shared/services/notification.service'; import { TaskListService } from '../../../shared/services/task-list.service'; -import { RbdNamespaceFormComponent } from '../rbd-namespace-form/rbd-namespace-form.component'; +import { RbdNamespaceFormModalComponent } from '../rbd-namespace-form/rbd-namespace-form-modal.component'; @Component({ selector: 'cd-rbd-namespace-list', @@ -119,7 +119,7 @@ export class RbdNamespaceListComponent implements OnInit { } createModal() { - this.modalRef = this.modalService.show(RbdNamespaceFormComponent); + this.modalRef = this.modalService.show(RbdNamespaceFormModalComponent); this.modalRef.content.onSubmit.subscribe(() => { this.refresh(); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.html new file mode 100644 index 000000000000..9a32a3ce887d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.html @@ -0,0 +1,45 @@ + + {{ action | titlecase }} {{ resource | upperFirst }} + + +
+ + + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.spec.ts new file mode 100644 index 000000000000..8feb20ae343e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.spec.ts @@ -0,0 +1,64 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; +import { ToastrModule } from 'ngx-toastr'; + +import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper'; +import { ApiModule } from '../../../shared/api/api.module'; +import { ComponentsModule } from '../../../shared/components/components.module'; +import { PipesModule } from '../../../shared/pipes/pipes.module'; +import { AuthStorageService } from '../../../shared/services/auth-storage.service'; +import { RbdSnapshotFormModalComponent } from './rbd-snapshot-form-modal.component'; + +describe('RbdSnapshotFormModalComponent', () => { + let component: RbdSnapshotFormModalComponent; + let fixture: ComponentFixture; + + configureTestBed({ + imports: [ + ReactiveFormsModule, + ComponentsModule, + PipesModule, + HttpClientTestingModule, + ApiModule, + ToastrModule.forRoot(), + RouterTestingModule + ], + declarations: [RbdSnapshotFormModalComponent], + providers: [BsModalRef, BsModalService, AuthStorageService, i18nProviders] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RbdSnapshotFormModalComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should show "Create" text', () => { + fixture.detectChanges(); + + const header = fixture.debugElement.nativeElement.querySelector('h4'); + expect(header.textContent).toBe('Create RBD Snapshot'); + + const button = fixture.debugElement.nativeElement.querySelector('cd-submit-button'); + expect(button.textContent).toBe('Create RBD Snapshot'); + }); + + it('should show "Rename" text', () => { + component.setEditing(); + + fixture.detectChanges(); + + const header = fixture.debugElement.nativeElement.querySelector('h4'); + expect(header.textContent).toBe('Rename RBD Snapshot'); + + const button = fixture.debugElement.nativeElement.querySelector('cd-submit-button'); + expect(button.textContent).toBe('Rename RBD Snapshot'); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.ts new file mode 100644 index 000000000000..d3888074cee5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.ts @@ -0,0 +1,138 @@ +import { Component, OnInit } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; + +import { I18n } from '@ngx-translate/i18n-polyfill'; +import { BsModalRef } from 'ngx-bootstrap/modal'; +import { Subject } from 'rxjs'; + +import { RbdService } from '../../../shared/api/rbd.service'; +import { ActionLabelsI18n } from '../../../shared/constants/app.constants'; +import { CdFormGroup } from '../../../shared/forms/cd-form-group'; +import { FinishedTask } from '../../../shared/models/finished-task'; +import { ImageSpec } from '../../../shared/models/image-spec'; +import { NotificationService } from '../../../shared/services/notification.service'; +import { TaskManagerService } from '../../../shared/services/task-manager.service'; + +@Component({ + selector: 'cd-rbd-snapshot-form-modal', + templateUrl: './rbd-snapshot-form-modal.component.html', + styleUrls: ['./rbd-snapshot-form-modal.component.scss'] +}) +export class RbdSnapshotFormModalComponent implements OnInit { + poolName: string; + namespace: string; + imageName: string; + snapName: string; + + snapshotForm: CdFormGroup; + + editing = false; + action: string; + resource: string; + + public onSubmit: Subject; + + constructor( + public modalRef: BsModalRef, + private rbdService: RbdService, + private taskManagerService: TaskManagerService, + private notificationService: NotificationService, + private i18n: I18n, + private actionLabels: ActionLabelsI18n + ) { + this.action = this.actionLabels.CREATE; + this.resource = this.i18n('RBD Snapshot'); + this.createForm(); + } + + createForm() { + this.snapshotForm = new CdFormGroup({ + snapshotName: new FormControl('', { + validators: [Validators.required] + }) + }); + } + + ngOnInit() { + this.onSubmit = new Subject(); + } + + setSnapName(snapName: string) { + this.snapName = snapName; + this.snapshotForm.get('snapshotName').setValue(snapName); + } + + /** + * Set the 'editing' flag. If set to TRUE, the modal dialog is in + * 'Edit' mode, otherwise in 'Create' mode. + * @param {boolean} editing + */ + setEditing(editing: boolean = true) { + this.editing = editing; + this.action = this.editing ? this.actionLabels.RENAME : this.actionLabels.CREATE; + } + + editAction() { + const snapshotName = this.snapshotForm.getValue('snapshotName'); + const imageSpec = new ImageSpec(this.poolName, this.namespace, this.imageName); + const finishedTask = new FinishedTask(); + finishedTask.name = 'rbd/snap/edit'; + finishedTask.metadata = { + image_spec: imageSpec.toString(), + snapshot_name: snapshotName + }; + this.rbdService + .renameSnapshot(imageSpec, this.snapName, snapshotName) + .toPromise() + .then(() => { + this.taskManagerService.subscribe( + finishedTask.name, + finishedTask.metadata, + (asyncFinishedTask: FinishedTask) => { + this.notificationService.notifyTask(asyncFinishedTask); + } + ); + this.modalRef.hide(); + this.onSubmit.next(this.snapName); + }) + .catch(() => { + this.snapshotForm.setErrors({ cdSubmitButton: true }); + }); + } + + createAction() { + const snapshotName = this.snapshotForm.getValue('snapshotName'); + const imageSpec = new ImageSpec(this.poolName, this.namespace, this.imageName); + const finishedTask = new FinishedTask(); + finishedTask.name = 'rbd/snap/create'; + finishedTask.metadata = { + image_spec: imageSpec.toString(), + snapshot_name: snapshotName + }; + this.rbdService + .createSnapshot(imageSpec, snapshotName) + .toPromise() + .then(() => { + this.taskManagerService.subscribe( + finishedTask.name, + finishedTask.metadata, + (asyncFinishedTask: FinishedTask) => { + this.notificationService.notifyTask(asyncFinishedTask); + } + ); + this.modalRef.hide(); + this.onSubmit.next(snapshotName); + }) + .catch(() => { + this.snapshotForm.setErrors({ cdSubmitButton: true }); + }); + } + + submit() { + if (this.editing) { + this.editAction(); + } else { + this.createAction(); + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.html deleted file mode 100644 index 9a32a3ce887d..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.html +++ /dev/null @@ -1,45 +0,0 @@ - - {{ action | titlecase }} {{ resource | upperFirst }} - - -
- - - -
-
-
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.spec.ts deleted file mode 100644 index f75cc8fbbe54..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; -import { ToastrModule } from 'ngx-toastr'; - -import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper'; -import { ApiModule } from '../../../shared/api/api.module'; -import { ComponentsModule } from '../../../shared/components/components.module'; -import { PipesModule } from '../../../shared/pipes/pipes.module'; -import { AuthStorageService } from '../../../shared/services/auth-storage.service'; -import { RbdSnapshotFormComponent } from './rbd-snapshot-form.component'; - -describe('RbdSnapshotFormComponent', () => { - let component: RbdSnapshotFormComponent; - let fixture: ComponentFixture; - - configureTestBed({ - imports: [ - ReactiveFormsModule, - ComponentsModule, - PipesModule, - HttpClientTestingModule, - ApiModule, - ToastrModule.forRoot(), - RouterTestingModule - ], - declarations: [RbdSnapshotFormComponent], - providers: [BsModalRef, BsModalService, AuthStorageService, i18nProviders] - }); - - beforeEach(() => { - fixture = TestBed.createComponent(RbdSnapshotFormComponent); - component = fixture.componentInstance; - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should show "Create" text', () => { - fixture.detectChanges(); - - const header = fixture.debugElement.nativeElement.querySelector('h4'); - expect(header.textContent).toBe('Create RBD Snapshot'); - - const button = fixture.debugElement.nativeElement.querySelector('cd-submit-button'); - expect(button.textContent).toBe('Create RBD Snapshot'); - }); - - it('should show "Rename" text', () => { - component.setEditing(); - - fixture.detectChanges(); - - const header = fixture.debugElement.nativeElement.querySelector('h4'); - expect(header.textContent).toBe('Rename RBD Snapshot'); - - const button = fixture.debugElement.nativeElement.querySelector('cd-submit-button'); - expect(button.textContent).toBe('Rename RBD Snapshot'); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.ts deleted file mode 100644 index 0dcb9ad1f3ff..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { FormControl, Validators } from '@angular/forms'; - -import { I18n } from '@ngx-translate/i18n-polyfill'; -import { BsModalRef } from 'ngx-bootstrap/modal'; -import { Subject } from 'rxjs'; - -import { RbdService } from '../../../shared/api/rbd.service'; -import { ActionLabelsI18n } from '../../../shared/constants/app.constants'; -import { CdFormGroup } from '../../../shared/forms/cd-form-group'; -import { FinishedTask } from '../../../shared/models/finished-task'; -import { ImageSpec } from '../../../shared/models/image-spec'; -import { NotificationService } from '../../../shared/services/notification.service'; -import { TaskManagerService } from '../../../shared/services/task-manager.service'; - -@Component({ - selector: 'cd-rbd-snapshot-form', - templateUrl: './rbd-snapshot-form.component.html', - styleUrls: ['./rbd-snapshot-form.component.scss'] -}) -export class RbdSnapshotFormComponent implements OnInit { - poolName: string; - namespace: string; - imageName: string; - snapName: string; - - snapshotForm: CdFormGroup; - - editing = false; - action: string; - resource: string; - - public onSubmit: Subject; - - constructor( - public modalRef: BsModalRef, - private rbdService: RbdService, - private taskManagerService: TaskManagerService, - private notificationService: NotificationService, - private i18n: I18n, - private actionLabels: ActionLabelsI18n - ) { - this.action = this.actionLabels.CREATE; - this.resource = this.i18n('RBD Snapshot'); - this.createForm(); - } - - createForm() { - this.snapshotForm = new CdFormGroup({ - snapshotName: new FormControl('', { - validators: [Validators.required] - }) - }); - } - - ngOnInit() { - this.onSubmit = new Subject(); - } - - setSnapName(snapName: string) { - this.snapName = snapName; - this.snapshotForm.get('snapshotName').setValue(snapName); - } - - /** - * Set the 'editing' flag. If set to TRUE, the modal dialog is in - * 'Edit' mode, otherwise in 'Create' mode. - * @param {boolean} editing - */ - setEditing(editing: boolean = true) { - this.editing = editing; - this.action = this.editing ? this.actionLabels.RENAME : this.actionLabels.CREATE; - } - - editAction() { - const snapshotName = this.snapshotForm.getValue('snapshotName'); - const imageSpec = new ImageSpec(this.poolName, this.namespace, this.imageName); - const finishedTask = new FinishedTask(); - finishedTask.name = 'rbd/snap/edit'; - finishedTask.metadata = { - image_spec: imageSpec.toString(), - snapshot_name: snapshotName - }; - this.rbdService - .renameSnapshot(imageSpec, this.snapName, snapshotName) - .toPromise() - .then(() => { - this.taskManagerService.subscribe( - finishedTask.name, - finishedTask.metadata, - (asyncFinishedTask: FinishedTask) => { - this.notificationService.notifyTask(asyncFinishedTask); - } - ); - this.modalRef.hide(); - this.onSubmit.next(this.snapName); - }) - .catch(() => { - this.snapshotForm.setErrors({ cdSubmitButton: true }); - }); - } - - createAction() { - const snapshotName = this.snapshotForm.getValue('snapshotName'); - const imageSpec = new ImageSpec(this.poolName, this.namespace, this.imageName); - const finishedTask = new FinishedTask(); - finishedTask.name = 'rbd/snap/create'; - finishedTask.metadata = { - image_spec: imageSpec.toString(), - snapshot_name: snapshotName - }; - this.rbdService - .createSnapshot(imageSpec, snapshotName) - .toPromise() - .then(() => { - this.taskManagerService.subscribe( - finishedTask.name, - finishedTask.metadata, - (asyncFinishedTask: FinishedTask) => { - this.notificationService.notifyTask(asyncFinishedTask); - } - ); - this.modalRef.hide(); - this.onSubmit.next(snapshotName); - }) - .catch(() => { - this.snapshotForm.setErrors({ cdSubmitButton: true }); - }); - } - - submit() { - if (this.editing) { - this.editAction(); - } else { - this.createAction(); - } - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts index b85d15d92091..a65e82e45fc0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts @@ -26,7 +26,7 @@ import { AuthStorageService } from '../../../shared/services/auth-storage.servic import { NotificationService } from '../../../shared/services/notification.service'; import { SummaryService } from '../../../shared/services/summary.service'; import { TaskListService } from '../../../shared/services/task-list.service'; -import { RbdSnapshotFormComponent } from '../rbd-snapshot-form/rbd-snapshot-form.component'; +import { RbdSnapshotFormModalComponent } from '../rbd-snapshot-form/rbd-snapshot-form-modal.component'; import { RbdSnapshotListComponent } from './rbd-snapshot-list.component'; import { RbdSnapshotModel } from './rbd-snapshot.model'; @@ -187,7 +187,7 @@ describe('RbdSnapshotListComponent', () => { component.rbdName = 'image01'; spyOn(TestBed.get(BsModalService), 'show').and.callFake(() => { const ref = new BsModalRef(); - ref.content = new RbdSnapshotFormComponent( + ref.content = new RbdSnapshotFormModalComponent( null, null, null, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts index fa69acafd1b6..380677430e3b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts @@ -25,7 +25,7 @@ import { NotificationService } from '../../../shared/services/notification.servi import { SummaryService } from '../../../shared/services/summary.service'; import { TaskListService } from '../../../shared/services/task-list.service'; import { TaskManagerService } from '../../../shared/services/task-manager.service'; -import { RbdSnapshotFormComponent } from '../rbd-snapshot-form/rbd-snapshot-form.component'; +import { RbdSnapshotFormModalComponent } from '../rbd-snapshot-form/rbd-snapshot-form-modal.component'; import { RbdSnapshotActionsModel } from './rbd-snapshot-actions.model'; import { RbdSnapshotModel } from './rbd-snapshot.model'; @@ -169,7 +169,7 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges { } private openSnapshotModal(taskName: string, snapName: string = null) { - this.modalRef = this.modalService.show(RbdSnapshotFormComponent); + this.modalRef = this.modalService.show(RbdSnapshotFormModalComponent); this.modalRef.content.poolName = this.poolName; this.modalRef.content.imageName = this.rbdName; this.modalRef.content.namespace = this.namespace; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.html new file mode 100644 index 000000000000..985f6a3fa551 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.html @@ -0,0 +1,313 @@ + + {{ action | titlecase }} {{ resource | upperFirst }} + + +
+ + + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.spec.ts new file mode 100644 index 000000000000..0d4ce97a2101 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.spec.ts @@ -0,0 +1,332 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation'; +import { BsModalRef } from 'ngx-bootstrap/modal'; +import { ToastrModule } from 'ngx-toastr'; +import { of } from 'rxjs'; + +import { + configureTestBed, + FixtureHelper, + FormHelper, + i18nProviders +} from '../../../../testing/unit-test-helper'; +import { ErasureCodeProfileService } from '../../../shared/api/erasure-code-profile.service'; +import { ErasureCodeProfile } from '../../../shared/models/erasure-code-profile'; +import { TaskWrapperService } from '../../../shared/services/task-wrapper.service'; +import { PoolModule } from '../pool.module'; +import { ErasureCodeProfileFormModalComponent } from './erasure-code-profile-form-modal.component'; + +describe('ErasureCodeProfileFormModalComponent', () => { + let component: ErasureCodeProfileFormModalComponent; + let ecpService: ErasureCodeProfileService; + let fixture: ComponentFixture; + let formHelper: FormHelper; + let fixtureHelper: FixtureHelper; + let data: {}; + + configureTestBed({ + imports: [ + HttpClientTestingModule, + RouterTestingModule, + ToastrModule.forRoot(), + PoolModule, + NgBootstrapFormValidationModule.forRoot() + ], + providers: [ErasureCodeProfileService, BsModalRef, i18nProviders] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ErasureCodeProfileFormModalComponent); + fixtureHelper = new FixtureHelper(fixture); + component = fixture.componentInstance; + formHelper = new FormHelper(component.form); + ecpService = TestBed.get(ErasureCodeProfileService); + data = { + failure_domains: ['host', 'osd'], + plugins: ['isa', 'jerasure', 'shec', 'lrc'], + names: ['ecp1', 'ecp2'], + devices: ['ssd', 'hdd'] + }; + spyOn(ecpService, 'getInfo').and.callFake(() => of(data)); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('calls listing to get ecps on ngInit', () => { + expect(ecpService.getInfo).toHaveBeenCalled(); + expect(component.names.length).toBe(2); + }); + + describe('form validation', () => { + it(`isn't valid if name is not set`, () => { + expect(component.form.invalid).toBeTruthy(); + formHelper.setValue('name', 'someProfileName'); + expect(component.form.valid).toBeTruthy(); + }); + + it('sets name invalid', () => { + component.names = ['awesomeProfileName']; + formHelper.expectErrorChange('name', 'awesomeProfileName', 'uniqueName'); + formHelper.expectErrorChange('name', 'some invalid text', 'pattern'); + formHelper.expectErrorChange('name', null, 'required'); + }); + + it('sets k to min error', () => { + formHelper.expectErrorChange('k', 0, 'min'); + }); + + it('sets m to min error', () => { + formHelper.expectErrorChange('m', 0, 'min'); + }); + + it(`should show all default form controls`, () => { + const showDefaults = (plugin: string) => { + formHelper.setValue('plugin', plugin); + fixtureHelper.expectIdElementsVisible( + [ + 'name', + 'plugin', + 'k', + 'm', + 'crushFailureDomain', + 'crushRoot', + 'crushDeviceClass', + 'directory' + ], + true + ); + }; + showDefaults('jerasure'); + showDefaults('shec'); + showDefaults('lrc'); + showDefaults('isa'); + }); + + describe(`for 'jerasure' plugin (default)`, () => { + it(`requires 'm' and 'k'`, () => { + formHelper.expectErrorChange('k', null, 'required'); + formHelper.expectErrorChange('m', null, 'required'); + }); + + it(`should show 'packetSize' and 'technique'`, () => { + fixtureHelper.expectIdElementsVisible(['packetSize', 'technique'], true); + }); + + it(`should not show any other plugin specific form control`, () => { + fixtureHelper.expectIdElementsVisible(['c', 'l', 'crushLocality'], false); + }); + }); + + describe(`for 'isa' plugin`, () => { + beforeEach(() => { + formHelper.setValue('plugin', 'isa'); + }); + + it(`does not require 'm' and 'k'`, () => { + formHelper.setValue('k', null); + formHelper.expectValidChange('k', null); + formHelper.expectValidChange('m', null); + }); + + it(`should show 'technique'`, () => { + fixtureHelper.expectIdElementsVisible(['technique'], true); + expect(fixture.debugElement.query(By.css('#technique'))).toBeTruthy(); + }); + + it(`should not show any other plugin specific form control`, () => { + fixtureHelper.expectIdElementsVisible(['c', 'l', 'crushLocality', 'packetSize'], false); + }); + }); + + describe(`for 'lrc' plugin`, () => { + beforeEach(() => { + formHelper.setValue('plugin', 'lrc'); + }); + + it(`requires 'm', 'l' and 'k'`, () => { + formHelper.expectErrorChange('k', null, 'required'); + formHelper.expectErrorChange('m', null, 'required'); + }); + + it(`should show 'l' and 'crushLocality'`, () => { + fixtureHelper.expectIdElementsVisible(['l', 'crushLocality'], true); + }); + + it(`should not show any other plugin specific form control`, () => { + fixtureHelper.expectIdElementsVisible(['c', 'packetSize', 'technique'], false); + }); + }); + + describe(`for 'shec' plugin`, () => { + beforeEach(() => { + formHelper.setValue('plugin', 'shec'); + }); + + it(`does not require 'm' and 'k'`, () => { + formHelper.expectValidChange('k', null); + formHelper.expectValidChange('m', null); + }); + + it(`should show 'c'`, () => { + fixtureHelper.expectIdElementsVisible(['c'], true); + }); + + it(`should not show any other plugin specific form control`, () => { + fixtureHelper.expectIdElementsVisible( + ['l', 'crushLocality', 'packetSize', 'technique'], + false + ); + }); + }); + }); + + describe('submission', () => { + let ecp: ErasureCodeProfile; + + const testCreation = () => { + fixture.detectChanges(); + component.onSubmit(); + expect(ecpService.create).toHaveBeenCalledWith(ecp); + }; + + beforeEach(() => { + ecp = new ErasureCodeProfile(); + const taskWrapper = TestBed.get(TaskWrapperService); + spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough(); + spyOn(ecpService, 'create').and.stub(); + }); + + describe(`'jerasure' usage`, () => { + beforeEach(() => { + ecp.name = 'jerasureProfile'; + }); + + it('should be able to create a profile with only required fields', () => { + formHelper.setMultipleValues(ecp, true); + ecp.k = 4; + ecp.m = 2; + testCreation(); + }); + + it(`does not create with missing 'k' or invalid form`, () => { + ecp.k = 0; + formHelper.setMultipleValues(ecp, true); + component.onSubmit(); + expect(ecpService.create).not.toHaveBeenCalled(); + }); + + it('should be able to create a profile with m, k, name, directory and packetSize', () => { + ecp.m = 3; + ecp.directory = '/different/ecp/path'; + formHelper.setMultipleValues(ecp, true); + ecp.k = 4; + formHelper.setValue('packetSize', 8192, true); + ecp.packetsize = 8192; + testCreation(); + }); + + it('should not send the profile with unsupported fields', () => { + formHelper.setMultipleValues(ecp, true); + ecp.k = 4; + ecp.m = 2; + formHelper.setValue('crushLocality', 'osd', true); + testCreation(); + }); + }); + + describe(`'isa' usage`, () => { + beforeEach(() => { + ecp.name = 'isaProfile'; + ecp.plugin = 'isa'; + }); + + it('should be able to create a profile with only plugin and name', () => { + formHelper.setMultipleValues(ecp, true); + testCreation(); + }); + + it('should send profile with plugin, name, failure domain and technique only', () => { + ecp.technique = 'cauchy'; + formHelper.setMultipleValues(ecp, true); + formHelper.setValue('crushFailureDomain', 'osd', true); + ecp['crush-failure-domain'] = 'osd'; + testCreation(); + }); + + it('should not send the profile with unsupported fields', () => { + formHelper.setMultipleValues(ecp, true); + formHelper.setValue('packetSize', 'osd', true); + testCreation(); + }); + }); + + describe(`'lrc' usage`, () => { + beforeEach(() => { + ecp.name = 'lreProfile'; + ecp.plugin = 'lrc'; + }); + + it('should be able to create a profile with only required fields', () => { + formHelper.setMultipleValues(ecp, true); + ecp.k = 4; + ecp.m = 2; + ecp.l = 3; + testCreation(); + }); + + it('should send profile with all required fields and crush root and locality', () => { + ecp.l = 8; + formHelper.setMultipleValues(ecp, true); + ecp.k = 4; + ecp.m = 2; + formHelper.setValue('crushLocality', 'osd', true); + formHelper.setValue('crushRoot', 'rack', true); + ecp['crush-locality'] = 'osd'; + ecp['crush-root'] = 'rack'; + testCreation(); + }); + + it('should not send the profile with unsupported fields', () => { + formHelper.setMultipleValues(ecp, true); + ecp.k = 4; + ecp.m = 2; + ecp.l = 3; + formHelper.setValue('c', 4, true); + testCreation(); + }); + }); + + describe(`'shec' usage`, () => { + beforeEach(() => { + ecp.name = 'shecProfile'; + ecp.plugin = 'shec'; + }); + + it('should be able to create a profile with only plugin and name', () => { + formHelper.setMultipleValues(ecp, true); + testCreation(); + }); + + it('should send profile with plugin, name, c and crush device class only', () => { + ecp.c = 4; + formHelper.setMultipleValues(ecp, true); + formHelper.setValue('crushDeviceClass', 'ssd', true); + ecp['crush-device-class'] = 'ssd'; + testCreation(); + }); + + it('should not send the profile with unsupported fields', () => { + formHelper.setMultipleValues(ecp, true); + formHelper.setValue('l', 8, true); + testCreation(); + }); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.ts new file mode 100644 index 000000000000..6a62a5c87a56 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.ts @@ -0,0 +1,259 @@ +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { Validators } from '@angular/forms'; + +import { I18n } from '@ngx-translate/i18n-polyfill'; +import { BsModalRef } from 'ngx-bootstrap/modal'; + +import { ErasureCodeProfileService } from '../../../shared/api/erasure-code-profile.service'; +import { ActionLabelsI18n } from '../../../shared/constants/app.constants'; +import { CdFormBuilder } from '../../../shared/forms/cd-form-builder'; +import { CdFormGroup } from '../../../shared/forms/cd-form-group'; +import { CdValidators } from '../../../shared/forms/cd-validators'; +import { ErasureCodeProfile } from '../../../shared/models/erasure-code-profile'; +import { FinishedTask } from '../../../shared/models/finished-task'; +import { TaskWrapperService } from '../../../shared/services/task-wrapper.service'; + +@Component({ + selector: 'cd-erasure-code-profile-form-modal', + templateUrl: './erasure-code-profile-form-modal.component.html', + styleUrls: ['./erasure-code-profile-form-modal.component.scss'] +}) +export class ErasureCodeProfileFormModalComponent implements OnInit { + @Output() + submitAction = new EventEmitter(); + + form: CdFormGroup; + failureDomains: string[]; + plugins: string[]; + names: string[]; + techniques: string[]; + requiredControls: string[] = []; + devices: string[] = []; + tooltips = this.ecpService.formTooltips; + + PLUGIN = { + LRC: 'lrc', // Locally Repairable Erasure Code + SHEC: 'shec', // Shingled Erasure Code + JERASURE: 'jerasure', // default + ISA: 'isa' // Intel Storage Acceleration + }; + plugin = this.PLUGIN.JERASURE; + action: string; + resource: string; + + constructor( + private formBuilder: CdFormBuilder, + public bsModalRef: BsModalRef, + private taskWrapper: TaskWrapperService, + private ecpService: ErasureCodeProfileService, + private i18n: I18n, + public actionLabels: ActionLabelsI18n + ) { + this.action = this.actionLabels.CREATE; + this.resource = this.i18n('EC Profile'); + this.createForm(); + this.setJerasureDefaults(); + } + + createForm() { + this.form = this.formBuilder.group({ + name: [ + null, + [ + Validators.required, + Validators.pattern('[A-Za-z0-9_-]+'), + CdValidators.custom( + 'uniqueName', + (value: string) => this.names && this.names.indexOf(value) !== -1 + ) + ] + ], + plugin: [this.PLUGIN.JERASURE, [Validators.required]], + k: [1], // Will be replaced by plugin defaults + m: [1], // Will be replaced by plugin defaults + crushFailureDomain: ['host'], + crushRoot: ['default'], // default for all - is a list possible??? + crushDeviceClass: [''], // set none to empty at submit - get list from configs? + directory: [''], + // Only for 'jerasure' and 'isa' use + technique: ['reed_sol_van'], + // Only for 'jerasure' use + packetSize: [2048, [Validators.min(1)]], + // Only for 'lrc' use + l: [1, [Validators.required, Validators.min(1)]], + crushLocality: [''], // set to none at the end (same list as for failure domains) + // Only for 'shec' use + c: [1, [Validators.required, Validators.min(1)]] + }); + this.form.get('plugin').valueChanges.subscribe((plugin) => this.onPluginChange(plugin)); + } + + onPluginChange(plugin: string) { + this.plugin = plugin; + if (plugin === this.PLUGIN.JERASURE) { + this.setJerasureDefaults(); + } else if (plugin === this.PLUGIN.LRC) { + this.setLrcDefaults(); + } else if (plugin === this.PLUGIN.ISA) { + this.setIsaDefaults(); + } else if (plugin === this.PLUGIN.SHEC) { + this.setShecDefaults(); + } + } + + private setNumberValidators(name: string, required: boolean) { + const validators = [Validators.min(1)]; + if (required) { + validators.push(Validators.required); + } + this.form.get(name).setValidators(validators); + } + + private setKMValidators(required: boolean) { + ['k', 'm'].forEach((name) => this.setNumberValidators(name, required)); + } + + private setJerasureDefaults() { + this.requiredControls = ['k', 'm']; + this.setDefaults({ + k: 4, + m: 2 + }); + this.setKMValidators(true); + this.techniques = [ + 'reed_sol_van', + 'reed_sol_r6_op', + 'cauchy_orig', + 'cauchy_good', + 'liberation', + 'blaum_roth', + 'liber8tion' + ]; + } + + private setLrcDefaults() { + this.requiredControls = ['k', 'm', 'l']; + this.setKMValidators(true); + this.setNumberValidators('l', true); + this.setDefaults({ + k: 4, + m: 2, + l: 3 + }); + } + + private setIsaDefaults() { + this.requiredControls = []; + this.setKMValidators(false); + this.setDefaults({ + k: 7, + m: 3 + }); + this.techniques = ['reed_sol_van', 'cauchy']; + } + + private setShecDefaults() { + this.requiredControls = []; + this.setKMValidators(false); + this.setDefaults({ + k: 4, + m: 3, + c: 2 + }); + } + + private setDefaults(defaults: object) { + Object.keys(defaults).forEach((controlName) => { + if (this.form.get(controlName).pristine) { + this.form.silentSet(controlName, defaults[controlName]); + } + }); + } + + ngOnInit() { + this.ecpService + .getInfo() + .subscribe( + ({ + failure_domains, + plugins, + names, + directory, + devices + }: { + failure_domains: string[]; + plugins: string[]; + names: string[]; + directory: string; + devices: string[]; + }) => { + this.failureDomains = failure_domains; + this.plugins = plugins; + this.names = names; + this.devices = devices; + this.form.silentSet('directory', directory); + } + ); + } + + private createJson() { + const pluginControls = { + technique: [this.PLUGIN.ISA, this.PLUGIN.JERASURE], + packetSize: [this.PLUGIN.JERASURE], + l: [this.PLUGIN.LRC], + crushLocality: [this.PLUGIN.LRC], + c: [this.PLUGIN.SHEC] + }; + const ecp = new ErasureCodeProfile(); + const plugin = this.form.getValue('plugin'); + Object.keys(this.form.controls) + .filter((name) => { + const pluginControl = pluginControls[name]; + const control = this.form.get(name); + const usable = (pluginControl && pluginControl.includes(plugin)) || !pluginControl; + return ( + usable && + (control.dirty || this.requiredControls.includes(name)) && + this.form.getValue(name) + ); + }) + .forEach((name) => { + this.extendJson(name, ecp); + }); + return ecp; + } + + private extendJson(name: string, ecp: ErasureCodeProfile) { + const differentApiAttributes = { + crushFailureDomain: 'crush-failure-domain', + crushRoot: 'crush-root', + crushDeviceClass: 'crush-device-class', + packetSize: 'packetsize', + crushLocality: 'crush-locality' + }; + ecp[differentApiAttributes[name] || name] = this.form.getValue(name); + } + + onSubmit() { + if (this.form.invalid) { + this.form.setErrors({ cdSubmitButton: true }); + return; + } + const profile = this.createJson(); + this.taskWrapper + .wrapTaskAroundCall({ + task: new FinishedTask('ecp/create', { name: profile.name }), + call: this.ecpService.create(profile) + }) + .subscribe( + undefined, + () => { + this.form.setErrors({ cdSubmitButton: true }); + }, + () => { + this.bsModalRef.hide(); + this.submitAction.emit(profile); + } + ); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form.component.html deleted file mode 100644 index 985f6a3fa551..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form.component.html +++ /dev/null @@ -1,313 +0,0 @@ - - {{ action | titlecase }} {{ resource | upperFirst }} - - -
- - - -
-
-
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form.component.spec.ts deleted file mode 100644 index 0ef4e37184dd..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form.component.spec.ts +++ /dev/null @@ -1,332 +0,0 @@ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation'; -import { BsModalRef } from 'ngx-bootstrap/modal'; -import { ToastrModule } from 'ngx-toastr'; -import { of } from 'rxjs'; - -import { - configureTestBed, - FixtureHelper, - FormHelper, - i18nProviders -} from '../../../../testing/unit-test-helper'; -import { ErasureCodeProfileService } from '../../../shared/api/erasure-code-profile.service'; -import { ErasureCodeProfile } from '../../../shared/models/erasure-code-profile'; -import { TaskWrapperService } from '../../../shared/services/task-wrapper.service'; -import { PoolModule } from '../pool.module'; -import { ErasureCodeProfileFormComponent } from './erasure-code-profile-form.component'; - -describe('ErasureCodeProfileFormComponent', () => { - let component: ErasureCodeProfileFormComponent; - let ecpService: ErasureCodeProfileService; - let fixture: ComponentFixture; - let formHelper: FormHelper; - let fixtureHelper: FixtureHelper; - let data: {}; - - configureTestBed({ - imports: [ - HttpClientTestingModule, - RouterTestingModule, - ToastrModule.forRoot(), - PoolModule, - NgBootstrapFormValidationModule.forRoot() - ], - providers: [ErasureCodeProfileService, BsModalRef, i18nProviders] - }); - - beforeEach(() => { - fixture = TestBed.createComponent(ErasureCodeProfileFormComponent); - fixtureHelper = new FixtureHelper(fixture); - component = fixture.componentInstance; - formHelper = new FormHelper(component.form); - ecpService = TestBed.get(ErasureCodeProfileService); - data = { - failure_domains: ['host', 'osd'], - plugins: ['isa', 'jerasure', 'shec', 'lrc'], - names: ['ecp1', 'ecp2'], - devices: ['ssd', 'hdd'] - }; - spyOn(ecpService, 'getInfo').and.callFake(() => of(data)); - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('calls listing to get ecps on ngInit', () => { - expect(ecpService.getInfo).toHaveBeenCalled(); - expect(component.names.length).toBe(2); - }); - - describe('form validation', () => { - it(`isn't valid if name is not set`, () => { - expect(component.form.invalid).toBeTruthy(); - formHelper.setValue('name', 'someProfileName'); - expect(component.form.valid).toBeTruthy(); - }); - - it('sets name invalid', () => { - component.names = ['awesomeProfileName']; - formHelper.expectErrorChange('name', 'awesomeProfileName', 'uniqueName'); - formHelper.expectErrorChange('name', 'some invalid text', 'pattern'); - formHelper.expectErrorChange('name', null, 'required'); - }); - - it('sets k to min error', () => { - formHelper.expectErrorChange('k', 0, 'min'); - }); - - it('sets m to min error', () => { - formHelper.expectErrorChange('m', 0, 'min'); - }); - - it(`should show all default form controls`, () => { - const showDefaults = (plugin: string) => { - formHelper.setValue('plugin', plugin); - fixtureHelper.expectIdElementsVisible( - [ - 'name', - 'plugin', - 'k', - 'm', - 'crushFailureDomain', - 'crushRoot', - 'crushDeviceClass', - 'directory' - ], - true - ); - }; - showDefaults('jerasure'); - showDefaults('shec'); - showDefaults('lrc'); - showDefaults('isa'); - }); - - describe(`for 'jerasure' plugin (default)`, () => { - it(`requires 'm' and 'k'`, () => { - formHelper.expectErrorChange('k', null, 'required'); - formHelper.expectErrorChange('m', null, 'required'); - }); - - it(`should show 'packetSize' and 'technique'`, () => { - fixtureHelper.expectIdElementsVisible(['packetSize', 'technique'], true); - }); - - it(`should not show any other plugin specific form control`, () => { - fixtureHelper.expectIdElementsVisible(['c', 'l', 'crushLocality'], false); - }); - }); - - describe(`for 'isa' plugin`, () => { - beforeEach(() => { - formHelper.setValue('plugin', 'isa'); - }); - - it(`does not require 'm' and 'k'`, () => { - formHelper.setValue('k', null); - formHelper.expectValidChange('k', null); - formHelper.expectValidChange('m', null); - }); - - it(`should show 'technique'`, () => { - fixtureHelper.expectIdElementsVisible(['technique'], true); - expect(fixture.debugElement.query(By.css('#technique'))).toBeTruthy(); - }); - - it(`should not show any other plugin specific form control`, () => { - fixtureHelper.expectIdElementsVisible(['c', 'l', 'crushLocality', 'packetSize'], false); - }); - }); - - describe(`for 'lrc' plugin`, () => { - beforeEach(() => { - formHelper.setValue('plugin', 'lrc'); - }); - - it(`requires 'm', 'l' and 'k'`, () => { - formHelper.expectErrorChange('k', null, 'required'); - formHelper.expectErrorChange('m', null, 'required'); - }); - - it(`should show 'l' and 'crushLocality'`, () => { - fixtureHelper.expectIdElementsVisible(['l', 'crushLocality'], true); - }); - - it(`should not show any other plugin specific form control`, () => { - fixtureHelper.expectIdElementsVisible(['c', 'packetSize', 'technique'], false); - }); - }); - - describe(`for 'shec' plugin`, () => { - beforeEach(() => { - formHelper.setValue('plugin', 'shec'); - }); - - it(`does not require 'm' and 'k'`, () => { - formHelper.expectValidChange('k', null); - formHelper.expectValidChange('m', null); - }); - - it(`should show 'c'`, () => { - fixtureHelper.expectIdElementsVisible(['c'], true); - }); - - it(`should not show any other plugin specific form control`, () => { - fixtureHelper.expectIdElementsVisible( - ['l', 'crushLocality', 'packetSize', 'technique'], - false - ); - }); - }); - }); - - describe('submission', () => { - let ecp: ErasureCodeProfile; - - const testCreation = () => { - fixture.detectChanges(); - component.onSubmit(); - expect(ecpService.create).toHaveBeenCalledWith(ecp); - }; - - beforeEach(() => { - ecp = new ErasureCodeProfile(); - const taskWrapper = TestBed.get(TaskWrapperService); - spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough(); - spyOn(ecpService, 'create').and.stub(); - }); - - describe(`'jerasure' usage`, () => { - beforeEach(() => { - ecp.name = 'jerasureProfile'; - }); - - it('should be able to create a profile with only required fields', () => { - formHelper.setMultipleValues(ecp, true); - ecp.k = 4; - ecp.m = 2; - testCreation(); - }); - - it(`does not create with missing 'k' or invalid form`, () => { - ecp.k = 0; - formHelper.setMultipleValues(ecp, true); - component.onSubmit(); - expect(ecpService.create).not.toHaveBeenCalled(); - }); - - it('should be able to create a profile with m, k, name, directory and packetSize', () => { - ecp.m = 3; - ecp.directory = '/different/ecp/path'; - formHelper.setMultipleValues(ecp, true); - ecp.k = 4; - formHelper.setValue('packetSize', 8192, true); - ecp.packetsize = 8192; - testCreation(); - }); - - it('should not send the profile with unsupported fields', () => { - formHelper.setMultipleValues(ecp, true); - ecp.k = 4; - ecp.m = 2; - formHelper.setValue('crushLocality', 'osd', true); - testCreation(); - }); - }); - - describe(`'isa' usage`, () => { - beforeEach(() => { - ecp.name = 'isaProfile'; - ecp.plugin = 'isa'; - }); - - it('should be able to create a profile with only plugin and name', () => { - formHelper.setMultipleValues(ecp, true); - testCreation(); - }); - - it('should send profile with plugin, name, failure domain and technique only', () => { - ecp.technique = 'cauchy'; - formHelper.setMultipleValues(ecp, true); - formHelper.setValue('crushFailureDomain', 'osd', true); - ecp['crush-failure-domain'] = 'osd'; - testCreation(); - }); - - it('should not send the profile with unsupported fields', () => { - formHelper.setMultipleValues(ecp, true); - formHelper.setValue('packetSize', 'osd', true); - testCreation(); - }); - }); - - describe(`'lrc' usage`, () => { - beforeEach(() => { - ecp.name = 'lreProfile'; - ecp.plugin = 'lrc'; - }); - - it('should be able to create a profile with only required fields', () => { - formHelper.setMultipleValues(ecp, true); - ecp.k = 4; - ecp.m = 2; - ecp.l = 3; - testCreation(); - }); - - it('should send profile with all required fields and crush root and locality', () => { - ecp.l = 8; - formHelper.setMultipleValues(ecp, true); - ecp.k = 4; - ecp.m = 2; - formHelper.setValue('crushLocality', 'osd', true); - formHelper.setValue('crushRoot', 'rack', true); - ecp['crush-locality'] = 'osd'; - ecp['crush-root'] = 'rack'; - testCreation(); - }); - - it('should not send the profile with unsupported fields', () => { - formHelper.setMultipleValues(ecp, true); - ecp.k = 4; - ecp.m = 2; - ecp.l = 3; - formHelper.setValue('c', 4, true); - testCreation(); - }); - }); - - describe(`'shec' usage`, () => { - beforeEach(() => { - ecp.name = 'shecProfile'; - ecp.plugin = 'shec'; - }); - - it('should be able to create a profile with only plugin and name', () => { - formHelper.setMultipleValues(ecp, true); - testCreation(); - }); - - it('should send profile with plugin, name, c and crush device class only', () => { - ecp.c = 4; - formHelper.setMultipleValues(ecp, true); - formHelper.setValue('crushDeviceClass', 'ssd', true); - ecp['crush-device-class'] = 'ssd'; - testCreation(); - }); - - it('should not send the profile with unsupported fields', () => { - formHelper.setMultipleValues(ecp, true); - formHelper.setValue('l', 8, true); - testCreation(); - }); - }); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form.component.ts deleted file mode 100644 index 614902e0c8be..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form.component.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { Component, EventEmitter, OnInit, Output } from '@angular/core'; -import { Validators } from '@angular/forms'; - -import { I18n } from '@ngx-translate/i18n-polyfill'; -import { BsModalRef } from 'ngx-bootstrap/modal'; - -import { ErasureCodeProfileService } from '../../../shared/api/erasure-code-profile.service'; -import { ActionLabelsI18n } from '../../../shared/constants/app.constants'; -import { CdFormBuilder } from '../../../shared/forms/cd-form-builder'; -import { CdFormGroup } from '../../../shared/forms/cd-form-group'; -import { CdValidators } from '../../../shared/forms/cd-validators'; -import { ErasureCodeProfile } from '../../../shared/models/erasure-code-profile'; -import { FinishedTask } from '../../../shared/models/finished-task'; -import { TaskWrapperService } from '../../../shared/services/task-wrapper.service'; - -@Component({ - selector: 'cd-erasure-code-profile-form', - templateUrl: './erasure-code-profile-form.component.html', - styleUrls: ['./erasure-code-profile-form.component.scss'] -}) -export class ErasureCodeProfileFormComponent implements OnInit { - @Output() - submitAction = new EventEmitter(); - - form: CdFormGroup; - failureDomains: string[]; - plugins: string[]; - names: string[]; - techniques: string[]; - requiredControls: string[] = []; - devices: string[] = []; - tooltips = this.ecpService.formTooltips; - - PLUGIN = { - LRC: 'lrc', // Locally Repairable Erasure Code - SHEC: 'shec', // Shingled Erasure Code - JERASURE: 'jerasure', // default - ISA: 'isa' // Intel Storage Acceleration - }; - plugin = this.PLUGIN.JERASURE; - action: string; - resource: string; - - constructor( - private formBuilder: CdFormBuilder, - public bsModalRef: BsModalRef, - private taskWrapper: TaskWrapperService, - private ecpService: ErasureCodeProfileService, - private i18n: I18n, - public actionLabels: ActionLabelsI18n - ) { - this.action = this.actionLabels.CREATE; - this.resource = this.i18n('EC Profile'); - this.createForm(); - this.setJerasureDefaults(); - } - - createForm() { - this.form = this.formBuilder.group({ - name: [ - null, - [ - Validators.required, - Validators.pattern('[A-Za-z0-9_-]+'), - CdValidators.custom( - 'uniqueName', - (value: string) => this.names && this.names.indexOf(value) !== -1 - ) - ] - ], - plugin: [this.PLUGIN.JERASURE, [Validators.required]], - k: [1], // Will be replaced by plugin defaults - m: [1], // Will be replaced by plugin defaults - crushFailureDomain: ['host'], - crushRoot: ['default'], // default for all - is a list possible??? - crushDeviceClass: [''], // set none to empty at submit - get list from configs? - directory: [''], - // Only for 'jerasure' and 'isa' use - technique: ['reed_sol_van'], - // Only for 'jerasure' use - packetSize: [2048, [Validators.min(1)]], - // Only for 'lrc' use - l: [1, [Validators.required, Validators.min(1)]], - crushLocality: [''], // set to none at the end (same list as for failure domains) - // Only for 'shec' use - c: [1, [Validators.required, Validators.min(1)]] - }); - this.form.get('plugin').valueChanges.subscribe((plugin) => this.onPluginChange(plugin)); - } - - onPluginChange(plugin: string) { - this.plugin = plugin; - if (plugin === this.PLUGIN.JERASURE) { - this.setJerasureDefaults(); - } else if (plugin === this.PLUGIN.LRC) { - this.setLrcDefaults(); - } else if (plugin === this.PLUGIN.ISA) { - this.setIsaDefaults(); - } else if (plugin === this.PLUGIN.SHEC) { - this.setShecDefaults(); - } - } - - private setNumberValidators(name: string, required: boolean) { - const validators = [Validators.min(1)]; - if (required) { - validators.push(Validators.required); - } - this.form.get(name).setValidators(validators); - } - - private setKMValidators(required: boolean) { - ['k', 'm'].forEach((name) => this.setNumberValidators(name, required)); - } - - private setJerasureDefaults() { - this.requiredControls = ['k', 'm']; - this.setDefaults({ - k: 4, - m: 2 - }); - this.setKMValidators(true); - this.techniques = [ - 'reed_sol_van', - 'reed_sol_r6_op', - 'cauchy_orig', - 'cauchy_good', - 'liberation', - 'blaum_roth', - 'liber8tion' - ]; - } - - private setLrcDefaults() { - this.requiredControls = ['k', 'm', 'l']; - this.setKMValidators(true); - this.setNumberValidators('l', true); - this.setDefaults({ - k: 4, - m: 2, - l: 3 - }); - } - - private setIsaDefaults() { - this.requiredControls = []; - this.setKMValidators(false); - this.setDefaults({ - k: 7, - m: 3 - }); - this.techniques = ['reed_sol_van', 'cauchy']; - } - - private setShecDefaults() { - this.requiredControls = []; - this.setKMValidators(false); - this.setDefaults({ - k: 4, - m: 3, - c: 2 - }); - } - - private setDefaults(defaults: object) { - Object.keys(defaults).forEach((controlName) => { - if (this.form.get(controlName).pristine) { - this.form.silentSet(controlName, defaults[controlName]); - } - }); - } - - ngOnInit() { - this.ecpService - .getInfo() - .subscribe( - ({ - failure_domains, - plugins, - names, - directory, - devices - }: { - failure_domains: string[]; - plugins: string[]; - names: string[]; - directory: string; - devices: string[]; - }) => { - this.failureDomains = failure_domains; - this.plugins = plugins; - this.names = names; - this.devices = devices; - this.form.silentSet('directory', directory); - } - ); - } - - private createJson() { - const pluginControls = { - technique: [this.PLUGIN.ISA, this.PLUGIN.JERASURE], - packetSize: [this.PLUGIN.JERASURE], - l: [this.PLUGIN.LRC], - crushLocality: [this.PLUGIN.LRC], - c: [this.PLUGIN.SHEC] - }; - const ecp = new ErasureCodeProfile(); - const plugin = this.form.getValue('plugin'); - Object.keys(this.form.controls) - .filter((name) => { - const pluginControl = pluginControls[name]; - const control = this.form.get(name); - const usable = (pluginControl && pluginControl.includes(plugin)) || !pluginControl; - return ( - usable && - (control.dirty || this.requiredControls.includes(name)) && - this.form.getValue(name) - ); - }) - .forEach((name) => { - this.extendJson(name, ecp); - }); - return ecp; - } - - private extendJson(name: string, ecp: ErasureCodeProfile) { - const differentApiAttributes = { - crushFailureDomain: 'crush-failure-domain', - crushRoot: 'crush-root', - crushDeviceClass: 'crush-device-class', - packetSize: 'packetsize', - crushLocality: 'crush-locality' - }; - ecp[differentApiAttributes[name] || name] = this.form.getValue(name); - } - - onSubmit() { - if (this.form.invalid) { - this.form.setErrors({ cdSubmitButton: true }); - return; - } - const profile = this.createJson(); - this.taskWrapper - .wrapTaskAroundCall({ - task: new FinishedTask('ecp/create', { name: profile.name }), - call: this.ecpService.create(profile) - }) - .subscribe( - undefined, - () => { - this.form.setErrors({ cdSubmitButton: true }); - }, - () => { - this.bsModalRef.hide(); - this.submitAction.emit(profile); - } - ); - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts index 25edf591d335..d8ceabe1b1a0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts @@ -33,7 +33,7 @@ import { AuthStorageService } from '../../../shared/services/auth-storage.servic import { FormatterService } from '../../../shared/services/formatter.service'; import { TaskWrapperService } from '../../../shared/services/task-wrapper.service'; import { CrushRuleFormModalComponent } from '../crush-rule-form-modal/crush-rule-form-modal.component'; -import { ErasureCodeProfileFormComponent } from '../erasure-code-profile-form/erasure-code-profile-form.component'; +import { ErasureCodeProfileFormModalComponent } from '../erasure-code-profile-form/erasure-code-profile-form-modal.component'; import { Pool } from '../pool'; import { PoolFormData } from './pool-form-data'; @@ -542,7 +542,7 @@ export class PoolFormComponent implements OnInit { addErasureCodeProfile() { this.modalSubscription = this.modalService.onHide.subscribe(() => this.reloadECPs()); - this.bsModalService.show(ErasureCodeProfileFormComponent); + this.bsModalService.show(ErasureCodeProfileFormModalComponent); } private reloadECPs() { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.module.ts index ee768f8e60da..98fd4360a0e9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.module.ts @@ -14,7 +14,7 @@ import { SharedModule } from '../../shared/shared.module'; import { BlockModule } from '../block/block.module'; import { CephSharedModule } from '../shared/ceph-shared.module'; import { CrushRuleFormModalComponent } from './crush-rule-form-modal/crush-rule-form-modal.component'; -import { ErasureCodeProfileFormComponent } from './erasure-code-profile-form/erasure-code-profile-form.component'; +import { ErasureCodeProfileFormModalComponent } from './erasure-code-profile-form/erasure-code-profile-form-modal.component'; import { PoolDetailsComponent } from './pool-details/pool-details.component'; import { PoolFormComponent } from './pool-form/pool-form.component'; import { PoolListComponent } from './pool-list/pool-list.component'; @@ -37,11 +37,11 @@ import { PoolListComponent } from './pool-list/pool-list.component'; declarations: [ PoolListComponent, PoolFormComponent, - ErasureCodeProfileFormComponent, + ErasureCodeProfileFormModalComponent, CrushRuleFormModalComponent, PoolDetailsComponent ], - entryComponents: [CrushRuleFormModalComponent, ErasureCodeProfileFormComponent] + entryComponents: [CrushRuleFormModalComponent, ErasureCodeProfileFormModalComponent] }) export class PoolModule {}