From 8a815ee72828e13f30c43b3734d2212db6db5937 Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Fri, 27 Apr 2018 15:50:21 +0200 Subject: [PATCH] mgr/dashboard: Add RGW management features Signed-off-by: Volker Theile --- src/pybind/mgr/dashboard/controllers/rgw.py | 10 + .../frontend/src/app/app-routing.module.ts | 22 + .../ceph/rgw/models/rgw-user-capability.ts | 4 + .../app/ceph/rgw/models/rgw-user-s3-key.ts | 6 + .../app/ceph/rgw/models/rgw-user-subuser.ts | 6 + .../app/ceph/rgw/models/rgw-user-swift-key.ts | 4 + .../rgw-bucket-form.component.html | 146 ++++ .../rgw-bucket-form.component.scss | 0 .../rgw-bucket-form.component.spec.ts | 106 +++ .../rgw-bucket-form.component.ts | 151 ++++ .../rgw-bucket-list.component.html | 70 +- .../rgw-bucket-list.component.spec.ts | 8 +- .../rgw-bucket-list.component.ts | 38 +- .../rgw-daemon-details.component.spec.ts | 8 +- .../rgw-user-capability-modal.component.html | 96 +++ .../rgw-user-capability-modal.component.scss | 0 ...gw-user-capability-modal.component.spec.ts | 34 + .../rgw-user-capability-modal.component.ts | 87 +++ .../rgw-user-details.component.html | 23 + .../rgw-user-details.component.spec.ts | 4 + .../rgw-user-details.component.ts | 84 +- .../rgw-user-form.component.html | 663 ++++++++++++++++ .../rgw-user-form.component.scss | 0 .../rgw-user-form.component.spec.ts | 130 ++++ .../rgw-user-form/rgw-user-form.component.ts | 722 ++++++++++++++++++ .../rgw-user-list.component.html | 67 +- .../rgw-user-list.component.spec.ts | 6 + .../rgw-user-list/rgw-user-list.component.ts | 39 +- .../rgw-user-s3-key-modal.component.html | 158 ++++ .../rgw-user-s3-key-modal.component.scss | 0 .../rgw-user-s3-key-modal.component.spec.ts | 34 + .../rgw-user-s3-key-modal.component.ts | 97 +++ .../rgw-user-subuser-modal.component.html | 168 ++++ .../rgw-user-subuser-modal.component.scss | 0 .../rgw-user-subuser-modal.component.spec.ts | 75 ++ .../rgw-user-subuser-modal.component.ts | 155 ++++ .../rgw-user-swift-key-modal.component.html | 67 ++ .../rgw-user-swift-key-modal.component.scss | 0 ...rgw-user-swift-key-modal.component.spec.ts | 32 + .../rgw-user-swift-key-modal.component.ts | 24 + .../frontend/src/app/ceph/rgw/rgw.module.ts | 42 +- .../src/app/shared/api/rgw-bucket.service.ts | 56 +- .../src/app/shared/api/rgw-user.service.ts | 122 ++- .../directives/password-button.directive.ts | 19 +- 44 files changed, 3524 insertions(+), 59 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-capability.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-s3-key.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-subuser.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-swift-key.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.ts diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index e7cdaa3939073..05420473980ae 100644 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -66,6 +66,7 @@ class RgwDaemon(RESTController): @ApiController('rgw/proxy/{path:.*}') @AuthRequired() class RgwProxy(BaseController): + @cherrypy.expose def __call__(self, path, **params): try: @@ -85,3 +86,12 @@ class RgwProxy(BaseController): cherrypy.response.headers['Content-Type'] = 'application/json' cherrypy.response.status = 500 return json.dumps({'detail': str(e)}).encode('utf-8') + + +@ApiController('rgw/bucket') +@AuthRequired() +class RgwBucket(RESTController): + + def create(self, bucket, uid): + rgw_client = RgwClient.instance(uid) + return rgw_client.create_bucket(bucket) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index 77d5e5e68b5db..a4fb77be41f10 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -16,8 +16,10 @@ import { PerformanceCounterComponent } from './ceph/performance-counter/performance-counter/performance-counter.component'; import { PoolListComponent } from './ceph/pool/pool-list/pool-list.component'; +import { RgwBucketFormComponent } from './ceph/rgw/rgw-bucket-form/rgw-bucket-form.component'; import { RgwBucketListComponent } from './ceph/rgw/rgw-bucket-list/rgw-bucket-list.component'; import { RgwDaemonListComponent } from './ceph/rgw/rgw-daemon-list/rgw-daemon-list.component'; +import { RgwUserFormComponent } from './ceph/rgw/rgw-user-form/rgw-user-form.component'; import { RgwUserListComponent } from './ceph/rgw/rgw-user-list/rgw-user-list.component'; import { LoginComponent } from './core/auth/login/login.component'; import { NotFoundComponent } from './core/not-found/not-found.component'; @@ -39,11 +41,31 @@ const routes: Routes = [ component: RgwUserListComponent, canActivate: [AuthGuardService] }, + { + path: 'rgw/user/add', + component: RgwUserFormComponent, + canActivate: [AuthGuardService] + }, + { + path: 'rgw/user/edit/:uid', + component: RgwUserFormComponent, + canActivate: [AuthGuardService] + }, { path: 'rgw/bucket', component: RgwBucketListComponent, canActivate: [AuthGuardService] }, + { + path: 'rgw/bucket/add', + component: RgwBucketFormComponent, + canActivate: [AuthGuardService] + }, + { + path: 'rgw/bucket/edit/:bucket', + component: RgwBucketFormComponent, + canActivate: [AuthGuardService] + }, { path: 'block/iscsi', component: IscsiComponent, canActivate: [AuthGuardService] }, { path: 'block/rbd', component: RbdListComponent, canActivate: [AuthGuardService] }, { path: 'rbd/add', component: RbdFormComponent, canActivate: [AuthGuardService] }, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-capability.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-capability.ts new file mode 100644 index 0000000000000..ee10088c08281 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-capability.ts @@ -0,0 +1,4 @@ +export class RgwUserCapability { + type: string; + perm: string; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-s3-key.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-s3-key.ts new file mode 100644 index 0000000000000..bcb9531065fb7 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-s3-key.ts @@ -0,0 +1,6 @@ +export class RgwUserS3Key { + user: string; + generate_key?: boolean; + access_key: string; + secret_key: string; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-subuser.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-subuser.ts new file mode 100644 index 0000000000000..788b6a2910fa4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-subuser.ts @@ -0,0 +1,6 @@ +export class RgwUserSubuser { + id: string; + permissions: string; + generate_secret?: boolean; + secret_key?: string; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-swift-key.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-swift-key.ts new file mode 100644 index 0000000000000..26abd2a99dc6a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-swift-key.ts @@ -0,0 +1,4 @@ +export class RgwUserSwiftKey { + user: string; + secret_key: string; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html new file mode 100644 index 0000000000000..cc14db99f2143 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html @@ -0,0 +1,146 @@ + + + + Loading bucket data... + + + The bucket data could not be loaded. + + +
+
+
+
+

+ {editing, select, 1 {Edit} other {Add}} bucket +

+
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + + This field is required. + + + The value is not valid. + + + The chosen name is already in use. + +
+
+ + +
+ +
+ + + This field is required. + +
+
+ +
+ +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.spec.ts new file mode 100644 index 0000000000000..8500ed2b4c593 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.spec.ts @@ -0,0 +1,106 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; + +import 'rxjs/add/observable/of'; +import { Observable } from 'rxjs/Observable'; + +import { RgwBucketService } from '../../../shared/api/rgw-bucket.service'; +import { RgwUserService } from '../../../shared/api/rgw-user.service'; +import { SharedModule } from '../../../shared/shared.module'; +import { RgwBucketFormComponent } from './rgw-bucket-form.component'; + +describe('RgwBucketFormComponent', () => { + let component: RgwBucketFormComponent; + let fixture: ComponentFixture; + let queryResult: Array = []; + + class MockRgwBucketService extends RgwBucketService { + enumerate() { + return Observable.of(queryResult); + } + } + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ RgwBucketFormComponent ], + imports: [ + HttpClientTestingModule, + ReactiveFormsModule, + RouterTestingModule, + SharedModule + ], + providers: [ + RgwUserService, + { provide: RgwBucketService, useClass: MockRgwBucketService } + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RgwBucketFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('bucketNameValidator', () => { + it('should validate name (1/4)', () => { + const validatorFn = component.bucketNameValidator(); + const ctrl = new FormControl(''); + const validatorPromise = validatorFn(ctrl); + expect(validatorPromise instanceof Promise).toBeTruthy(); + if (validatorPromise instanceof Promise) { + validatorPromise.then((resp) => { + expect(resp).toBe(null); + }); + } + }); + + it('should validate name (2/4)', () => { + const validatorFn = component.bucketNameValidator(); + const ctrl = new FormControl('ab'); + ctrl.markAsDirty(); + const validatorPromise = validatorFn(ctrl); + expect(validatorPromise instanceof Promise).toBeTruthy(); + if (validatorPromise instanceof Promise) { + validatorPromise.then((resp) => { + expect(resp.bucketNameInvalid).toBeTruthy(); + }); + } + }); + + it('should validate name (3/4)', () => { + const validatorFn = component.bucketNameValidator(); + const ctrl = new FormControl('abc'); + ctrl.markAsDirty(); + const validatorPromise = validatorFn(ctrl); + expect(validatorPromise instanceof Promise).toBeTruthy(); + if (validatorPromise instanceof Promise) { + validatorPromise.then((resp) => { + expect(resp).toBe(null); + }); + } + }); + + it('should validate name (4/4)', () => { + queryResult = ['abcd']; + const validatorFn = component.bucketNameValidator(); + const ctrl = new FormControl('abcd'); + ctrl.markAsDirty(); + const validatorPromise = validatorFn(ctrl); + expect(validatorPromise instanceof Promise).toBeTruthy(); + if (validatorPromise instanceof Promise) { + validatorPromise.then((resp) => { + expect(resp instanceof Object).toBeTruthy(); + expect(resp.bucketNameExists).toBeTruthy(); + }); + } + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts new file mode 100644 index 0000000000000..0e9d7f5eb0a1a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts @@ -0,0 +1,151 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { + AbstractControl, + AsyncValidatorFn, + FormBuilder, + FormGroup, + ValidationErrors, + Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; + +import * as _ from 'lodash'; + +import { RgwBucketService } from '../../../shared/api/rgw-bucket.service'; +import { RgwUserService } from '../../../shared/api/rgw-user.service'; + +@Component({ + selector: 'cd-rgw-bucket-form', + templateUrl: './rgw-bucket-form.component.html', + styleUrls: ['./rgw-bucket-form.component.scss'] +}) +export class RgwBucketFormComponent implements OnInit, OnDestroy { + + bucketForm: FormGroup; + routeParamsSubscribe: any; + editing = false; + error = false; + loading = false; + owners = null; + + constructor(private formBuilder: FormBuilder, + private route: ActivatedRoute, + private router: Router, + private rgwBucketService: RgwBucketService, + private rgwUserService: RgwUserService) { + this.createForm(); + } + + createForm() { + this.bucketForm = this.formBuilder.group({ + 'id': [ + null + ], + 'bucket': [ + null, + [ Validators.required ], + [ this.bucketNameValidator() ] + ], + 'owner': [ + null, + [ Validators.required ] + ] + }); + } + + ngOnInit() { + // Get the list of possible owners. + this.rgwUserService.enumerate() + .subscribe((resp: string[]) => { + this.owners = resp.sort(); + }); + + // Process route parameters. + this.routeParamsSubscribe = this.route.params + .subscribe((params: { bucket: string }) => { + if (!params.hasOwnProperty('bucket')) { + return; + } + this.loading = true; + // Load the bucket data in 'edit' mode. + this.editing = true; + this.rgwBucketService.get(params.bucket) + .subscribe((resp: object) => { + this.loading = false; + // Get the default values. + const defaults = _.clone(this.bucketForm.value); + // Extract the values displayed in the form. + let value = _.pick(resp, _.keys(this.bucketForm.value)); + // Append default values. + value = _.merge(defaults, value); + // Update the form. + this.bucketForm.setValue(value); + }); + }, (error) => { + this.error = error; + }); + } + + ngOnDestroy() { + this.routeParamsSubscribe.unsubscribe(); + } + + goToListView() { + this.router.navigate(['/rgw/bucket']); + } + + submit() { + // Exit immediately if the form isn't dirty. + if (this.bucketForm.pristine) { + this.goToListView(); + } + const bucketCtl = this.bucketForm.get('bucket'); + const ownerCtl = this.bucketForm.get('owner'); + if (this.editing) { // Edit + const idCtl = this.bucketForm.get('id'); + this.rgwBucketService.update(idCtl.value, bucketCtl.value, ownerCtl.value) + .subscribe(() => { + this.goToListView(); + }, () => { + // Reset the 'Submit' button. + this.bucketForm.setErrors({'cdSubmitButton': true}); + }); + } else { // Add + this.rgwBucketService.create(bucketCtl.value, ownerCtl.value) + .subscribe(() => { + this.goToListView(); + }, () => { + // Reset the 'Submit' button. + this.bucketForm.setErrors({'cdSubmitButton': true}); + }); + } + } + + bucketNameValidator(): AsyncValidatorFn { + const rgwBucketService = this.rgwBucketService; + return (control: AbstractControl): Promise => { + return new Promise((resolve) => { + // Exit immediately if user has not interacted with the control yet + // or the control value is empty. + if (control.pristine || control.value === '') { + resolve(null); + return; + } + // Validate the bucket name. + const nameRe = /^[0-9A-Za-z][\w-\.]{2,254}$/; + if (!nameRe.test(control.value)) { + resolve({bucketNameInvalid: true}); + return; + } + // Does any bucket with the given name already exist? + rgwBucketService.exists(control.value) + .subscribe((resp: boolean) => { + if (!resp) { + resolve(null); + } else { + resolve({bucketNameExists: true}); + } + }); + }); + }; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html index b7b5f3d45358f..965884e750374 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html @@ -7,14 +7,76 @@ aria-current="page">Buckets - + (fetchData)="getBucketList()"> +
+
+ + + + + +
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts index 53d1f853de28f..4df24b8d4b8fa 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts @@ -15,10 +15,10 @@ describe('RgwBucketListComponent', () => { let component: RgwBucketListComponent; let fixture: ComponentFixture; - const fakeService = { + const fakeRgwBucketService = { list: () => { - return new Promise(function(resolve, reject) { - return []; + return new Promise(function(resolve) { + resolve([]); }); } }; @@ -37,7 +37,7 @@ describe('RgwBucketListComponent', () => { DataTableModule, SharedModule ], - providers: [{ provide: RgwBucketService, useValue: fakeService }] + providers: [{ provide: RgwBucketService, useValue: fakeRgwBucketService }] }) .compileComponents(); })); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts index 85d88da8912b9..1042a1a336ff3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts @@ -1,6 +1,15 @@ import { Component, ViewChild } from '@angular/core'; +import { Router } from '@angular/router'; + +import { BsModalService } from 'ngx-bootstrap'; +import 'rxjs/add/observable/forkJoin'; +import { Observable } from 'rxjs/Observable'; +import { Subscriber } from 'rxjs/Subscriber'; import { RgwBucketService } from '../../../shared/api/rgw-bucket.service'; +import { + DeletionModalComponent +} from '../../../shared/components/deletion-modal/deletion-modal.component'; import { TableComponent } from '../../../shared/datatable/table/table.component'; import { CdTableColumn } from '../../../shared/models/cd-table-column'; import { CdTableSelection } from '../../../shared/models/cd-table-selection'; @@ -11,13 +20,16 @@ import { CdTableSelection } from '../../../shared/models/cd-table-selection'; styleUrls: ['./rgw-bucket-list.component.scss'] }) export class RgwBucketListComponent { - @ViewChild('table') table: TableComponent; + + @ViewChild(TableComponent) table: TableComponent; columns: CdTableColumn[] = []; buckets: object[] = []; selection: CdTableSelection = new CdTableSelection(); - constructor(private rgwBucketService: RgwBucketService) { + constructor(private router: Router, + private rgwBucketService: RgwBucketService, + private bsModalService: BsModalService) { this.columns = [ { name: 'Name', @@ -46,4 +58,26 @@ export class RgwBucketListComponent { updateSelection(selection: CdTableSelection) { this.selection = selection; } + + deleteAction() { + const modalRef = this.bsModalService.show(DeletionModalComponent); + modalRef.content.setUp({ + metaType: this.selection.hasSingleSelection ? 'bucket' : 'buckets', + deletionObserver: (): Observable => { + return new Observable((observer: Subscriber) => { + Observable.forkJoin( + this.selection.selected.map((bucket: any) => { + return this.rgwBucketService.delete(bucket.bucket); + })) + .subscribe(null, null, () => { + observer.complete(); + // Finally reload the data table content. + this.table.refreshBtn(); + }); + }); + }, + modalRef: modalRef + }); + } + } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts index 13fd3f0a88bf9..c271666a98af2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts @@ -12,10 +12,10 @@ describe('RgwDaemonDetailsComponent', () => { let component: RgwDaemonDetailsComponent; let fixture: ComponentFixture; - const fakeService = { + const fakeRgwDaemonService = { get: (id: string) => { - return new Promise(function(resolve, reject) { - return []; + return new Promise(function(resolve) { + resolve([]); }); } }; @@ -28,7 +28,7 @@ describe('RgwDaemonDetailsComponent', () => { PerformanceCounterModule, TabsModule.forRoot() ], - providers: [{ provide: RgwDaemonService, useValue: fakeService }] + providers: [{ provide: RgwDaemonService, useValue: fakeRgwDaemonService }] }); })); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.html new file mode 100644 index 0000000000000..68770897ff185 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.html @@ -0,0 +1,96 @@ + +
+ + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.spec.ts new file mode 100644 index 0000000000000..313f66722a2f3 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.spec.ts @@ -0,0 +1,34 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; + +import { BsModalRef } from 'ngx-bootstrap/modal'; + +import { SharedModule } from '../../../shared/shared.module'; +import { RgwUserCapabilityModalComponent } from './rgw-user-capability-modal.component'; + +describe('RgwUserCapabilityModalComponent', () => { + let component: RgwUserCapabilityModalComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ RgwUserCapabilityModalComponent ], + imports: [ + ReactiveFormsModule, + SharedModule + ], + providers: [ BsModalRef ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RgwUserCapabilityModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.ts new file mode 100644 index 0000000000000..dca0efc077166 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-capability-modal/rgw-user-capability-modal.component.ts @@ -0,0 +1,87 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +import * as _ from 'lodash'; +import { BsModalRef } from 'ngx-bootstrap/modal/bs-modal-ref.service'; + +import { RgwUserCapability } from '../models/rgw-user-capability'; + +@Component({ + selector: 'cd-rgw-user-capability-modal', + templateUrl: './rgw-user-capability-modal.component.html', + styleUrls: ['./rgw-user-capability-modal.component.scss'] +}) +export class RgwUserCapabilityModalComponent { + + /** + * The event that is triggered when the 'Add' or 'Update' button + * has been pressed. + */ + @Output() submitAction = new EventEmitter(); + + formGroup: FormGroup; + editing = true; + types: string[] = []; + + constructor(private formBuilder: FormBuilder, + public bsModalRef: BsModalRef) { + this.createForm(); + } + + createForm() { + this.formGroup = this.formBuilder.group({ + 'type': [ + null, + [Validators.required] + ], + 'perm': [ + null, + [Validators.required] + ] + }); + } + + /** + * Set the 'editing' flag. If set to TRUE, the modal dialog is in 'Edit' mode, + * otherwise in 'Add' mode. According to the mode the dialog and its controls + * behave different. + * @param {boolean} viewing + */ + setEditing(editing: boolean = true) { + this.editing = editing; + } + + /** + * Set the values displayed in the dialog. + */ + setValues(type: string, perm: string) { + this.formGroup.setValue({ + 'type': type, + 'perm': perm + }); + } + + /** + * Set the current capabilities of the user. + */ + setCapabilities(capabilities: RgwUserCapability[]) { + // Parse the configured capabilities to get a list of types that + // should be displayed. + const usedTypes = []; + capabilities.forEach((capability) => { + usedTypes.push(capability.type); + }); + this.types = []; + ['users', 'buckets', 'metadata', 'usage', 'zone'].forEach((type) => { + if (_.indexOf(usedTypes, type) === -1) { + this.types.push(type); + } + }); + } + + onSubmit() { + const capability: RgwUserCapability = this.formGroup.value; + this.submitAction.emit(capability); + this.bsModalRef.hide(); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.html index f05ff79497685..ca51f56970b2d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.html @@ -122,4 +122,27 @@ + + +
+ +
+
+ +
+
+
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.spec.ts index 938f0f419f7d6..015dfc581e556 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.spec.ts @@ -2,6 +2,7 @@ import { HttpClientModule } from '@angular/common/http'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { BsModalService } from 'ngx-bootstrap/modal'; import { TabsModule } from 'ngx-bootstrap/tabs'; import { CdTableSelection } from '../../../shared/models/cd-table-selection'; @@ -20,6 +21,9 @@ describe('RgwUserDetailsComponent', () => { HttpClientModule, SharedModule, TabsModule.forRoot() + ], + providers: [ + BsModalService ] }) .compileComponents(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.ts index 1c21ce94176b8..35cbec9902d91 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-details/rgw-user-details.component.ts @@ -1,21 +1,57 @@ -import { Component, Input, OnChanges } from '@angular/core'; +import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core'; import * as _ from 'lodash'; +import { BsModalService } from 'ngx-bootstrap'; import { RgwUserService } from '../../../shared/api/rgw-user.service'; +import { CdTableColumn } from '../../../shared/models/cd-table-column'; import { CdTableSelection } from '../../../shared/models/cd-table-selection'; +import { RgwUserS3Key } from '../models/rgw-user-s3-key'; +import { RgwUserSwiftKey } from '../models/rgw-user-swift-key'; +import { + RgwUserS3KeyModalComponent +} from '../rgw-user-s3-key-modal/rgw-user-s3-key-modal.component'; +import { + RgwUserSwiftKeyModalComponent +} from '../rgw-user-swift-key-modal/rgw-user-swift-key-modal.component'; @Component({ selector: 'cd-rgw-user-details', templateUrl: './rgw-user-details.component.html', styleUrls: ['./rgw-user-details.component.scss'] }) -export class RgwUserDetailsComponent implements OnChanges { - user: any; +export class RgwUserDetailsComponent implements OnChanges, OnInit { + + @ViewChild('accessKeyTpl') public accessKeyTpl: TemplateRef; + @ViewChild('secretKeyTpl') public secretKeyTpl: TemplateRef; @Input() selection: CdTableSelection; - constructor(private rgwUserService: RgwUserService) {} + // Details tab + user: any; + + // Keys tab + keys: any = []; + keysColumns: CdTableColumn[] = []; + keysSelection: CdTableSelection = new CdTableSelection(); + + constructor(private rgwUserService: RgwUserService, + private bsModalService: BsModalService) {} + + ngOnInit() { + this.keysColumns = [ + { + name: 'Username', + prop: 'username', + flexGrow: 1 + }, + { + name: 'Type', + prop: 'type', + flexGrow: 1 + } + ]; + } ngOnChanges() { if (this.selection.hasSelection) { @@ -32,6 +68,46 @@ export class RgwUserDetailsComponent implements OnChanges { _.extend(this.user, resp); }); } + + // Process the keys. + this.keys = []; + this.user.keys.forEach((key: RgwUserS3Key) => { + this.keys.push({ + id: this.keys.length + 1, // Create an unique identifier + type: 'S3', + username: key.user, + ref: key + }); + }); + this.user.swift_keys.forEach((key: RgwUserSwiftKey) => { + this.keys.push({ + id: this.keys.length + 1, // Create an unique identifier + type: 'Swift', + username: key.user, + ref: key + }); + }); + this.keys = _.sortBy(this.keys, 'user'); + } + } + + updateKeysSelection(selection: CdTableSelection) { + this.keysSelection = selection; + } + + showKeyModal() { + const key = this.keysSelection.first(); + const modalRef = this.bsModalService.show(key.type === 'S3' ? + RgwUserS3KeyModalComponent : RgwUserSwiftKeyModalComponent); + switch (key.type) { + case 'S3': + modalRef.content.setViewing(); + modalRef.content.setValues(key.ref.user, key.ref.access_key, + key.ref.secret_key); + break; + case 'Swift': + modalRef.content.setValues(key.ref.user, key.ref.secret_key); + break; } } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html new file mode 100644 index 0000000000000..0a93303d7b01f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html @@ -0,0 +1,663 @@ + + + + Loading user data... + + + The user data could not be loaded. + + +
+
+
+
+

+ {editing, select, 1 {Edit} other {Add}} user +

+
+
+ + +
+ +
+ + + This field is required. + + + The chosen user ID is already in use. + +
+
+ + +
+ +
+ + + This field is required. + +
+
+ + +
+ +
+ + + This is not a valid email address. + +
+
+ + +
+ +
+ + + The entered value must be >= 0. + +
+
+ + +
+
+
+ + +
+
+
+ + +
+ S3 key + + +
+
+
+ + +
+
+
+ + +
+ +
+
+ + + + + +
+ + This field is required. + +
+
+ + +
+ +
+
+ + + + + +
+ + This field is required. + +
+
+
+ + +
+ Subusers + +
+ + Empty + + + + +
+ + + + + + + + + + + + +
+ +
+ + + + +
+
+ + +
+ Keys + + + +
+ + Empty + + + + +
+ + + + + + + + +
+ +
+ + + + +
+
+ + + +
+ + Empty + + + + +
+ + + + + + + +
+ +
+
+
+ + +
+ Capabilities + +
+ + Empty + + + + +
+ + + + + + + + +
+ +
+ + + + +
+
+ + +
+ User quota + + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+ +
+ + + This field is required. + + + The value is not valid. + +
+
+ + +
+
+
+ + +
+
+
+ + +
+ +
+ + + This field is required. + + + The entered value must be >= 0. + +
+
+
+ + +
+ Bucket quota + + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+ +
+ + + This field is required. + + + The value is not valid. + +
+
+ + +
+
+
+ + +
+
+
+ + +
+ +
+ + + This field is required. + + + The entered value must be >= 0. + +
+
+
+
+ + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.spec.ts new file mode 100644 index 0000000000000..573a86d135c81 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.spec.ts @@ -0,0 +1,130 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { BsModalService } from 'ngx-bootstrap/modal'; +import 'rxjs/add/observable/of'; +import { Observable } from 'rxjs/Observable'; + +import { RgwUserService } from '../../../shared/api/rgw-user.service'; +import { SharedModule } from '../../../shared/shared.module'; +import { RgwUserFormComponent } from './rgw-user-form.component'; + +describe('RgwUserFormComponent', () => { + let component: RgwUserFormComponent; + let fixture: ComponentFixture; + let queryResult: Array = []; + + class MockRgwUserService extends RgwUserService { + enumerate() { + return Observable.of(queryResult); + } + } + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ RgwUserFormComponent ], + imports: [ + HttpClientTestingModule, + ReactiveFormsModule, + RouterTestingModule, + SharedModule + ], + providers: [ + BsModalService, + { provide: RgwUserService, useClass: MockRgwUserService } + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RgwUserFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('quotaMaxSizeValidator', () => { + it('should validate max size (1/7)', () => { + const resp = component.quotaMaxSizeValidator(new FormControl('')); + expect(resp).toBe(null); + }); + + it('should validate max size (2/7)', () => { + const resp = component.quotaMaxSizeValidator(new FormControl('xxxx')); + expect(resp.quotaMaxSize).toBeTruthy(); + }); + + it('should validate max size (3/7)', () => { + const resp = component.quotaMaxSizeValidator(new FormControl('1023')); + expect(resp.quotaMaxSize).toBeTruthy(); + }); + + it('should validate max size (4/7)', () => { + const resp = component.quotaMaxSizeValidator(new FormControl('1024')); + expect(resp).toBe(null); + }); + + it('should validate max size (5/7)', () => { + const resp = component.quotaMaxSizeValidator(new FormControl('1M')); + expect(resp).toBe(null); + }); + + it('should validate max size (6/7)', () => { + const resp = component.quotaMaxSizeValidator(new FormControl('1024 gib')); + expect(resp).toBe(null); + }); + + it('should validate max size (7/7)', () => { + const resp = component.quotaMaxSizeValidator(new FormControl('10 X')); + expect(resp.quotaMaxSize).toBeTruthy(); + }); + }); + + describe('userIdValidator', () => { + it('should validate user id (1/3)', () => { + const validatorFn = component.userIdValidator(); + const ctrl = new FormControl(''); + const validatorPromise = validatorFn(ctrl); + expect(validatorPromise instanceof Promise).toBeTruthy(); + if (validatorPromise instanceof Promise) { + validatorPromise.then((resp) => { + expect(resp).toBe(null); + }); + } + }); + + it('should validate user id (2/3)', () => { + const validatorFn = component.userIdValidator(); + const ctrl = new FormControl('ab'); + ctrl.markAsDirty(); + const validatorPromise = validatorFn(ctrl); + expect(validatorPromise instanceof Promise).toBeTruthy(); + if (validatorPromise instanceof Promise) { + validatorPromise.then((resp) => { + expect(resp).toBe(null); + }); + } + }); + + it('should validate user id (3/3)', () => { + queryResult = ['abc']; + const validatorFn = component.userIdValidator(); + const ctrl = new FormControl('abc'); + ctrl.markAsDirty(); + const validatorPromise = validatorFn(ctrl); + expect(validatorPromise instanceof Promise).toBeTruthy(); + if (validatorPromise instanceof Promise) { + validatorPromise.then((resp) => { + expect(resp instanceof Object).toBeTruthy(); + expect(resp.userIdExists).toBeTruthy(); + }); + } + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts new file mode 100644 index 0000000000000..984906cf7f663 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts @@ -0,0 +1,722 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { + AbstractControl, + AsyncValidatorFn, + FormBuilder, + FormGroup, + ValidationErrors, + Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; + +import * as _ from 'lodash'; +import { BsModalService } from 'ngx-bootstrap'; +import 'rxjs/add/observable/forkJoin'; +import { Observable } from 'rxjs/Observable'; + +import { RgwUserService } from '../../../shared/api/rgw-user.service'; +import { FormatterService } from '../../../shared/services/formatter.service'; +import { CdValidators, isEmptyInputValue } from '../../../shared/validators/cd-validators'; +import { RgwUserCapability } from '../models/rgw-user-capability'; +import { RgwUserS3Key } from '../models/rgw-user-s3-key'; +import { RgwUserSubuser } from '../models/rgw-user-subuser'; +import { RgwUserSwiftKey } from '../models/rgw-user-swift-key'; +import { + RgwUserCapabilityModalComponent +} from '../rgw-user-capability-modal/rgw-user-capability-modal.component'; +import { + RgwUserS3KeyModalComponent +} from '../rgw-user-s3-key-modal/rgw-user-s3-key-modal.component'; +import { + RgwUserSubuserModalComponent +} from '../rgw-user-subuser-modal/rgw-user-subuser-modal.component'; +import { + RgwUserSwiftKeyModalComponent +} from '../rgw-user-swift-key-modal/rgw-user-swift-key-modal.component'; + +@Component({ + selector: 'cd-rgw-user-form', + templateUrl: './rgw-user-form.component.html', + styleUrls: ['./rgw-user-form.component.scss'] +}) +export class RgwUserFormComponent implements OnInit, OnDestroy { + + userForm: FormGroup; + routeParamsSubscribe: any; + editing = false; + error = false; + loading = false; + submitObservables: Observable[] = []; + + subusers: RgwUserSubuser[] = []; + s3Keys: RgwUserS3Key[] = []; + swiftKeys: RgwUserSwiftKey[] = []; + capabilities: RgwUserCapability[] = []; + + constructor(private formBuilder: FormBuilder, + private route: ActivatedRoute, + private router: Router, + private rgwUserService: RgwUserService, + private bsModalService: BsModalService) { + this.createForm(); + this.listenToChanges(); + } + + createForm() { + this.userForm = this.formBuilder.group({ + // General + 'user_id': [ + null, + [Validators.required], + [this.userIdValidator()] + ], + 'display_name': [ + null, + [Validators.required] + ], + 'email': [ + null, + [CdValidators.email] + ], + 'max_buckets': [ + null, + [Validators.min(0)] + ], + 'suspended': [ + false + ], + // S3 key + 'generate_key': [ + true + ], + 'access_key': [ + null, + [CdValidators.requiredIf({'generate_key': false})] + ], + 'secret_key': [ + null, + [CdValidators.requiredIf({'generate_key': false})] + ], + // User quota + 'user_quota_enabled': [ + false + ], + 'user_quota_max_size_unlimited': [ + true + ], + 'user_quota_max_size': [ + null, + [ + CdValidators.requiredIf({ + 'user_quota_enabled': true, + 'user_quota_max_size_unlimited': false + }), + this.quotaMaxSizeValidator + ] + ], + 'user_quota_max_objects_unlimited': [ + true + ], + 'user_quota_max_objects': [ + null, + [ + Validators.min(0), + CdValidators.requiredIf({ + 'user_quota_enabled': true, + 'user_quota_max_objects_unlimited': false + }) + ] + ], + // Bucket quota + 'bucket_quota_enabled': [ + false + ], + 'bucket_quota_max_size_unlimited': [ + true + ], + 'bucket_quota_max_size': [ + null, + [ + CdValidators.requiredIf({ + 'bucket_quota_enabled': true, + 'bucket_quota_max_size_unlimited': false + }), + this.quotaMaxSizeValidator + ] + ], + 'bucket_quota_max_objects_unlimited': [ + true + ], + 'bucket_quota_max_objects': [ + null, + [ + Validators.min(0), + CdValidators.requiredIf({ + 'bucket_quota_enabled': true, + 'bucket_quota_max_objects_unlimited': false + }) + ] + ] + }); + } + + listenToChanges() { + // Reset the validation status of various controls, especially those that are using + // the 'requiredIf' validator. This is necessary because the controls itself are not + // validated again if the status of their prerequisites have been changed. + this.userForm.get('generate_key').valueChanges.subscribe(() => { + ['access_key', 'secret_key'].forEach((path) => { + this.userForm.get(path).updateValueAndValidity({onlySelf: true}); + }); + }); + this.userForm.get('user_quota_enabled').valueChanges.subscribe(() => { + ['user_quota_max_size', 'user_quota_max_objects'].forEach((path) => { + this.userForm.get(path).updateValueAndValidity({onlySelf: true}); + }); + }); + this.userForm.get('user_quota_max_size_unlimited').valueChanges.subscribe(() => { + this.userForm.get('user_quota_max_size').updateValueAndValidity({onlySelf: true}); + }); + this.userForm.get('user_quota_max_objects_unlimited').valueChanges.subscribe(() => { + this.userForm.get('user_quota_max_objects').updateValueAndValidity({onlySelf: true}); + }); + this.userForm.get('bucket_quota_enabled').valueChanges.subscribe(() => { + ['bucket_quota_max_size', 'bucket_quota_max_objects'].forEach((path) => { + this.userForm.get(path).updateValueAndValidity({onlySelf: true}); + }); + }); + this.userForm.get('bucket_quota_max_size_unlimited').valueChanges.subscribe(() => { + this.userForm.get('bucket_quota_max_size').updateValueAndValidity({onlySelf: true}); + }); + this.userForm.get('bucket_quota_max_objects_unlimited').valueChanges.subscribe(() => { + this.userForm.get('bucket_quota_max_objects').updateValueAndValidity({onlySelf: true}); + }); + } + + ngOnInit() { + // Process route parameters. + this.routeParamsSubscribe = this.route.params + .subscribe((params: {uid: string}) => { + if (!params.hasOwnProperty('uid')) { + return; + } + this.loading = true; + // Load the user data in 'edit' mode. + this.editing = true; + // Load the user and quota information. + const observables = []; + observables.push(this.rgwUserService.get(params.uid)); + observables.push(this.rgwUserService.getQuota(params.uid)); + Observable.forkJoin(observables) + .subscribe((resp: any[]) => { + this.loading = false; + // Get the default values. + const defaults = _.clone(this.userForm.value); + // Extract the values displayed in the form. + let value = _.pick(resp[0], _.keys(this.userForm.value)); + // Map the quota values. + ['user', 'bucket'].forEach((type) => { + const quota = resp[1][type + '_quota']; + value[type + '_quota_enabled'] = quota.enabled; + if (quota.max_size < 0) { + value[type + '_quota_max_size_unlimited'] = true; + value[type + '_quota_max_size'] = null; + } else { + value[type + '_quota_max_size_unlimited'] = false; + value[type + '_quota_max_size'] = quota.max_size; + } + if (quota.max_objects < 0) { + value[type + '_quota_max_size_unlimited'] = true; + value[type + '_quota_max_size'] = null; + } else { + value[type + '_quota_max_objects_unlimited'] = false; + value[type + '_quota_max_objects'] = quota.max_objects; + } + }); + // Merge with default values. + value = _.merge(defaults, value); + // Update the form. + this.userForm.setValue(value); + + // Get the sub users. + this.subusers = resp[0].subusers; + + // Get the keys. + this.s3Keys = resp[0].keys; + this.swiftKeys = resp[0].swift_keys; + + // Process the capabilities. + const mapPerm = {'read, write': '*'}; + resp[0].caps.forEach((cap) => { + if (cap.perm in mapPerm) { + cap.perm = mapPerm[cap.perm]; + } + }); + this.capabilities = resp[0].caps; + }, (error) => { + this.error = error; + }); + }); + } + + ngOnDestroy() { + this.routeParamsSubscribe.unsubscribe(); + } + + goToListView() { + this.router.navigate(['/rgw/user']); + } + + onSubmit() { + // Exit immediately if the form isn't dirty. + if (this.userForm.pristine) { + this.goToListView(); + } + if (this.editing) { // Edit + if (this._isGeneralDirty()) { + const args = this._getApiPostArgs(); + this.submitObservables.push(this.rgwUserService.post(args)); + } + } else { // Add + const args = this._getApiPutArgs(); + this.submitObservables.push(this.rgwUserService.put(args)); + } + // Check if user quota has been modified. + if (this._isUserQuotaDirty()) { + const userQuotaArgs = this._getApiUserQuotaArgs(); + this.submitObservables.push(this.rgwUserService.putQuota(userQuotaArgs)); + } + // Check if bucket quota has been modified. + if (this._isBucketQuotaDirty()) { + const bucketQuotaArgs = this._getApiBucketQuotaArgs(); + this.submitObservables.push(this.rgwUserService.putQuota(bucketQuotaArgs)); + } + // Finally execute all observables. + Observable.forkJoin(this.submitObservables) + .subscribe(() => { + this.goToListView(); + }, () => { + // Reset the 'Submit' button. + this.userForm.setErrors({'cdSubmitButton': true}); + }); + } + + /** + * Validate the quota maximum size, e.g. 1096, 1K, 30M. Only integer numbers are valid, + * something like 1.9M is not recognized as valid. + */ + quotaMaxSizeValidator(control: AbstractControl): ValidationErrors | null { + if (isEmptyInputValue(control.value)) { + return null; + } + const m = RegExp('^(\\d+)\\s*(B|K(B|iB)?|M(B|iB)?|G(B|iB)?|T(B|iB)?)?$', + 'i').exec(control.value); + if (m === null) { + return {'quotaMaxSize': true}; + } + const bytes = new FormatterService().toBytes(control.value); + return (bytes < 1024) ? {'quotaMaxSize': true} : null; + } + + /** + * Validate the username. + */ + userIdValidator(): AsyncValidatorFn { + const rgwUserService = this.rgwUserService; + return (control: AbstractControl): Promise => { + return new Promise((resolve) => { + // Exit immediately if user has not interacted with the control yet + // or the control value is empty. + if (control.pristine || control.value === '') { + resolve(null); + return; + } + rgwUserService.exists(control.value) + .subscribe((resp: boolean) => { + if (!resp) { + resolve(null); + } else { + resolve({'userIdExists': true}); + } + }); + }); + }; + } + + /** + * Add/Update a subuser. + */ + setSubuser(subuser: RgwUserSubuser, index?: number) { + if (_.isNumber(index)) { // Modify + // Create an observable to modify the subuser when the form is submitted. + this.submitObservables.push(this.rgwUserService.addSubuser( + this.userForm.get('user_id').value, subuser.id, subuser.permissions, + subuser.secret_key, subuser.generate_secret)); + this.subusers[index] = subuser; + } else { // Add + // Create an observable to add the subuser when the form is submitted. + this.submitObservables.push(this.rgwUserService.addSubuser( + this.userForm.get('user_id').value, subuser.id, subuser.permissions, + subuser.secret_key, subuser.generate_secret)); + this.subusers.push(subuser); + // Add a Swift key. If the secret key is auto-generated, then visualize + // this to the user by displaying a notification instead of the key. + this.swiftKeys.push({ + 'user': subuser.id, + 'secret_key': subuser.generate_secret ? + 'Apply your changes first...' : subuser.secret_key + }); + } + // Mark the form as dirty to be able to submit it. + this.userForm.markAsDirty(); + } + + /** + * Delete a subuser. + * @param {number} index The subuser to delete. + */ + deleteSubuser(index: number) { + const subuser = this.subusers[index]; + // Create an observable to delete the subuser when the form is submitted. + this.submitObservables.push(this.rgwUserService.deleteSubuser( + this.userForm.get('user_id').value, subuser.id)); + // Remove the associated S3 keys. + this.s3Keys = this.s3Keys.filter((key) => { + return key.user !== subuser.id; + }); + // Remove the associated Swift keys. + this.swiftKeys = this.swiftKeys.filter((key) => { + return key.user !== subuser.id; + }); + // Remove the subuser to update the UI. + this.subusers.splice(index, 1); + // Mark the form as dirty to be able to submit it. + this.userForm.markAsDirty(); + } + + /** + * Add/Update a capability. + */ + setCapability(cap: RgwUserCapability, index?: number) { + const uid = this.userForm.get('user_id').value; + if (_.isNumber(index)) { // Modify + const oldCap = this.capabilities[index]; + // Note, the RadosGW Admin OPS API does not support the modification of + // user capabilities. Because of that it is necessary to delete it and + // then to re-add the capability with its new value/permission. + this.submitObservables.push(this.rgwUserService.deleteCapability( + uid, oldCap.type, oldCap.perm)); + this.submitObservables.push(this.rgwUserService.addCapability( + uid, cap.type, cap.perm)); + this.capabilities[index] = cap; + } else { // Add + // Create an observable to add the capability when the form is submitted. + this.submitObservables.push(this.rgwUserService.addCapability( + uid, cap.type, cap.perm)); + this.capabilities.push(cap); + } + // Mark the form as dirty to be able to submit it. + this.userForm.markAsDirty(); + } + + /** + * Delete the given capability: + * - Delete it from the local array to update the UI + * - Create an observable that will be executed on form submit + * @param {number} index The capability to delete. + */ + deleteCapability(index: number) { + const cap = this.capabilities[index]; + // Create an observable to delete the capability when the form is submitted. + this.submitObservables.push(this.rgwUserService.deleteCapability( + this.userForm.get('user_id').value, cap.type, cap.perm)); + // Remove the capability to update the UI. + this.capabilities.splice(index, 1); + // Mark the form as dirty to be able to submit it. + this.userForm.markAsDirty(); + } + + /** + * Add/Update a S3 key. + */ + setS3Key(key: RgwUserS3Key, index?: number) { + if (_.isNumber(index)) { // Modify + // Nothing to do here at the moment. + } else { // Add + // Create an observable to add the S3 key when the form is submitted. + this.submitObservables.push(this.rgwUserService.addS3Key( + this.userForm.get('user_id').value, key.user, key.access_key, + key.secret_key, key.generate_key)); + // If the access and the secret key are auto-generated, then visualize + // this to the user by displaying a notification instead of the key. + this.s3Keys.push({ + 'user': key.user, + 'access_key': key.generate_key ? 'Apply your changes first...' : key.access_key, + 'secret_key': key.generate_key ? 'Apply your changes first...' : key.secret_key + }); + } + // Mark the form as dirty to be able to submit it. + this.userForm.markAsDirty(); + } + + /** + * Delete a S3 key. + * @param {number} index The S3 key to delete. + */ + deleteS3Key(index: number) { + const key = this.s3Keys[index]; + // Create an observable to delete the S3 key when the form is submitted. + this.submitObservables.push(this.rgwUserService.deleteS3Key( + this.userForm.get('user_id').value, key.access_key)); + // Remove the S3 key to update the UI. + this.s3Keys.splice(index, 1); + // Mark the form as dirty to be able to submit it. + this.userForm.markAsDirty(); + } + + /** + * Show the specified subuser in a modal dialog. + * @param {number | undefined} index The subuser to show. + */ + showSubuserModal(index?: number) { + const uid = this.userForm.get('user_id').value; + const modalRef = this.bsModalService.show(RgwUserSubuserModalComponent); + if (_.isNumber(index)) { // Edit + const subuser = this.subusers[index]; + modalRef.content.setEditing(); + modalRef.content.setValues(uid, subuser.id, subuser.permissions); + } else { // Add + modalRef.content.setEditing(false); + modalRef.content.setValues(uid); + modalRef.content.setSubusers(this.subusers); + } + modalRef.content.submitAction.subscribe((subuser: RgwUserSubuser) => { + this.setSubuser(subuser, index); + }); + } + + /** + * Show the specified S3 key in a modal dialog. + * @param {number | undefined} index The S3 key to show. + */ + showS3KeyModal(index?: number) { + const modalRef = this.bsModalService.show(RgwUserS3KeyModalComponent); + if (_.isNumber(index)) { // View + const key = this.s3Keys[index]; + modalRef.content.setViewing(); + modalRef.content.setValues(key.user, key.access_key, key.secret_key); + } else { // Add + const candidates = this._getS3KeyUserCandidates(); + modalRef.content.setViewing(false); + modalRef.content.setUserCandidates(candidates); + modalRef.content.submitAction.subscribe((key: RgwUserS3Key) => { + this.setS3Key(key); + }); + } + } + + /** + * Show the specified Swift key in a modal dialog. + * @param {number} index The Swift key to show. + */ + showSwiftKeyModal(index: number) { + const modalRef = this.bsModalService.show(RgwUserSwiftKeyModalComponent); + const key = this.swiftKeys[index]; + modalRef.content.setValues(key.user, key.secret_key); + } + + /** + * Show the specified capability in a modal dialog. + * @param {number | undefined} index The S3 key to show. + */ + showCapabilityModal(index?: number) { + const modalRef = this.bsModalService.show(RgwUserCapabilityModalComponent); + if (_.isNumber(index)) { // Edit + const cap = this.capabilities[index]; + modalRef.content.setEditing(); + modalRef.content.setValues(cap.type, cap.perm); + } else { // Add + modalRef.content.setEditing(false); + modalRef.content.setCapabilities(this.capabilities); + } + modalRef.content.submitAction.subscribe((cap: RgwUserCapability) => { + this.setCapability(cap, index); + }); + } + + /** + * Check if the general user settings (display name, email, ...) have been modified. + * @return {Boolean} Returns TRUE if the general user settings have been modified. + */ + private _isGeneralDirty(): boolean { + return [ + 'display_name', + 'email', + 'max_buckets', + 'suspended' + ].some((path) => { + return this.userForm.get(path).dirty; + }); + } + + /** + * Check if the user quota has been modified. + * @return {Boolean} Returns TRUE if the user quota has been modified. + */ + private _isUserQuotaDirty(): boolean { + return [ + 'user_quota_enabled', + 'user_quota_max_size_unlimited', + 'user_quota_max_size', + 'user_quota_max_objects_unlimited', + 'user_quota_max_objects' + ].some((path) => { + return this.userForm.get(path).dirty; + }); + } + + /** + * Check if the bucket quota has been modified. + * @return {Boolean} Returns TRUE if the bucket quota has been modified. + */ + private _isBucketQuotaDirty(): boolean { + return [ + 'bucket_quota_enabled', + 'bucket_quota_max_size_unlimited', + 'bucket_quota_max_size', + 'bucket_quota_max_objects_unlimited', + 'bucket_quota_max_objects' + ].some((path) => { + return this.userForm.get(path).dirty; + }); + } + + /** + * Helper function to get the arguments of the API request when a new + * user is created. + */ + private _getApiPutArgs() { + const result = { + 'uid': this.userForm.get('user_id').value, + 'display-name': this.userForm.get('display_name').value + }; + const suspendedCtl = this.userForm.get('suspended'); + if (suspendedCtl.value) { + _.extend(result, {'suspended': suspendedCtl.value}); + } + const emailCtl = this.userForm.get('email'); + if (_.isString(emailCtl.value) && emailCtl.value.length > 0) { + _.extend(result, { 'email': emailCtl.value }); + } + const maxBucketsCtl = this.userForm.get('max_buckets'); + if (maxBucketsCtl.value > 0) { + _.extend(result, {'max-buckets': maxBucketsCtl.value}); + } + const generateKeyCtl = this.userForm.get('generate_key'); + if (!generateKeyCtl.value) { + _.extend(result, { + 'access-key': this.userForm.get('access_key').value, + 'secret-key': this.userForm.get('secret_key').value + }); + } else { + _.extend(result, {'generate-key': true}); + } + return result; + } + + /** + * Helper function to get the arguments for the API request when the user + * configuration has been modified. + */ + private _getApiPostArgs() { + const result = { + 'uid': this.userForm.get('user_id').value + }; + const argsMap = { + 'display-name': 'display_name', + 'email': 'email', + 'max-buckets': 'max_buckets', + 'suspended': 'suspended' + }; + for (const key of Object.keys(argsMap)) { + const ctl = this.userForm.get(argsMap[key]); + if (ctl.dirty) { + result[key] = ctl.value; + } + } + return result; + } + + /** + * Helper function to get the arguments for the API request when the user + * quota configuration has been modified. + */ + private _getApiUserQuotaArgs(): object { + const result = { + 'uid': this.userForm.get('user_id').value, + 'quota-type': 'user', + 'enabled': this.userForm.get('user_quota_enabled').value, + 'max-size-kb': -1, + 'max-objects': -1 + }; + if (!this.userForm.get('user_quota_max_size_unlimited').value) { + // Convert the given value to bytes. + const bytes = new FormatterService().toBytes(this.userForm.get( + 'user_quota_max_size').value); + // Finally convert the value to KiB. + result['max-size-kb'] = (bytes / 1024).toFixed(0) as any; + } + if (!this.userForm.get('user_quota_max_objects_unlimited').value) { + result['max-objects'] = this.userForm.get('user_quota_max_objects').value; + } + return result; + } + + /** + * Helper function to get the arguments for the API request when the bucket + * quota configuration has been modified. + */ + private _getApiBucketQuotaArgs(): object { + const result = { + 'uid': this.userForm.get('user_id').value, + 'quota-type': 'bucket', + 'enabled': this.userForm.get('bucket_quota_enabled').value, + 'max-size-kb': -1, + 'max-objects': -1 + }; + if (!this.userForm.get('bucket_quota_max_size_unlimited').value) { + // Convert the given value to bytes. + const bytes = new FormatterService().toBytes(this.userForm.get( + 'bucket_quota_max_size').value); + // Finally convert the value to KiB. + result['max-size-kb'] = (bytes / 1024).toFixed(0) as any; + } + if (!this.userForm.get('bucket_quota_max_objects_unlimited').value) { + result['max-objects'] = this.userForm.get('bucket_quota_max_objects').value; + } + return result; + } + + /** + * Helper method to get the user candidates for S3 keys. + * @returns {Array} Returns a list of user identifiers. + */ + private _getS3KeyUserCandidates() { + let result = []; + // Add the current user id. + const user_id = this.userForm.get('user_id').value; + if (_.isString(user_id) && !_.isEmpty(user_id)) { + result.push(user_id); + } + // Append the subusers. + this.subusers.forEach((subUser) => { + result.push(subUser.id); + }); + // Note that it's possible to create multiple S3 key pairs for a user, + // thus we append already configured users, too. + this.s3Keys.forEach((key) => { + result.push(key.user); + }); + result = _.uniq(result); + return result; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html index 66434a0e66605..b39c007a3e587 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html @@ -7,13 +7,76 @@ aria-current="page">Users - +
+
+ + + + + +
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.spec.ts index 1b0c18b8550db..04cafb7ad3cd4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.spec.ts @@ -1,7 +1,9 @@ import { HttpClientModule } from '@angular/common/http'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; import { BsDropdownModule } from 'ngx-bootstrap'; +import { BsModalService } from 'ngx-bootstrap/modal'; import { TabsModule } from 'ngx-bootstrap/tabs'; import { SharedModule } from '../../../shared/shared.module'; @@ -20,9 +22,13 @@ describe('RgwUserListComponent', () => { ], imports: [ HttpClientModule, + RouterTestingModule, BsDropdownModule.forRoot(), TabsModule.forRoot(), SharedModule + ], + providers: [ + BsModalService ] }) .compileComponents(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts index 2953ab7523ac4..3304841d7adf3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts @@ -1,6 +1,16 @@ -import { Component } from '@angular/core'; +import { Component, ViewChild } from '@angular/core'; +import { Router } from '@angular/router'; + +import { BsModalService } from 'ngx-bootstrap'; +import 'rxjs/add/observable/forkJoin'; +import { Observable } from 'rxjs/Observable'; +import { Subscriber } from 'rxjs/Subscriber'; import { RgwUserService } from '../../../shared/api/rgw-user.service'; +import { + DeletionModalComponent +} from '../../../shared/components/deletion-modal/deletion-modal.component'; +import { TableComponent } from '../../../shared/datatable/table/table.component'; import { CellTemplate } from '../../../shared/enum/cell-template.enum'; import { CdTableColumn } from '../../../shared/models/cd-table-column'; import { CdTableSelection } from '../../../shared/models/cd-table-selection'; @@ -12,11 +22,15 @@ import { CdTableSelection } from '../../../shared/models/cd-table-selection'; }) export class RgwUserListComponent { + @ViewChild(TableComponent) table: TableComponent; + columns: CdTableColumn[] = []; users: object[] = []; selection: CdTableSelection = new CdTableSelection(); - constructor(private rgwUserService: RgwUserService) { + constructor(private router: Router, + private rgwUserService: RgwUserService, + private bsModalService: BsModalService) { this.columns = [ { name: 'Username', @@ -61,4 +75,25 @@ export class RgwUserListComponent { updateSelection(selection: CdTableSelection) { this.selection = selection; } + + deleteAction() { + const modalRef = this.bsModalService.show(DeletionModalComponent); + modalRef.content.setUp({ + metaType: this.selection.hasSingleSelection ? 'user' : 'users', + deletionObserver: (): Observable => { + return new Observable((observer: Subscriber) => { + Observable.forkJoin( + this.selection.selected.map((user: any) => { + return this.rgwUserService.delete(user.user_id); + })) + .subscribe(null, null, () => { + observer.complete(); + // Finally reload the data table content. + this.table.refreshBtn(); + }); + }); + }, + modalRef: modalRef + }); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.html new file mode 100644 index 0000000000000..64b29025eab32 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.html @@ -0,0 +1,158 @@ + +
+ + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.spec.ts new file mode 100644 index 0000000000000..382f9df2bac3b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.spec.ts @@ -0,0 +1,34 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; + +import { BsModalRef } from 'ngx-bootstrap/modal'; + +import { SharedModule } from '../../../shared/shared.module'; +import { RgwUserS3KeyModalComponent } from './rgw-user-s3-key-modal.component'; + +describe('RgwUserS3KeyModalComponent', () => { + let component: RgwUserS3KeyModalComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ RgwUserS3KeyModalComponent ], + imports: [ + ReactiveFormsModule, + SharedModule + ], + providers: [ BsModalRef ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RgwUserS3KeyModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.ts new file mode 100644 index 0000000000000..613962f536584 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-s3-key-modal/rgw-user-s3-key-modal.component.ts @@ -0,0 +1,97 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +import * as _ from 'lodash'; +import { BsModalRef } from 'ngx-bootstrap/modal/bs-modal-ref.service'; + +import { CdValidators } from '../../../shared/validators/cd-validators'; +import { RgwUserS3Key } from '../models/rgw-user-s3-key'; + +@Component({ + selector: 'cd-rgw-user-s3-key-modal', + templateUrl: './rgw-user-s3-key-modal.component.html', + styleUrls: ['./rgw-user-s3-key-modal.component.scss'] +}) +export class RgwUserS3KeyModalComponent { + + /** + * The event that is triggered when the 'Add' button as been pressed. + */ + @Output() submitAction = new EventEmitter(); + + formGroup: FormGroup; + viewing = true; + userCandidates: string[] = []; + + constructor(private formBuilder: FormBuilder, + public bsModalRef: BsModalRef) { + this.createForm(); + this.listenToChanges(); + } + + createForm() { + this.formGroup = this.formBuilder.group({ + 'user': [ + null, + [Validators.required] + ], + 'generate_key': [ + true + ], + 'access_key': [ + null, + [CdValidators.requiredIf({'generate_key': false})] + ], + 'secret_key': [ + null, + [CdValidators.requiredIf({'generate_key': false})] + ] + }); + } + + listenToChanges() { + // Reset the validation status of various controls, especially those that are using + // the 'requiredIf' validator. This is necessary because the controls itself are not + // validated again if the status of their prerequisites have been changed. + this.formGroup.get('generate_key').valueChanges.subscribe(() => { + ['access_key', 'secret_key'].forEach((path) => { + this.formGroup.get(path).updateValueAndValidity({onlySelf: true}); + }); + }); + } + + /** + * Set the 'viewing' flag. If set to TRUE, the modal dialog is in 'View' mode, + * otherwise in 'Add' mode. According to the mode the dialog and its controls + * behave different. + * @param {boolean} viewing + */ + setViewing(viewing: boolean = true) { + this.viewing = viewing; + } + + /** + * Set the values displayed in the dialog. + */ + setValues(user: string, access_key: string, secret_key: string) { + this.formGroup.setValue({ + 'user': user, + 'generate_key': _.isEmpty(access_key), + 'access_key': access_key, + 'secret_key': secret_key + }); + } + + /** + * Set the user candidates displayed in the 'Username' dropdown box. + */ + setUserCandidates(candidates: string[]) { + this.userCandidates = candidates; + } + + onSubmit() { + const key: RgwUserS3Key = this.formGroup.value; + this.submitAction.emit(key); + this.bsModalRef.hide(); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.html new file mode 100644 index 0000000000000..12b7148464e9b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.html @@ -0,0 +1,168 @@ + +
+ + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.spec.ts new file mode 100644 index 0000000000000..6e6691a90dc7e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.spec.ts @@ -0,0 +1,75 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; + +import { BsModalRef } from 'ngx-bootstrap/modal'; + +import { SharedModule } from '../../../shared/shared.module'; +import { RgwUserSubuserModalComponent } from './rgw-user-subuser-modal.component'; + +describe('RgwUserSubuserModalComponent', () => { + let component: RgwUserSubuserModalComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ RgwUserSubuserModalComponent ], + imports: [ + ReactiveFormsModule, + SharedModule + ], + providers: [ BsModalRef ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RgwUserSubuserModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('subuserValidator', () => { + beforeEach(() => { + component.editing = false; + component.subusers = [ + {id: 'Edith', permissions: 'full-control'}, + {id: 'Edith:images', permissions: 'read-write'} + ]; + }); + + it('should validate subuser (1/5)', () => { + component.editing = true; + const validatorFn = component.subuserValidator(); + const resp = validatorFn(new FormControl()); + expect(resp).toBe(null); + }); + + it('should validate subuser (2/5)', () => { + const validatorFn = component.subuserValidator(); + const resp = validatorFn(new FormControl('')); + expect(resp).toBe(null); + }); + + it('should validate subuser (3/5)', () => { + const validatorFn = component.subuserValidator(); + const resp = validatorFn(new FormControl('Melissa')); + expect(resp).toBe(null); + }); + + it('should validate subuser (4/5)', () => { + const validatorFn = component.subuserValidator(); + const resp = validatorFn(new FormControl('Edith')); + expect(resp.subuserIdExists).toBeTruthy(); + }); + + it('should validate subuser (5/5)', () => { + const validatorFn = component.subuserValidator(); + const resp = validatorFn(new FormControl('images')); + expect(resp.subuserIdExists).toBeTruthy(); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.ts new file mode 100644 index 0000000000000..ac3597e40ac00 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-subuser-modal/rgw-user-subuser-modal.component.ts @@ -0,0 +1,155 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormGroup, + ValidationErrors, + ValidatorFn, + Validators +} from '@angular/forms'; + +import * as _ from 'lodash'; +import { BsModalRef } from 'ngx-bootstrap/modal/bs-modal-ref.service'; + +import { CdValidators, isEmptyInputValue } from '../../../shared/validators/cd-validators'; +import { RgwUserSubuser } from '../models/rgw-user-subuser'; + +@Component({ + selector: 'cd-rgw-user-subuser-modal', + templateUrl: './rgw-user-subuser-modal.component.html', + styleUrls: ['./rgw-user-subuser-modal.component.scss'] +}) +export class RgwUserSubuserModalComponent { + + /** + * The event that is triggered when the 'Add' or 'Update' button + * has been pressed. + */ + @Output() submitAction = new EventEmitter(); + + formGroup: FormGroup; + editing = true; + subusers: RgwUserSubuser[] = []; + + constructor(private formBuilder: FormBuilder, + public bsModalRef: BsModalRef) { + this.createForm(); + this.listenToChanges(); + } + + createForm() { + this.formGroup = this.formBuilder.group({ + 'uid': [ + null + ], + 'subuid': [ + null, + [ + Validators.required, + this.subuserValidator() + ] + ], + 'perm': [ + null, + [Validators.required] + ], + // Swift key + 'generate_secret': [ + true + ], + 'secret_key': [ + null, + [CdValidators.requiredIf({'generate_secret': false})] + ] + }); + } + + listenToChanges() { + // Reset the validation status of various controls, especially those that are using + // the 'requiredIf' validator. This is necessary because the controls itself are not + // validated again if the status of their prerequisites have been changed. + this.formGroup.get('generate_secret').valueChanges.subscribe(() => { + ['secret_key'].forEach((path) => { + this.formGroup.get(path).updateValueAndValidity({onlySelf: true}); + }); + }); + } + + /** + * Validates whether the subuser already exists. + */ + subuserValidator(): ValidatorFn { + const self = this; + return (control: AbstractControl): ValidationErrors | null => { + if (self.editing) { + return null; + } + if (isEmptyInputValue(control.value)) { + return null; + } + const found = self.subusers.some((subuser) => { + return _.isEqual(self.getSubuserName(subuser.id), control.value); + }); + return found ? {'subuserIdExists': true} : null; + }; + } + + /** + * Get the subuser name. + * Examples: + * 'johndoe' => 'johndoe' + * 'janedoe:xyz' => 'xyz' + * @param {string} value The value to process. + * @returns {string} Returns the user ID. + */ + private getSubuserName(value: string) { + if (_.isEmpty(value)) { + return value; + } + const matches = value.match(/([^:]+)(:(.+))?/); + return _.isUndefined(matches[3]) ? matches[1] : matches[3]; + } + + /** + * Set the 'editing' flag. If set to TRUE, the modal dialog is in 'Edit' mode, + * otherwise in 'Add' mode. According to the mode the dialog and its controls + * behave different. + * @param {boolean} viewing + */ + setEditing(editing: boolean = true) { + this.editing = editing; + } + + /** + * Set the values displayed in the dialog. + */ + setValues(uid: string, subuser: string = '', permissions: string = '') { + this.formGroup.setValue({ + 'uid': uid, + 'subuid': this.getSubuserName(subuser), + 'perm': permissions, + 'generate_secret': true, + 'secret_key': null + }); + } + + /** + * Set the current capabilities of the user. + */ + setSubusers(subusers: RgwUserSubuser[]) { + this.subusers = subusers; + } + + onSubmit() { + // Get the values from the form and create an object that is sent + // by the triggered submit action event. + const values = this.formGroup.value; + const subuser = new RgwUserSubuser; + subuser.id = `${values.uid}:${values.subuid}`; + subuser.permissions = values.perm; + subuser.generate_secret = values.generate_secret; + subuser.secret_key = values.secret_key; + this.submitAction.emit(subuser); + this.bsModalRef.hide(); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.html new file mode 100644 index 0000000000000..67253144b7716 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.html @@ -0,0 +1,67 @@ + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.spec.ts new file mode 100644 index 0000000000000..ac4ae5386ffcc --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.spec.ts @@ -0,0 +1,32 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; + +import { BsModalRef } from 'ngx-bootstrap/modal'; + +import { RgwUserSwiftKeyModalComponent } from './rgw-user-swift-key-modal.component'; + +describe('RgwUserSwiftKeyModalComponent', () => { + let component: RgwUserSwiftKeyModalComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ RgwUserSwiftKeyModalComponent ], + imports: [ + FormsModule + ], + providers: [ BsModalRef ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RgwUserSwiftKeyModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.ts new file mode 100644 index 0000000000000..4ebe4ad3d9e32 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-swift-key-modal/rgw-user-swift-key-modal.component.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; + +import { BsModalRef } from 'ngx-bootstrap/modal/bs-modal-ref.service'; + +@Component({ + selector: 'cd-rgw-user-swift-key-modal', + templateUrl: './rgw-user-swift-key-modal.component.html', + styleUrls: ['./rgw-user-swift-key-modal.component.scss'] +}) +export class RgwUserSwiftKeyModalComponent { + + user: string; + secret_key: string; + + constructor(public bsModalRef: BsModalRef) {} + + /** + * Set the values displayed in the dialog. + */ + setValues(user: string, secret_key: string) { + this.user = user; + this.secret_key = secret_key; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts index 6e5aaa5b9d0cd..90623dc98684d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts @@ -1,32 +1,59 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { TabsModule } from 'ngx-bootstrap/tabs'; +import { BsDropdownModule, ModalModule, TabsModule, TooltipModule } from 'ngx-bootstrap'; +import { AppRoutingModule } from '../../app-routing.module'; import { SharedModule } from '../../shared/shared.module'; import { PerformanceCounterModule } from '../performance-counter/performance-counter.module'; import { RgwBucketDetailsComponent } from './rgw-bucket-details/rgw-bucket-details.component'; +import { RgwBucketFormComponent } from './rgw-bucket-form/rgw-bucket-form.component'; import { RgwBucketListComponent } from './rgw-bucket-list/rgw-bucket-list.component'; import { RgwDaemonDetailsComponent } from './rgw-daemon-details/rgw-daemon-details.component'; import { RgwDaemonListComponent } from './rgw-daemon-list/rgw-daemon-list.component'; +import { + RgwUserCapabilityModalComponent +} from './rgw-user-capability-modal/rgw-user-capability-modal.component'; import { RgwUserDetailsComponent } from './rgw-user-details/rgw-user-details.component'; +import { RgwUserFormComponent } from './rgw-user-form/rgw-user-form.component'; import { RgwUserListComponent } from './rgw-user-list/rgw-user-list.component'; +import { + RgwUserS3KeyModalComponent +} from './rgw-user-s3-key-modal/rgw-user-s3-key-modal.component'; +import { + RgwUserSubuserModalComponent +} from './rgw-user-subuser-modal/rgw-user-subuser-modal.component'; +import { + RgwUserSwiftKeyModalComponent +} from './rgw-user-swift-key-modal/rgw-user-swift-key-modal.component'; @NgModule({ entryComponents: [ RgwDaemonDetailsComponent, RgwBucketDetailsComponent, - RgwUserDetailsComponent + RgwUserDetailsComponent, + RgwUserSwiftKeyModalComponent, + RgwUserS3KeyModalComponent, + RgwUserCapabilityModalComponent, + RgwUserSubuserModalComponent ], imports: [ CommonModule, SharedModule, + AppRoutingModule, + FormsModule, + ReactiveFormsModule, PerformanceCounterModule, - TabsModule.forRoot() + BsDropdownModule.forRoot(), + TabsModule.forRoot(), + TooltipModule.forRoot(), + ModalModule.forRoot() ], exports: [ RgwDaemonListComponent, RgwDaemonDetailsComponent, + RgwBucketFormComponent, RgwBucketListComponent, RgwBucketDetailsComponent, RgwUserListComponent, @@ -35,10 +62,17 @@ import { RgwUserListComponent } from './rgw-user-list/rgw-user-list.component'; declarations: [ RgwDaemonListComponent, RgwDaemonDetailsComponent, + RgwBucketFormComponent, RgwBucketListComponent, RgwBucketDetailsComponent, RgwUserListComponent, - RgwUserDetailsComponent + RgwUserDetailsComponent, + RgwBucketFormComponent, + RgwUserFormComponent, + RgwUserSwiftKeyModalComponent, + RgwUserS3KeyModalComponent, + RgwUserCapabilityModalComponent, + RgwUserSubuserModalComponent ] }) export class RgwModule { } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts index adcfc4a0fe933..2b357e7edf6d9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts @@ -1,11 +1,11 @@ -import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; +import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; + +import * as _ from 'lodash'; import 'rxjs/add/observable/forkJoin'; import 'rxjs/add/observable/of'; import { Observable } from 'rxjs/Observable'; -import * as _ from 'lodash'; - @Injectable() export class RgwBucketService { @@ -13,32 +13,43 @@ export class RgwBucketService { constructor(private http: HttpClient) { } + /** + * Get the list of buckets. + * @return {Observable} + */ list() { - return this.http.get(this.url) + return this.enumerate() .flatMap((buckets: string[]) => { if (buckets.length > 0) { return Observable.forkJoin( buckets.map((bucket: string) => { return this.get(bucket); - }) - ); + })); } return Observable.of([]); }); } + /** + * Get the list of bucket names. + * @return {Observable} + */ + enumerate() { + return this.http.get(this.url); + } + get(bucket: string) { let params = new HttpParams(); params = params.append('bucket', bucket); - return this.http.get(this.url, { params: params }); + return this.http.get(this.url, {params: params}); } create(bucket: string, uid: string) { - const body = JSON.stringify({ + const body = { 'bucket': bucket, 'uid': uid - }); - return this.http.post(`/api/rgw/bucket`, body); + }; + return this.http.post('/api/rgw/bucket', body); } update(bucketId: string, bucket: string, uid: string) { @@ -46,27 +57,26 @@ export class RgwBucketService { params = params.append('bucket', bucket); params = params.append('bucket-id', bucketId as string); params = params.append('uid', uid); - return this.http.put(this.url, null, { params: params }); + return this.http.put(this.url, null, {params: params}); } delete(bucket: string, purgeObjects = true) { let params = new HttpParams(); params = params.append('bucket', bucket); params = params.append('purge-objects', purgeObjects ? 'true' : 'false'); - return this.http.delete(this.url, { params: params }); + return this.http.delete(this.url, {params: params}); } - find(bucket: string) { - let params = new HttpParams(); - params = params.append('bucket', bucket); - return this.http.get(this.url, { params: params }) - .flatMap((resp: object | null) => { - // Make sure we have received valid data. - if ((null === resp) || (!_.isObjectLike(resp))) { - return Observable.of([]); - } - // Return an array to be able to support wildcard searching someday. - return Observable.of([resp]); + /** + * Check if the specified bucket exists. + * @param {string} uid The bucket name to check. + * @return {Observable} + */ + exists(bucket: string) { + return this.enumerate() + .flatMap((resp: string[]) => { + const index = _.indexOf(resp, bucket); + return Observable.of(-1 !== index); }); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts index 9585fdfa501a5..06766f991169d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts @@ -1,5 +1,7 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; + +import * as _ from 'lodash'; import 'rxjs/add/observable/forkJoin'; import 'rxjs/add/observable/of'; import { Observable } from 'rxjs/Observable'; @@ -11,6 +13,10 @@ export class RgwUserService { constructor(private http: HttpClient) { } + /** + * Get the list of users. + * @return {Observable} + */ list() { return this.enumerate() .flatMap((uids: string[]) => { @@ -18,13 +24,16 @@ export class RgwUserService { return Observable.forkJoin( uids.map((uid: string) => { return this.get(uid); - }) - ); + })); } return Observable.of([]); }); } + /** + * Get the list of usernames. + * @return {Observable} + */ enumerate() { return this.http.get('/api/rgw/proxy/metadata/user'); } @@ -32,7 +41,7 @@ export class RgwUserService { get(uid: string) { let params = new HttpParams(); params = params.append('uid', uid); - return this.http.get(this.url, { params: params }); + return this.http.get(this.url, {params: params}); } getQuota(uid: string) { @@ -40,4 +49,111 @@ export class RgwUserService { params = params.append('uid', uid); return this.http.get(`${this.url}?quota`, {params: params}); } + + put(args: object) { + let params = new HttpParams(); + _.keys(args).forEach((key) => { + params = params.append(key, args[key]); + }); + return this.http.put(this.url, null, {params: params}); + } + + putQuota(args: object) { + let params = new HttpParams(); + _.keys(args).forEach((key) => { + params = params.append(key, args[key]); + }); + return this.http.put(`${this.url}?quota`, null, {params: params}); + } + + post(args: object) { + let params = new HttpParams(); + _.keys(args).forEach((key) => { + params = params.append(key, args[key]); + }); + return this.http.post(this.url, null, {params: params}); + } + + delete(uid: string) { + let params = new HttpParams(); + params = params.append('uid', uid); + return this.http.delete(this.url, {params: params}); + } + + addSubuser(uid: string, subuser: string, permissions: string, + secretKey: string, generateSecret: boolean) { + const mapPermissions = { + 'full-control': 'full', + 'read-write': 'readwrite' + }; + let params = new HttpParams(); + params = params.append('uid', uid); + params = params.append('subuser', subuser); + params = params.append('key-type', 'swift'); + params = params.append('access', (permissions in mapPermissions) ? + mapPermissions[permissions] : permissions); + if (generateSecret) { + params = params.append('generate-secret', 'true'); + } else { + params = params.append('secret-key', secretKey); + } + return this.http.put(this.url, null, {params: params}); + } + + deleteSubuser(uid: string, subuser: string) { + let params = new HttpParams(); + params = params.append('uid', uid); + params = params.append('subuser', subuser); + params = params.append('purge-keys', 'true'); + return this.http.delete(this.url, {params: params}); + } + + addCapability(uid: string, type: string, perm: string) { + let params = new HttpParams(); + params = params.append('uid', uid); + params = params.append('user-caps', `${type}=${perm}`); + return this.http.put(`${this.url}?caps`, null, {params: params}); + } + + deleteCapability(uid: string, type: string, perm: string) { + let params = new HttpParams(); + params = params.append('uid', uid); + params = params.append('user-caps', `${type}=${perm}`); + return this.http.delete(`${this.url}?caps`, {params: params}); + } + + addS3Key(uid: string, subuser: string, accessKey: string, + secretKey: string, generateKey: boolean) { + let params = new HttpParams(); + params = params.append('uid', uid); + params = params.append('key-type', 's3'); + params = params.append('generate-key', generateKey ? 'true' : 'false'); + if (!generateKey) { + params = params.append('access-key', accessKey); + params = params.append('secret-key', secretKey); + } + params = params.append('subuser', subuser); + return this.http.put(`${this.url}?key`, null, {params: params}); + } + + deleteS3Key(uid: string, accessKey: string) { + let params = new HttpParams(); + params = params.append('uid', uid); + params = params.append('key-type', 's3'); + params = params.append('access-key', accessKey); + return this.http.delete(`${this.url}?key`, {params: params}); + } + + /** + * Check if the specified user ID exists. + * @param {string} uid The user ID to check. + * @return {Observable} + */ + exists(uid: string) { + return this.enumerate() + .flatMap((resp: string[]) => { + const index = _.indexOf(resp, uid); + return Observable.of(-1 !== index); + }); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/password-button.directive.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/password-button.directive.ts index b375ba256b0a2..3e152a3334ade 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/password-button.directive.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/password-button.directive.ts @@ -4,24 +4,28 @@ import { Directive, ElementRef, HostListener, Input, OnInit, Renderer2 } from '@ selector: '[cdPasswordButton]' }) export class PasswordButtonDirective implements OnInit { - private inputElement: any; - private iElement: any; + private iElement: HTMLElement; @Input('cdPasswordButton') private cdPasswordButton: string; - constructor(private el: ElementRef, private renderer: Renderer2) { } + constructor(private elementRef: ElementRef, + private renderer: Renderer2) {} ngOnInit() { - this.inputElement = document.getElementById(this.cdPasswordButton); this.iElement = this.renderer.createElement('i'); this.renderer.addClass(this.iElement, 'icon-prepend'); this.renderer.addClass(this.iElement, 'fa'); - this.renderer.appendChild(this.el.nativeElement, this.iElement); + this.renderer.appendChild(this.elementRef.nativeElement, this.iElement); this.update(); } + private getInputElement() { + return document.getElementById(this.cdPasswordButton) as HTMLInputElement; + } + private update() { - if (this.inputElement.type === 'text') { + const inputElement = this.getInputElement(); + if (inputElement && (inputElement.type === 'text')) { this.renderer.removeClass(this.iElement, 'fa-eye'); this.renderer.addClass(this.iElement, 'fa-eye-slash'); } else { @@ -32,8 +36,9 @@ export class PasswordButtonDirective implements OnInit { @HostListener('click') onClick() { + const inputElement = this.getInputElement(); // Modify the type of the input field. - this.inputElement.type = (this.inputElement.type === 'password') ? 'text' : 'password'; + inputElement.type = (inputElement.type === 'password') ? 'text' : 'password'; // Update the button icon/tooltip. this.update(); } -- 2.39.5