]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Append "Modal" to all modal components names 33858/head
authorTiago Melo <tmelo@suse.com>
Tue, 10 Mar 2020 13:22:49 +0000 (12:22 -0100)
committerTiago Melo <tmelo@suse.com>
Thu, 12 Mar 2020 15:25:11 +0000 (14:25 -0100)
Fixes: https://tracker.ceph.com/issues/44547
Signed-off-by: Tiago Melo <tmelo@suse.com>
30 files changed:
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form-modal.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool.module.ts

index 217bd98e5575f0f57bdcf676f4612b439d2ec045..c592e29386b755631a09a4e24e2e052a8daad48a 100644 (file)
@@ -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 (file)
index 0000000..fb0d365
--- /dev/null
@@ -0,0 +1,85 @@
+<cd-modal [modalRef]="modalRef">
+  <ng-container class="modal-title"
+                i18n>Create Namespace</ng-container>
+
+  <ng-container class="modal-content">
+    <form name="namespaceForm"
+          #formDir="ngForm"
+          [formGroup]="namespaceForm"
+          novalidate>
+      <div class="modal-body">
+
+        <!-- Pool -->
+        <div class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="pool"
+                 i18n>Pool</label>
+          <div class="cd-col-form-input">
+            <input class="form-control"
+                   type="text"
+                   placeholder="Pool name..."
+                   id="pool"
+                   name="pool"
+                   formControlName="pool"
+                   *ngIf="!poolPermission.read">
+            <select id="pool"
+                    name="pool"
+                    class="form-control custom-select"
+                    formControlName="pool"
+                    *ngIf="poolPermission.read">
+              <option *ngIf="pools === null"
+                      [ngValue]="null"
+                      i18n>Loading...</option>
+              <option *ngIf="pools !== null && pools.length === 0"
+                      [ngValue]="null"
+                      i18n>-- No rbd pools available --</option>
+              <option *ngIf="pools !== null && pools.length > 0"
+                      [ngValue]="null"
+                      i18n>-- Select a pool --</option>
+              <option *ngFor="let pool of pools"
+                      [value]="pool.pool_name">{{ pool.pool_name }}</option>
+            </select>
+            <span *ngIf="namespaceForm.showError('pool', formDir, 'required')"
+                  class="invalid-feedback"
+                  i18n>This field is required.</span>
+          </div>
+        </div>
+
+        <!-- Name -->
+        <div class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="namespace"
+                 i18n>Name</label>
+          <div class="cd-col-form-input">
+            <input class="form-control"
+                   type="text"
+                   placeholder="Namespace name..."
+                   id="namespace"
+                   name="namespace"
+                   formControlName="namespace"
+                   autofocus>
+            <span class="invalid-feedback"
+                  *ngIf="namespaceForm.showError('namespace', formDir, 'required')"
+                  i18n>This field is required.</span>
+            <span class="invalid-feedback"
+                  *ngIf="namespaceForm.showError('namespace', formDir, 'namespaceExists')"
+                  i18n>Namespace already exists.</span>
+          </div>
+        </div>
+
+      </div>
+
+      <div class="modal-footer">
+        <div class="button-group text-right">
+          <cd-submit-button [form]="namespaceForm"
+                            (submitAction)="submit()"
+                            i18n>Create Namespace</cd-submit-button>
+          <cd-back-button [back]="modalRef.hide"
+                          name="Close"
+                          i18n-name>
+          </cd-back-button>
+        </div>
+      </div>
+    </form>
+  </ng-container>
+</cd-modal>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..f776015
--- /dev/null
@@ -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<RbdNamespaceFormModalComponent>;
+
+  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 (file)
index 0000000..e25f006
--- /dev/null
@@ -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<Pool> = null;
+  pool: string;
+  namespace: string;
+
+  namespaceForm: CdFormGroup;
+
+  editing = false;
+
+  public onSubmit: Subject<void>;
+
+  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<ValidationErrors | null> => {
+      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 (file)
index fb0d365..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-<cd-modal [modalRef]="modalRef">
-  <ng-container class="modal-title"
-                i18n>Create Namespace</ng-container>
-
-  <ng-container class="modal-content">
-    <form name="namespaceForm"
-          #formDir="ngForm"
-          [formGroup]="namespaceForm"
-          novalidate>
-      <div class="modal-body">
-
-        <!-- Pool -->
-        <div class="form-group row">
-          <label class="cd-col-form-label required"
-                 for="pool"
-                 i18n>Pool</label>
-          <div class="cd-col-form-input">
-            <input class="form-control"
-                   type="text"
-                   placeholder="Pool name..."
-                   id="pool"
-                   name="pool"
-                   formControlName="pool"
-                   *ngIf="!poolPermission.read">
-            <select id="pool"
-                    name="pool"
-                    class="form-control custom-select"
-                    formControlName="pool"
-                    *ngIf="poolPermission.read">
-              <option *ngIf="pools === null"
-                      [ngValue]="null"
-                      i18n>Loading...</option>
-              <option *ngIf="pools !== null && pools.length === 0"
-                      [ngValue]="null"
-                      i18n>-- No rbd pools available --</option>
-              <option *ngIf="pools !== null && pools.length > 0"
-                      [ngValue]="null"
-                      i18n>-- Select a pool --</option>
-              <option *ngFor="let pool of pools"
-                      [value]="pool.pool_name">{{ pool.pool_name }}</option>
-            </select>
-            <span *ngIf="namespaceForm.showError('pool', formDir, 'required')"
-                  class="invalid-feedback"
-                  i18n>This field is required.</span>
-          </div>
-        </div>
-
-        <!-- Name -->
-        <div class="form-group row">
-          <label class="cd-col-form-label required"
-                 for="namespace"
-                 i18n>Name</label>
-          <div class="cd-col-form-input">
-            <input class="form-control"
-                   type="text"
-                   placeholder="Namespace name..."
-                   id="namespace"
-                   name="namespace"
-                   formControlName="namespace"
-                   autofocus>
-            <span class="invalid-feedback"
-                  *ngIf="namespaceForm.showError('namespace', formDir, 'required')"
-                  i18n>This field is required.</span>
-            <span class="invalid-feedback"
-                  *ngIf="namespaceForm.showError('namespace', formDir, 'namespaceExists')"
-                  i18n>Namespace already exists.</span>
-          </div>
-        </div>
-
-      </div>
-
-      <div class="modal-footer">
-        <div class="button-group text-right">
-          <cd-submit-button [form]="namespaceForm"
-                            (submitAction)="submit()"
-                            i18n>Create Namespace</cd-submit-button>
-          <cd-back-button [back]="modalRef.hide"
-                          name="Close"
-                          i18n-name>
-          </cd-back-button>
-        </div>
-      </div>
-    </form>
-  </ng-container>
-</cd-modal>
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 (file)
index e69de29..0000000
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 (file)
index c243810..0000000
+++ /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<RbdNamespaceFormComponent>;
-
-  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 (file)
index acaf334..0000000
+++ /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<Pool> = null;
-  pool: string;
-  namespace: string;
-
-  namespaceForm: CdFormGroup;
-
-  editing = false;
-
-  public onSubmit: Subject<void>;
-
-  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<ValidationErrors | null> => {
-      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 });
-      });
-  }
-}
index 3dd514ee46166b905f5773e4a97340242c269a95..80b0945f65d58a402671534267f749fddcdaa9de 100644 (file)
@@ -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 (file)
index 0000000..9a32a3c
--- /dev/null
@@ -0,0 +1,45 @@
+<cd-modal [modalRef]="modalRef">
+  <ng-container i18n="form title|Example: Create rbdSnapshot@@formTitle"
+                class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+
+  <ng-container class="modal-content">
+    <form name="snapshotForm"
+          #formDir="ngForm"
+          [formGroup]="snapshotForm"
+          novalidate>
+      <div class="modal-body">
+        <!-- Name -->
+        <div class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="snapshotName"
+                 i18n>Name</label>
+          <div class="cd-col-form-input">
+            <input class="form-control"
+                   type="text"
+                   placeholder="Snapshot name..."
+                   id="snapshotName"
+                   name="snapshotName"
+                   formControlName="snapshotName"
+                   autofocus>
+            <span class="invalid-feedback"
+                  *ngIf="snapshotForm.showError('snapshotName', formDir, 'required')"
+                  i18n>This field is required.</span>
+          </div>
+        </div>
+      </div>
+
+      <div class="modal-footer">
+        <div class="button-group text-right">
+          <cd-submit-button [form]="snapshotForm"
+                            i18n="form action button|Example: Create rbdSnapshot@@formActionButton"
+                            (submitAction)="submit()">{{ action | titlecase }}
+            {{ resource | upperFirst }}</cd-submit-button>
+          <cd-back-button [back]="modalRef.hide"
+                          name="Close"
+                          i18n-name>
+          </cd-back-button>
+        </div>
+      </div>
+    </form>
+  </ng-container>
+</cd-modal>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..8feb20a
--- /dev/null
@@ -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<RbdSnapshotFormModalComponent>;
+
+  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 (file)
index 0000000..d388807
--- /dev/null
@@ -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<string>;
+
+  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 (file)
index 9a32a3c..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-<cd-modal [modalRef]="modalRef">
-  <ng-container i18n="form title|Example: Create rbdSnapshot@@formTitle"
-                class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
-
-  <ng-container class="modal-content">
-    <form name="snapshotForm"
-          #formDir="ngForm"
-          [formGroup]="snapshotForm"
-          novalidate>
-      <div class="modal-body">
-        <!-- Name -->
-        <div class="form-group row">
-          <label class="cd-col-form-label required"
-                 for="snapshotName"
-                 i18n>Name</label>
-          <div class="cd-col-form-input">
-            <input class="form-control"
-                   type="text"
-                   placeholder="Snapshot name..."
-                   id="snapshotName"
-                   name="snapshotName"
-                   formControlName="snapshotName"
-                   autofocus>
-            <span class="invalid-feedback"
-                  *ngIf="snapshotForm.showError('snapshotName', formDir, 'required')"
-                  i18n>This field is required.</span>
-          </div>
-        </div>
-      </div>
-
-      <div class="modal-footer">
-        <div class="button-group text-right">
-          <cd-submit-button [form]="snapshotForm"
-                            i18n="form action button|Example: Create rbdSnapshot@@formActionButton"
-                            (submitAction)="submit()">{{ action | titlecase }}
-            {{ resource | upperFirst }}</cd-submit-button>
-          <cd-back-button [back]="modalRef.hide"
-                          name="Close"
-                          i18n-name>
-          </cd-back-button>
-        </div>
-      </div>
-    </form>
-  </ng-container>
-</cd-modal>
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 (file)
index e69de29..0000000
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 (file)
index f75cc8f..0000000
+++ /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<RbdSnapshotFormComponent>;
-
-  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 (file)
index 0dcb9ad..0000000
+++ /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<string>;
-
-  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();
-    }
-  }
-}
index b85d15d92091cc85adb058c13a2c8aecfb0d0afe..a65e82e45fc036337b03f34ea75c35911ff615c9 100644 (file)
@@ -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,
index fa69acafd1b67cf0672fe252f418d5b1b68fc729..380677430e3b4c1bb792273bb76d8892b571aed2 100644 (file)
@@ -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 (file)
index 0000000..985f6a3
--- /dev/null
@@ -0,0 +1,313 @@
+<cd-modal [modalRef]="bsModalRef">
+  <ng-container i18n="form title|Example: Create Pool@@formTitle"
+                class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+
+  <ng-container class="modal-content">
+    <form #frm="ngForm"
+          [formGroup]="form"
+          novalidate>
+      <div class="modal-body">
+        <div class="form-group row">
+          <label class="cd-col-form-label"
+                 for="name"
+                 i18n>Name</label>
+          <div class="cd-col-form-input">
+            <input type="text"
+                   id="name"
+                   name="name"
+                   class="form-control"
+                   placeholder="Name..."
+                   formControlName="name"
+                   autofocus>
+            <span class="invalid-feedback"
+                  *ngIf="form.showError('name', frm, 'required')"
+                  i18n>This field is required!</span>
+            <span class="invalid-feedback"
+                  *ngIf="form.showError('name', frm, 'pattern')"
+                  i18n>The name can only consist of alphanumeric characters, dashes and underscores.</span>
+            <span class="invalid-feedback"
+                  *ngIf="form.showError('name', frm, 'uniqueName')"
+                  i18n>The chosen erasure code profile name is already in use.</span>
+          </div>
+        </div>
+
+        <div class="form-group row">
+          <label for="plugin"
+                 class="cd-col-form-label">
+            <span class="required"
+                  i18n>Plugin</span>
+            <cd-helper [html]="tooltips.plugins[plugin].description">
+            </cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <select class="form-control custom-select"
+                    id="plugin"
+                    name="plugin"
+                    formControlName="plugin">
+              <option *ngIf="!plugins"
+                      ngValue=""
+                      i18n>Loading...</option>
+              <option *ngFor="let plugin of plugins"
+                      [ngValue]="plugin">
+                {{ plugin }}
+              </option>
+            </select>
+            <span class="invalid-feedback"
+                  *ngIf="form.showError('name', frm, 'required')"
+                  i18n>This field is required!</span>
+          </div>
+        </div>
+
+        <div class="form-group row">
+          <label for="k"
+                 class="cd-col-form-label">
+            <span [ngClass]="{'required': requiredControls.includes('k')}"
+                  i18n>Data chunks (k)</span>
+            <cd-helper [html]="tooltips.k">
+            </cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <input type="number"
+                   id="k"
+                   name="k"
+                   class="form-control"
+                   ng-model="$ctrl.erasureCodeProfile.k"
+                   placeholder="Data chunks..."
+                   formControlName="k">
+            <span class="invalid-feedback"
+                  *ngIf="form.showError('k', frm, 'required')"
+                  i18n>This field is required!</span>
+            <span class="invalid-feedback"
+                  *ngIf="form.showError('k', frm, 'min')"
+                  i18n>Must be equal to or greater than 2.</span>
+          </div>
+        </div>
+
+        <div class="form-group row">
+          <label for="m"
+                 class="cd-col-form-label">
+            <span [ngClass]="{'required': requiredControls.includes('m')}"
+                  i18n>Coding chunks (m)</span>
+            <cd-helper [html]="tooltips.m">
+            </cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <input type="number"
+                   id="m"
+                   name="m"
+                   class="form-control"
+                   placeholder="Coding chunks..."
+                   formControlName="m">
+            <span class="invalid-feedback"
+                  *ngIf="form.showError('m', frm, 'required')"
+                  i18n>This field is required!</span>
+            <span class="invalid-feedback"
+                  *ngIf="form.showError('m', frm, 'min')"
+                  i18n>Must be equal to or greater than 1.</span>
+          </div>
+        </div>
+
+        <div class="form-group row"
+             *ngIf="plugin === 'shec'">
+          <label for="c"
+                 class="cd-col-form-label">
+            <ng-container i18n>Durability estimator (c)</ng-container>
+            <cd-helper [html]="tooltips.plugins.shec.c">
+            </cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <input type="number"
+                   id="c"
+                   name="c"
+                   class="form-control"
+                   placeholder="Coding chunks..."
+                   formControlName="c">
+            <span class="invalid-feedback"
+                  *ngIf="form.showError('c', frm, 'min')"
+                  i18n>Must be equal to or greater than 1.</span>
+          </div>
+        </div>
+
+        <div class="form-group row"
+             *ngIf="plugin === PLUGIN.LRC">
+          <label class="cd-col-form-label"
+                 for="l">
+            <span class="required"
+                  i18n>Locality (l)</span>
+            <cd-helper [html]="tooltips.plugins.lrc.l">
+            </cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <input type="number"
+                   id="l"
+                   name="l"
+                   class="form-control"
+                   placeholder="Coding chunks..."
+                   formControlName="l">
+            <span class="invalid-feedback"
+                  *ngIf="form.showError('l', frm, 'required')"
+                  i18n>This field is required!</span>
+            <span class="invalid-feedback"
+                  *ngIf="form.showError('l', frm, 'min')"
+                  i18n>Must be equal to or greater than 1.</span>
+          </div>
+        </div>
+
+        <div class="form-group row">
+          <label for="crushFailureDomain"
+                 class="cd-col-form-label">
+            <ng-container i18n>Crush failure domain</ng-container>
+            <cd-helper [html]="tooltips.crushFailureDomain">
+            </cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <select class="form-control custom-select"
+                    id="crushFailureDomain"
+                    name="crushFailureDomain"
+                    formControlName="crushFailureDomain">
+              <option *ngIf="!failureDomains"
+                      ngValue=""
+                      i18n>Loading...</option>
+              <option *ngFor="let domain of failureDomains"
+                      [ngValue]="domain">
+                {{ domain }}
+              </option>
+            </select>
+          </div>
+        </div>
+
+        <div class="form-group row"
+             *ngIf="plugin === PLUGIN.LRC">
+          <label for="crushLocality"
+                 class="cd-col-form-label">
+            <ng-container i18n>Crush Locality</ng-container>
+            <cd-helper [html]="tooltips.plugins.lrc.crushLocality">
+            </cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <select class="form-control custom-select"
+                    id="crushLocality"
+                    name="crushLocality"
+                    formControlName="crushLocality">
+              <option *ngIf="!failureDomains"
+                      ngValue=""
+                      i18n>Loading...</option>
+              <option *ngIf="failureDomains && failureDomains.length > 0"
+                      ngValue=""
+                      i18n>None</option>
+              <option *ngFor="let domain of failureDomains"
+                      [ngValue]="domain">
+                {{ domain }}
+              </option>
+            </select>
+          </div>
+        </div>
+
+        <div class="form-group row"
+             *ngIf="[PLUGIN.JERASURE, PLUGIN.ISA].includes(plugin)">
+          <label for="technique"
+                 class="cd-col-form-label">
+            <ng-container i18n>Technique</ng-container>
+            <cd-helper [html]="tooltips.plugins[plugin].technique">
+            </cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <select class="form-control custom-select"
+                    id="technique"
+                    name="technique"
+                    formControlName="technique">
+              <option *ngFor="let technique of techniques"
+                      [ngValue]="technique">
+                {{ technique }}
+              </option>
+            </select>
+          </div>
+        </div>
+
+        <div class="form-group row"
+             *ngIf="plugin === PLUGIN.JERASURE">
+          <label for="packetSize"
+                 class="cd-col-form-label">
+            <ng-container i18n>Packetsize</ng-container>
+            <cd-helper [html]="tooltips.plugins.jerasure.packetSize">
+            </cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <input type="number"
+                   id="packetSize"
+                   name="packetSize"
+                   class="form-control"
+                   placeholder="Packetsize..."
+                   formControlName="packetSize">
+            <span class="invalid-feedback"
+                  *ngIf="form.showError('packetSize', frm, 'min')"
+                  i18n>Must be equal to or greater than 1.</span>
+          </div>
+        </div>
+
+        <div class="form-group row">
+          <label for="crushRoot"
+                 class="cd-col-form-label">
+            <ng-container i18n>Crush root</ng-container>
+            <cd-helper [html]="tooltips.crushRoot">
+            </cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <input type="text"
+                   id="crushRoot"
+                   name="crushRoot"
+                   class="form-control"
+                   placeholder="root..."
+                   formControlName="crushRoot">
+          </div>
+        </div>
+
+        <div class="form-group row">
+          <label for="crushDeviceClass"
+                 class="cd-col-form-label">
+            <ng-container i18n>Crush device class</ng-container>
+            <cd-helper [html]="tooltips.crushDeviceClass">
+            </cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <select class="form-control custom-select"
+                    id="crushDeviceClass"
+                    name="crushDeviceClass"
+                    formControlName="crushDeviceClass">
+              <option ngValue=""
+                      i18n>any</option>
+              <option *ngFor="let deviceClass of devices"
+                      [ngValue]="deviceClass">
+                {{ deviceClass }}
+              </option>
+            </select>
+          </div>
+        </div>
+
+        <div class="form-group row">
+          <label for="directory"
+                 class="cd-col-form-label">
+            <ng-container i18n>Directory</ng-container>
+            <cd-helper [html]="tooltips.directory">
+            </cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <input type="text"
+                   id="directory"
+                   name="directory"
+                   class="form-control"
+                   placeholder="Path..."
+                   formControlName="directory">
+          </div>
+        </div>
+      </div>
+
+      <div class="modal-footer">
+        <cd-submit-button (submitAction)="onSubmit()"
+                          i18n="form action button|Example: Create Pool@@formActionButton"
+                          [form]="frm">{{ action | titlecase }} {{ resource | upperFirst }}</cd-submit-button>
+        <cd-back-button [back]="bsModalRef.hide"></cd-back-button>
+      </div>
+    </form>
+  </ng-container>
+</cd-modal>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..0d4ce97
--- /dev/null
@@ -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<ErasureCodeProfileFormModalComponent>;
+  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 (file)
index 0000000..6a62a5c
--- /dev/null
@@ -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 (file)
index 985f6a3..0000000
+++ /dev/null
@@ -1,313 +0,0 @@
-<cd-modal [modalRef]="bsModalRef">
-  <ng-container i18n="form title|Example: Create Pool@@formTitle"
-                class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
-
-  <ng-container class="modal-content">
-    <form #frm="ngForm"
-          [formGroup]="form"
-          novalidate>
-      <div class="modal-body">
-        <div class="form-group row">
-          <label class="cd-col-form-label"
-                 for="name"
-                 i18n>Name</label>
-          <div class="cd-col-form-input">
-            <input type="text"
-                   id="name"
-                   name="name"
-                   class="form-control"
-                   placeholder="Name..."
-                   formControlName="name"
-                   autofocus>
-            <span class="invalid-feedback"
-                  *ngIf="form.showError('name', frm, 'required')"
-                  i18n>This field is required!</span>
-            <span class="invalid-feedback"
-                  *ngIf="form.showError('name', frm, 'pattern')"
-                  i18n>The name can only consist of alphanumeric characters, dashes and underscores.</span>
-            <span class="invalid-feedback"
-                  *ngIf="form.showError('name', frm, 'uniqueName')"
-                  i18n>The chosen erasure code profile name is already in use.</span>
-          </div>
-        </div>
-
-        <div class="form-group row">
-          <label for="plugin"
-                 class="cd-col-form-label">
-            <span class="required"
-                  i18n>Plugin</span>
-            <cd-helper [html]="tooltips.plugins[plugin].description">
-            </cd-helper>
-          </label>
-          <div class="cd-col-form-input">
-            <select class="form-control custom-select"
-                    id="plugin"
-                    name="plugin"
-                    formControlName="plugin">
-              <option *ngIf="!plugins"
-                      ngValue=""
-                      i18n>Loading...</option>
-              <option *ngFor="let plugin of plugins"
-                      [ngValue]="plugin">
-                {{ plugin }}
-              </option>
-            </select>
-            <span class="invalid-feedback"
-                  *ngIf="form.showError('name', frm, 'required')"
-                  i18n>This field is required!</span>
-          </div>
-        </div>
-
-        <div class="form-group row">
-          <label for="k"
-                 class="cd-col-form-label">
-            <span [ngClass]="{'required': requiredControls.includes('k')}"
-                  i18n>Data chunks (k)</span>
-            <cd-helper [html]="tooltips.k">
-            </cd-helper>
-          </label>
-          <div class="cd-col-form-input">
-            <input type="number"
-                   id="k"
-                   name="k"
-                   class="form-control"
-                   ng-model="$ctrl.erasureCodeProfile.k"
-                   placeholder="Data chunks..."
-                   formControlName="k">
-            <span class="invalid-feedback"
-                  *ngIf="form.showError('k', frm, 'required')"
-                  i18n>This field is required!</span>
-            <span class="invalid-feedback"
-                  *ngIf="form.showError('k', frm, 'min')"
-                  i18n>Must be equal to or greater than 2.</span>
-          </div>
-        </div>
-
-        <div class="form-group row">
-          <label for="m"
-                 class="cd-col-form-label">
-            <span [ngClass]="{'required': requiredControls.includes('m')}"
-                  i18n>Coding chunks (m)</span>
-            <cd-helper [html]="tooltips.m">
-            </cd-helper>
-          </label>
-          <div class="cd-col-form-input">
-            <input type="number"
-                   id="m"
-                   name="m"
-                   class="form-control"
-                   placeholder="Coding chunks..."
-                   formControlName="m">
-            <span class="invalid-feedback"
-                  *ngIf="form.showError('m', frm, 'required')"
-                  i18n>This field is required!</span>
-            <span class="invalid-feedback"
-                  *ngIf="form.showError('m', frm, 'min')"
-                  i18n>Must be equal to or greater than 1.</span>
-          </div>
-        </div>
-
-        <div class="form-group row"
-             *ngIf="plugin === 'shec'">
-          <label for="c"
-                 class="cd-col-form-label">
-            <ng-container i18n>Durability estimator (c)</ng-container>
-            <cd-helper [html]="tooltips.plugins.shec.c">
-            </cd-helper>
-          </label>
-          <div class="cd-col-form-input">
-            <input type="number"
-                   id="c"
-                   name="c"
-                   class="form-control"
-                   placeholder="Coding chunks..."
-                   formControlName="c">
-            <span class="invalid-feedback"
-                  *ngIf="form.showError('c', frm, 'min')"
-                  i18n>Must be equal to or greater than 1.</span>
-          </div>
-        </div>
-
-        <div class="form-group row"
-             *ngIf="plugin === PLUGIN.LRC">
-          <label class="cd-col-form-label"
-                 for="l">
-            <span class="required"
-                  i18n>Locality (l)</span>
-            <cd-helper [html]="tooltips.plugins.lrc.l">
-            </cd-helper>
-          </label>
-          <div class="cd-col-form-input">
-            <input type="number"
-                   id="l"
-                   name="l"
-                   class="form-control"
-                   placeholder="Coding chunks..."
-                   formControlName="l">
-            <span class="invalid-feedback"
-                  *ngIf="form.showError('l', frm, 'required')"
-                  i18n>This field is required!</span>
-            <span class="invalid-feedback"
-                  *ngIf="form.showError('l', frm, 'min')"
-                  i18n>Must be equal to or greater than 1.</span>
-          </div>
-        </div>
-
-        <div class="form-group row">
-          <label for="crushFailureDomain"
-                 class="cd-col-form-label">
-            <ng-container i18n>Crush failure domain</ng-container>
-            <cd-helper [html]="tooltips.crushFailureDomain">
-            </cd-helper>
-          </label>
-          <div class="cd-col-form-input">
-            <select class="form-control custom-select"
-                    id="crushFailureDomain"
-                    name="crushFailureDomain"
-                    formControlName="crushFailureDomain">
-              <option *ngIf="!failureDomains"
-                      ngValue=""
-                      i18n>Loading...</option>
-              <option *ngFor="let domain of failureDomains"
-                      [ngValue]="domain">
-                {{ domain }}
-              </option>
-            </select>
-          </div>
-        </div>
-
-        <div class="form-group row"
-             *ngIf="plugin === PLUGIN.LRC">
-          <label for="crushLocality"
-                 class="cd-col-form-label">
-            <ng-container i18n>Crush Locality</ng-container>
-            <cd-helper [html]="tooltips.plugins.lrc.crushLocality">
-            </cd-helper>
-          </label>
-          <div class="cd-col-form-input">
-            <select class="form-control custom-select"
-                    id="crushLocality"
-                    name="crushLocality"
-                    formControlName="crushLocality">
-              <option *ngIf="!failureDomains"
-                      ngValue=""
-                      i18n>Loading...</option>
-              <option *ngIf="failureDomains && failureDomains.length > 0"
-                      ngValue=""
-                      i18n>None</option>
-              <option *ngFor="let domain of failureDomains"
-                      [ngValue]="domain">
-                {{ domain }}
-              </option>
-            </select>
-          </div>
-        </div>
-
-        <div class="form-group row"
-             *ngIf="[PLUGIN.JERASURE, PLUGIN.ISA].includes(plugin)">
-          <label for="technique"
-                 class="cd-col-form-label">
-            <ng-container i18n>Technique</ng-container>
-            <cd-helper [html]="tooltips.plugins[plugin].technique">
-            </cd-helper>
-          </label>
-          <div class="cd-col-form-input">
-            <select class="form-control custom-select"
-                    id="technique"
-                    name="technique"
-                    formControlName="technique">
-              <option *ngFor="let technique of techniques"
-                      [ngValue]="technique">
-                {{ technique }}
-              </option>
-            </select>
-          </div>
-        </div>
-
-        <div class="form-group row"
-             *ngIf="plugin === PLUGIN.JERASURE">
-          <label for="packetSize"
-                 class="cd-col-form-label">
-            <ng-container i18n>Packetsize</ng-container>
-            <cd-helper [html]="tooltips.plugins.jerasure.packetSize">
-            </cd-helper>
-          </label>
-          <div class="cd-col-form-input">
-            <input type="number"
-                   id="packetSize"
-                   name="packetSize"
-                   class="form-control"
-                   placeholder="Packetsize..."
-                   formControlName="packetSize">
-            <span class="invalid-feedback"
-                  *ngIf="form.showError('packetSize', frm, 'min')"
-                  i18n>Must be equal to or greater than 1.</span>
-          </div>
-        </div>
-
-        <div class="form-group row">
-          <label for="crushRoot"
-                 class="cd-col-form-label">
-            <ng-container i18n>Crush root</ng-container>
-            <cd-helper [html]="tooltips.crushRoot">
-            </cd-helper>
-          </label>
-          <div class="cd-col-form-input">
-            <input type="text"
-                   id="crushRoot"
-                   name="crushRoot"
-                   class="form-control"
-                   placeholder="root..."
-                   formControlName="crushRoot">
-          </div>
-        </div>
-
-        <div class="form-group row">
-          <label for="crushDeviceClass"
-                 class="cd-col-form-label">
-            <ng-container i18n>Crush device class</ng-container>
-            <cd-helper [html]="tooltips.crushDeviceClass">
-            </cd-helper>
-          </label>
-          <div class="cd-col-form-input">
-            <select class="form-control custom-select"
-                    id="crushDeviceClass"
-                    name="crushDeviceClass"
-                    formControlName="crushDeviceClass">
-              <option ngValue=""
-                      i18n>any</option>
-              <option *ngFor="let deviceClass of devices"
-                      [ngValue]="deviceClass">
-                {{ deviceClass }}
-              </option>
-            </select>
-          </div>
-        </div>
-
-        <div class="form-group row">
-          <label for="directory"
-                 class="cd-col-form-label">
-            <ng-container i18n>Directory</ng-container>
-            <cd-helper [html]="tooltips.directory">
-            </cd-helper>
-          </label>
-          <div class="cd-col-form-input">
-            <input type="text"
-                   id="directory"
-                   name="directory"
-                   class="form-control"
-                   placeholder="Path..."
-                   formControlName="directory">
-          </div>
-        </div>
-      </div>
-
-      <div class="modal-footer">
-        <cd-submit-button (submitAction)="onSubmit()"
-                          i18n="form action button|Example: Create Pool@@formActionButton"
-                          [form]="frm">{{ action | titlecase }} {{ resource | upperFirst }}</cd-submit-button>
-        <cd-back-button [back]="bsModalRef.hide"></cd-back-button>
-      </div>
-    </form>
-  </ng-container>
-</cd-modal>
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 (file)
index e69de29..0000000
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 (file)
index 0ef4e37..0000000
+++ /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<ErasureCodeProfileFormComponent>;
-  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 (file)
index 614902e..0000000
+++ /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);
-        }
-      );
-  }
-}
index 25edf591d335abb83a201afd322da2296608c886..d8ceabe1b1a0d5545cc40c47cd97bc6b150d2f5a 100644 (file)
@@ -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() {
index ee768f8e60da558efd007bdd6f5e697d8083ae70..98fd4360a0e99c69e88d279c714a5e68a4649fcc 100644 (file)
@@ -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 {}