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';
@NgModule({
entryComponents: [
RbdDetailsComponent,
- RbdNamespaceFormComponent,
- RbdSnapshotFormComponent,
+ RbdNamespaceFormModalComponent,
+ RbdSnapshotFormModalComponent,
RbdTrashMoveModalComponent,
RbdTrashRestoreModalComponent,
RbdTrashPurgeModalComponent,
IscsiTargetListComponent,
RbdDetailsComponent,
RbdFormComponent,
- RbdNamespaceFormComponent,
+ RbdNamespaceFormModalComponent,
RbdNamespaceListComponent,
RbdSnapshotListComponent,
- RbdSnapshotFormComponent,
+ RbdSnapshotFormModalComponent,
RbdTrashListComponent,
RbdTrashMoveModalComponent,
RbdImagesComponent,
--- /dev/null
+<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>
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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 });
+ });
+ }
+}
+++ /dev/null
-<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>
+++ /dev/null
-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();
- });
-});
+++ /dev/null
-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 });
- });
- }
-}
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',
}
createModal() {
- this.modalRef = this.modalService.show(RbdNamespaceFormComponent);
+ this.modalRef = this.modalService.show(RbdNamespaceFormModalComponent);
this.modalRef.content.onSubmit.subscribe(() => {
this.refresh();
});
--- /dev/null
+<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>
--- /dev/null
+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');
+ });
+});
--- /dev/null
+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();
+ }
+ }
+}
+++ /dev/null
-<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>
+++ /dev/null
-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');
- });
-});
+++ /dev/null
-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();
- }
- }
-}
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';
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,
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';
}
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;
--- /dev/null
+<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>
--- /dev/null
+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();
+ });
+ });
+ });
+});
--- /dev/null
+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);
+ }
+ );
+ }
+}
+++ /dev/null
-<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>
+++ /dev/null
-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();
- });
- });
- });
-});
+++ /dev/null
-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);
- }
- );
- }
-}
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';
addErasureCodeProfile() {
this.modalSubscription = this.modalService.onHide.subscribe(() => this.reloadECPs());
- this.bsModalService.show(ErasureCodeProfileFormComponent);
+ this.bsModalService.show(ErasureCodeProfileFormModalComponent);
}
private reloadECPs() {
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';
declarations: [
PoolListComponent,
PoolFormComponent,
- ErasureCodeProfileFormComponent,
+ ErasureCodeProfileFormModalComponent,
CrushRuleFormModalComponent,
PoolDetailsComponent
],
- entryComponents: [CrushRuleFormModalComponent, ErasureCodeProfileFormComponent]
+ entryComponents: [CrushRuleFormModalComponent, ErasureCodeProfileFormModalComponent]
})
export class PoolModule {}