]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add unique validator 23802/head
authorVolker Theile <vtheile@suse.com>
Wed, 29 Aug 2018 10:18:37 +0000 (12:18 +0200)
committerVolker Theile <vtheile@suse.com>
Thu, 30 Aug 2018 13:43:37 +0000 (15:43 +0200)
Relocate an already existing async validator into a separate validator that can be reused by every other form. This validator is useful to check immediately after typing if an entered value, e.g. username, already exists.

The API request will be triggered after a delay of 500ms (can be modified). During this delay, every keystroke will reset the timer, so the REST API is not flooded with request.

Signed-off-by: Volker Theile <vtheile@suse.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts

index 8b46cea8ca92ea3d20c0abfd20a4438fd068cdd2..943a4d96ff3bd7b73bb0fc223faa44a5f1d30ee6 100644 (file)
@@ -45,7 +45,7 @@
               This field is required.
             </span>
             <span class="help-block"
-                  *ngIf="userForm.showError('user_id', frm, 'userIdExists')"
+                  *ngIf="userForm.showError('user_id', frm, 'notUnique')"
                   i18n>
               The chosen user ID is already in use.
             </span>
index e47a33571925c8442303c1fd4e1257231d92f70b..c353455fbdfbcc2509d0bf06483e2e992021258e 100644 (file)
@@ -4,7 +4,7 @@ import { FormControl, ReactiveFormsModule } from '@angular/forms';
 import { RouterTestingModule } from '@angular/router/testing';
 
 import { BsModalService } from 'ngx-bootstrap/modal';
-import { Observable, of as observableOf } from 'rxjs';
+import { of as observableOf } from 'rxjs';
 
 import { configureTestBed } from '../../../../testing/unit-test-helper';
 import { RgwUserService } from '../../../shared/api/rgw-user.service';
@@ -15,18 +15,11 @@ import { RgwUserFormComponent } from './rgw-user-form.component';
 describe('RgwUserFormComponent', () => {
   let component: RgwUserFormComponent;
   let fixture: ComponentFixture<RgwUserFormComponent>;
-  let queryResult: Array<string> = [];
-
-  class MockRgwUserService extends RgwUserService {
-    enumerate() {
-      return observableOf(queryResult);
-    }
-  }
 
   configureTestBed({
     declarations: [RgwUserFormComponent],
     imports: [HttpClientTestingModule, ReactiveFormsModule, RouterTestingModule, SharedModule],
-    providers: [BsModalService, { provide: RgwUserService, useClass: MockRgwUserService }]
+    providers: [BsModalService, RgwUserService]
   });
 
   beforeEach(() => {
@@ -119,52 +112,43 @@ describe('RgwUserFormComponent', () => {
     });
   });
 
-  describe('userIdValidator', () => {
-    it('should validate user id (1/3)', () => {
-      const validatorFn = component.userIdValidator();
-      const ctrl = new FormControl('');
-      const validator$ = validatorFn(ctrl);
-      expect(validator$ instanceof Observable).toBeTruthy();
-      if (validator$ instanceof Observable) {
-        validator$.subscribe((resp) => {
-          expect(resp).toBe(null);
-        });
-      }
+  describe('username validation', () => {
+    let rgwUserService: RgwUserService;
+
+    beforeEach(() => {
+      rgwUserService = TestBed.get(RgwUserService);
+      spyOn(rgwUserService, 'enumerate').and.returnValue(observableOf(['abc', 'xyz']));
+    });
+
+    it('should validate that username is required', () => {
+      const user_id = component.userForm.get('user_id');
+      user_id.markAsDirty();
+      user_id.setValue('');
+      expect(user_id.hasError('required')).toBeTruthy();
+      expect(user_id.valid).toBeFalsy();
     });
 
     it(
-      'should validate user id (2/3)',
+      'should validate that username is valid',
       fakeAsync(() => {
-        const validatorFn = component.userIdValidator(0);
-        const ctrl = new FormControl('ab');
-        ctrl.markAsDirty();
-        const validator$ = validatorFn(ctrl);
-        expect(validator$ instanceof Observable).toBeTruthy();
-        if (validator$ instanceof Observable) {
-          validator$.subscribe((resp) => {
-            expect(resp).toBe(null);
-          });
-          tick();
-        }
+        const user_id = component.userForm.get('user_id');
+        user_id.markAsDirty();
+        user_id.setValue('ab');
+        tick(500);
+        expect(user_id.hasError('notUnique')).toBeFalsy();
+        expect(user_id.valid).toBeTruthy();
       })
     );
 
     it(
-      'should validate user id (3/3)',
+      'should validate that username is invalid',
       fakeAsync(() => {
-        queryResult = ['abc'];
-        const validatorFn = component.userIdValidator(0);
-        const ctrl = new FormControl('abc');
-        ctrl.markAsDirty();
-        const validator$ = validatorFn(ctrl);
-        expect(validator$ instanceof Observable).toBeTruthy();
-        if (validator$ instanceof Observable) {
-          validator$.subscribe((resp) => {
-            expect(resp instanceof Object).toBeTruthy();
-            expect(resp.userIdExists).toBeTruthy();
-          });
-          tick();
-        }
+        const user_id = component.userForm.get('user_id');
+        user_id.markAsDirty();
+        user_id.setValue('abc');
+        tick(500);
+        expect(user_id.hasError('notUnique')).toBeTruthy();
+        expect(user_id.valid).toBeFalsy();
       })
     );
   });
index f681542fee6ae729cb0aa8a10fd29ad3c968c054..8623351791de6104ff097b152e3056b677e349d5 100644 (file)
@@ -4,13 +4,7 @@ import { ActivatedRoute, Router } from '@angular/router';
 
 import * as _ from 'lodash';
 import { BsModalService } from 'ngx-bootstrap';
-import {
-  forkJoin as observableForkJoin,
-  Observable,
-  of as observableOf,
-  timer as observableTimer
-} from 'rxjs';
-import { map, switchMapTo, take } from 'rxjs/operators';
+import { forkJoin as observableForkJoin, Observable } from 'rxjs';
 
 import { RgwUserService } from '../../../shared/api/rgw-user.service';
 import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
@@ -57,7 +51,11 @@ export class RgwUserFormComponent implements OnInit {
   createForm() {
     this.userForm = this.formBuilder.group({
       // General
-      user_id: [null, [Validators.required], [this.userIdValidator()]],
+      user_id: [
+        null,
+        [Validators.required],
+        [CdValidators.unique(this.rgwUserService.exists, this.rgwUserService)]
+      ],
       display_name: [null, [Validators.required]],
       email: [null, [CdValidators.email]],
       max_buckets: [1000, [Validators.required, Validators.min(0)]],
@@ -278,36 +276,6 @@ export class RgwUserFormComponent implements OnInit {
     return bytes < 1024 ? { quotaMaxSize: true } : null;
   }
 
-  /**
-   * Validate the username.
-   * @param {number|Date} dueTime The delay time to wait before the
-   *   API call is executed. This is useful to prevent API calls on
-   *   every keystroke. Defaults to 500.
-   */
-  userIdValidator(dueTime = 500): AsyncValidatorFn {
-    const rgwUserService = this.rgwUserService;
-    return (control: AbstractControl): Observable<ValidationErrors | null> => {
-      // Exit immediately if user has not interacted with the control yet
-      // or the control value is empty.
-      if (control.pristine || control.value === '') {
-        return observableOf(null);
-      }
-      // Forgot previous requests if a new one arrives within the specified
-      // delay time.
-      return observableTimer(dueTime).pipe(
-        switchMapTo(rgwUserService.exists(control.value)),
-        map((resp: boolean) => {
-          if (!resp) {
-            return null;
-          } else {
-            return { userIdExists: true };
-          }
-        }),
-        take(1)
-      );
-    };
-  }
-
   /**
    * Add/Update a subuser.
    */
index cc7b749a584b82ede68d2bfeb625fd28b3746aab..07b05b78b01c31890f97edce53e333c1928baf1d 100644 (file)
@@ -2,7 +2,7 @@ import { HttpClient, HttpParams } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 
 import * as _ from 'lodash';
-import { forkJoin as observableForkJoin, of as observableOf } from 'rxjs';
+import { forkJoin as observableForkJoin, Observable, of as observableOf } from 'rxjs';
 import { mergeMap } from 'rxjs/operators';
 
 import { cdEncode } from '../decorators/cd-encode';
@@ -127,7 +127,7 @@ export class RgwUserService {
    * @param {string} uid The user ID to check.
    * @return {Observable<boolean>}
    */
-  exists(uid: string) {
+  exists(uid: string): Observable<boolean> {
     return this.enumerate().pipe(
       mergeMap((resp: string[]) => {
         const index = _.indexOf(resp, uid);
index 3f111b555e7c41387ec88bd245fef81ebb0f8a3c..857800d9559ffd4a3ff00f97010eb1c7732ad8ec 100644 (file)
@@ -1,4 +1,7 @@
-import { FormControl, FormGroup } from '@angular/forms';
+import { fakeAsync, tick } from '@angular/core/testing';
+import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
+
+import { of as observableOf } from 'rxjs';
 
 import { CdValidators } from './cd-validators';
 
@@ -199,4 +202,49 @@ describe('CdValidators', () => {
       expect(y.hasError('match')).toBeFalsy();
     });
   });
+
+  describe('unique', () => {
+    let form: FormGroup;
+    let x: AbstractControl;
+
+    beforeEach(() => {
+      form = new FormGroup({
+        x: new FormControl(
+          '',
+          null,
+          CdValidators.unique((value) => {
+            return observableOf('xyz' === value);
+          })
+        )
+      });
+      x = form.get('x');
+      x.markAsDirty();
+    });
+
+    it('should not error because of empty input', () => {
+      x.setValue('');
+      expect(x.hasError('notUnique')).toBeFalsy();
+      expect(x.valid).toBeTruthy();
+    });
+
+    it(
+      'should not error because of not existing input',
+      fakeAsync(() => {
+        x.setValue('abc');
+        tick(500);
+        expect(x.hasError('notUnique')).toBeFalsy();
+        expect(x.valid).toBeTruthy();
+      })
+    );
+
+    it(
+      'should error because of already existing input',
+      fakeAsync(() => {
+        x.setValue('xyz');
+        tick(500);
+        expect(x.hasError('notUnique')).toBeTruthy();
+        expect(x.valid).toBeFalsy();
+      })
+    );
+  });
 });
index 7898260c3f98674478793e49c31aa00d75722673..f1480409cfb10d6cae64a8a838a1e1cd398d51c1 100644 (file)
@@ -1,11 +1,21 @@
-import { AbstractControl, ValidationErrors, ValidatorFn, Validators } from '@angular/forms';
+import {
+  AbstractControl,
+  AsyncValidatorFn,
+  ValidationErrors,
+  ValidatorFn,
+  Validators
+} from '@angular/forms';
 
 import * as _ from 'lodash';
+import { Observable, of as observableOf, timer as observableTimer } from 'rxjs';
+import { map, switchMapTo, take } from 'rxjs/operators';
 
 export function isEmptyInputValue(value: any): boolean {
   return value == null || value.length === 0;
 }
 
+export type existsServiceFn = (value: any) => Observable<boolean>;
+
 export class CdValidators {
   /**
    * Validator that performs email validation. In contrast to the Angular
@@ -112,4 +122,47 @@ export class CdValidators {
       return null;
     };
   }
+
+  /**
+   * Asynchronous validator that requires the control's value to be unique.
+   * The validation is only executed after the specified delay. Every
+   * keystroke during this delay will restart the timer.
+   * @param serviceFn {existsServiceFn} The service function that is
+   *   called to check whether the given value exists. It must return
+   *   boolean 'true' if the given value exists, otherwise 'false'.
+   * @param serviceFnThis {any} The object to be used as the 'this' object
+   *   when calling the serviceFn function. Defaults to null.
+   * @param {number|Date} dueTime The delay time to wait before the
+   *   serviceFn call is executed. This is useful to prevent calls on
+   *   every keystroke. Defaults to 500.
+   * @return {AsyncValidatorFn} Returns an asynchronous validator function
+   *   that returns an error map with the `notUnique` property if the
+   *   validation check succeeds, otherwise `null`.
+   */
+  static unique(
+    serviceFn: existsServiceFn,
+    serviceFnThis: any = null,
+    dueTime = 500
+  ): AsyncValidatorFn {
+    return (control: AbstractControl): Observable<ValidationErrors | null> => {
+      // Exit immediately if user has not interacted with the control yet
+      // or the control value is empty.
+      if (control.pristine || isEmptyInputValue(control.value)) {
+        return observableOf(null);
+      }
+      // Forgot previous requests if a new one arrives within the specified
+      // delay time.
+      return observableTimer(dueTime).pipe(
+        switchMapTo(serviceFn.call(serviceFnThis, control.value)),
+        map((resp: boolean) => {
+          if (!resp) {
+            return null;
+          } else {
+            return { notUnique: true };
+          }
+        }),
+        take(1)
+      );
+    };
+  }
 }