@ApiController('rgw/proxy/{path:.*}')
@AuthRequired()
class RgwProxy(BaseController):
+
@cherrypy.expose
def __call__(self, path, **params):
try:
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)
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';
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] },
--- /dev/null
+export class RgwUserCapability {
+ type: string;
+ perm: string;
+}
--- /dev/null
+export class RgwUserS3Key {
+ user: string;
+ generate_key?: boolean;
+ access_key: string;
+ secret_key: string;
+}
--- /dev/null
+export class RgwUserSubuser {
+ id: string;
+ permissions: string;
+ generate_secret?: boolean;
+ secret_key?: string;
+}
--- /dev/null
+export class RgwUserSwiftKey {
+ user: string;
+ secret_key: string;
+}
--- /dev/null
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item"
+ i18n>Object Gateway</li>
+ <li class="breadcrumb-item">
+ <a routerLink="/rgw/bucket"
+ i18n>Buckets</a>
+ </li>
+ <li class="breadcrumb-item active"
+ aria-current="page"
+ i18n>
+ {editing, select, 1 {Edit} other {Add}}
+ </li>
+ </ol>
+</nav>
+
+<cd-loading-panel *ngIf="editing && loading && !error"
+ i18n>
+ Loading bucket data...
+</cd-loading-panel>
+<cd-error-panel *ngIf="editing && error"
+ (backAction)="goToListView()"
+ i18n>
+ The bucket data could not be loaded.
+</cd-error-panel>
+
+<div class="col-sm-12 col-lg-6"
+ *ngIf="!loading && !error">
+ <form name="bucketForm"
+ class="form-horizontal"
+ #frm="ngForm"
+ [formGroup]="bucketForm"
+ novalidate>
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <h3 class="panel-title"
+ i18n>
+ {editing, select, 1 {Edit} other {Add}} bucket
+ </h3>
+ </div>
+ <div class="panel-body">
+
+ <!-- Id -->
+ <div class="form-group"
+ *ngIf="editing">
+ <label i18n
+ class="col-sm-3 control-label"
+ for="id">Id</label>
+ <div class="col-sm-9">
+ <input id="id"
+ name="id"
+ class="form-control"
+ type="text"
+ formControlName="id"
+ readonly>
+ </div>
+ </div>
+
+ <!-- Name -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (frm.submitted || bucketForm.controls.bucket.dirty) && bucketForm.controls.bucket.invalid}">
+ <label i18n
+ class="control-label col-sm-3"
+ for="bucket">Name
+ <span class="required"
+ *ngIf="!editing"></span>
+ </label>
+ <div class="col-sm-9">
+ <input id="bucket"
+ name="bucket"
+ class="form-control"
+ type="text"
+ i18n-placeholder
+ placeholder="Name..."
+ formControlName="bucket"
+ [readonly]="editing"
+ autofocus>
+ <span i18n
+ class="help-block"
+ *ngIf="(frm.submitted || bucketForm.controls.bucket.dirty) && bucketForm.controls.bucket.hasError('required')">
+ This field is required.
+ </span>
+ <span i18n
+ class="help-block"
+ *ngIf="(frm.submitted || bucketForm.controls.bucket.dirty) && bucketForm.controls.bucket.hasError('bucketNameInvalid')">
+ The value is not valid.
+ </span>
+ <span i18n
+ class="help-block"
+ *ngIf="(frm.submitted || bucketForm.controls.bucket.dirty) && bucketForm.controls.bucket.hasError('bucketNameExists')">
+ The chosen name is already in use.
+ </span>
+ </div>
+ </div>
+
+ <!-- Owner -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (frm.submitted || bucketForm.controls.owner.dirty) && bucketForm.controls.owner.invalid}">
+ <label i18n
+ class="control-label col-sm-3"
+ for="owner">Owner
+ <span class="required"></span>
+ </label>
+ <div class="col-sm-9">
+ <select id="owner"
+ name="owner"
+ class="form-control"
+ formControlName="owner">
+ <option i18n
+ *ngIf="owners === null"
+ [ngValue]="null">Loading...
+ </option>
+ <option i18n
+ *ngIf="owners !== null"
+ [ngValue]="null">-- Select a user --
+ </option>
+ <option *ngFor="let owner of owners"
+ [value]="owner">{{ owner }}</option>
+ </select>
+ <span i18n
+ class="help-block"
+ *ngIf="(frm.submitted || bucketForm.controls.owner.dirty) && bucketForm.controls.owner.hasError('required')">
+ This field is required.
+ </span>
+ </div>
+ </div>
+
+ </div>
+ <div class="panel-footer">
+ <div class="button-group text-right">
+ <cd-submit-button (submitAction)="submit()"
+ [form]="bucketForm"
+ i18n>
+ {editing, select, 1 {Update} other {Add}}
+ </cd-submit-button>
+ <button i18n
+ type="button"
+ class="btn btn-sm btn-default"
+ routerLink="/rgw/bucket">
+ Back
+ </button>
+ </div>
+ </div>
+ </div>
+ </form>
+</div>
--- /dev/null
+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<RgwBucketFormComponent>;
+ let queryResult: Array<string> = [];
+
+ 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();
+ });
+ }
+ });
+ });
+});
--- /dev/null
+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<ValidationErrors | null> => {
+ 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});
+ }
+ });
+ });
+ };
+ }
+}
aria-current="page">Buckets</li>
</ol>
</nav>
-<cd-table [data]="buckets"
+<cd-table #table
+ [autoReload]="false"
+ [data]="buckets"
[columns]="columns"
columnMode="flex"
- selectionType="single"
+ selectionType="multi"
(updateSelection)="updateSelection($event)"
identifier="bucket"
- (fetchData)="getBucketList()"
- #table>
+ (fetchData)="getBucketList()">
+ <div class="table-actions">
+ <div class="btn-group" dropdown>
+ <button type="button"
+ class="btn btn-sm btn-primary"
+ *ngIf="!selection.hasSelection"
+ routerLink="/rgw/bucket/add">
+ <i class="fa fa-fw fa-plus"></i>
+ <ng-container i18n>Add</ng-container>
+ </button>
+ <button type="button"
+ class="btn btn-sm btn-primary"
+ *ngIf="selection.hasSingleSelection"
+ routerLink="/rgw/bucket/edit/{{ selection.first()?.bucket }}">
+ <i class="fa fa-fw fa-pencil"></i>
+ <ng-container i18n>Edit</ng-container>
+ </button>
+ <button type="button"
+ class="btn btn-sm btn-primary"
+ *ngIf="selection.hasMultiSelection"
+ (click)="deleteAction()">
+ <i class="fa fa-fw fa-trash-o"></i>
+ <ng-container i18n>Delete</ng-container>
+ </button>
+ <button type="button"
+ class="btn btn-sm btn-primary dropdown-toggle dropdown-toggle-split"
+ dropdownToggle>
+ <span class="caret"></span>
+ <span class="sr-only"></span>
+ </button>
+ <ul class="dropdown-menu"
+ *dropdownMenu
+ role="menu">
+ <li role="menuitem">
+ <a class="dropdown-item"
+ routerLink="/rgw/bucket/add"
+ i18n>
+ <i class="fa fa-fw fa-plus"></i>
+ Add
+ </a>
+ </li>
+ <li role="menuitem"
+ [ngClass]="{'disabled': !selection.hasSingleSelection}">
+ <a class="dropdown-item"
+ routerLink="/rgw/bucket/edit/{{ selection.first()?.bucket }}"
+ i18n>
+ <i class="fa fa-fw fa-pencil"></i>
+ Edit
+ </a>
+ </li>
+ <li role="menuitem"
+ [ngClass]="{'disabled': !selection.hasSelection}">
+ <a class="dropdown-item"
+ (click)="deleteAction()"
+ i18n>
+ <i class="fa fa-fw fa-trash-o"></i>
+ Delete
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
<cd-rgw-bucket-details cdTableDetail
[selection]="selection">
</cd-rgw-bucket-details>
let component: RgwBucketListComponent;
let fixture: ComponentFixture<RgwBucketListComponent>;
- const fakeService = {
+ const fakeRgwBucketService = {
list: () => {
- return new Promise(function(resolve, reject) {
- return [];
+ return new Promise(function(resolve) {
+ resolve([]);
});
}
};
DataTableModule,
SharedModule
],
- providers: [{ provide: RgwBucketService, useValue: fakeService }]
+ providers: [{ provide: RgwBucketService, useValue: fakeRgwBucketService }]
})
.compileComponents();
}));
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';
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',
updateSelection(selection: CdTableSelection) {
this.selection = selection;
}
+
+ deleteAction() {
+ const modalRef = this.bsModalService.show(DeletionModalComponent);
+ modalRef.content.setUp({
+ metaType: this.selection.hasSingleSelection ? 'bucket' : 'buckets',
+ deletionObserver: (): Observable<any> => {
+ return new Observable((observer: Subscriber<any>) => {
+ 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
+ });
+ }
+
}
let component: RgwDaemonDetailsComponent;
let fixture: ComponentFixture<RgwDaemonDetailsComponent>;
- const fakeService = {
+ const fakeRgwDaemonService = {
get: (id: string) => {
- return new Promise(function(resolve, reject) {
- return [];
+ return new Promise(function(resolve) {
+ resolve([]);
});
}
};
PerformanceCounterModule,
TabsModule.forRoot()
],
- providers: [{ provide: RgwDaemonService, useValue: fakeService }]
+ providers: [{ provide: RgwDaemonService, useValue: fakeRgwDaemonService }]
});
}));
--- /dev/null
+<div class="modal-header">
+ <h4 class="modal-title pull-left"
+ i18n>Capability
+ </h4>
+ <button type="button"
+ class="close pull-right"
+ aria-label="Close"
+ (click)="bsModalRef.hide()">
+ <span aria-hidden="true">×</span>
+ </button>
+</div>
+<form class="form-horizontal"
+ #frm="ngForm"
+ [formGroup]="formGroup"
+ novalidate>
+ <div class="modal-body">
+
+ <!-- Type -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (frm.submitted || formGroup.controls.type.dirty) && formGroup.controls.type.invalid}">
+ <label class="control-label col-sm-3"
+ for="type"
+ i18n>Type
+ <span class="required"
+ *ngIf="!editing">
+ </span>
+ </label>
+ <div class="col-sm-9">
+ <input id="type"
+ class="form-control"
+ type="text"
+ *ngIf="editing"
+ [readonly]="true"
+ formControlName="type">
+ <select id="type"
+ class="form-control"
+ formControlName="type"
+ *ngIf="!editing"
+ autofocus>
+ <option i18n
+ *ngIf="types !== null"
+ [ngValue]="null">-- Select a type --
+ </option>
+ <option *ngFor="let type of types"
+ [value]="type">{{ type }}</option>
+ </select>
+ <span class="help-block"
+ *ngIf="(frm.submitted || formGroup.controls.type.dirty) && formGroup.controls.type.hasError('required')"
+ i18n>
+ This field is required.
+ </span>
+ </div>
+ </div>
+
+ <!-- Permission -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (frm.submitted || formGroup.controls.perm.dirty) && formGroup.controls.perm.invalid}">
+ <label class="control-label col-sm-3"
+ for="perm"
+ i18n>Permission
+ <span class="required"></span>
+ </label>
+ <div class="col-sm-9">
+ <select id="perm"
+ class="form-control"
+ formControlName="perm">
+ <option i18n
+ [ngValue]="null">-- Select a permission --
+ </option>
+ <option *ngFor="let perm of ['read', 'write', '*']"
+ [value]="perm">
+ {{ perm }}
+ </option>
+ </select>
+ <span class="help-block"
+ *ngIf="(frm.submitted || formGroup.controls.perm.dirty) && formGroup.controls.perm.hasError('required')"
+ i18n>
+ This field is required.
+ </span>
+ </div>
+ </div>
+
+ </div>
+ <div class="modal-footer">
+ <cd-submit-button (submitAction)="onSubmit()"
+ [form]="formGroup"
+ i18n>
+ {editing, select, 1 {Update} other {Add}}
+ </cd-submit-button>
+ <button class="btn btn-sm btn-default"
+ type="button"
+ (click)="bsModalRef.hide()"
+ i18n>Close
+ </button>
+ </div>
+</form>
--- /dev/null
+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<RgwUserCapabilityModalComponent>;
+
+ 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();
+ });
+});
--- /dev/null
+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();
+ }
+}
</div>
</div>
</tab>
+
+ <tab i18n-heading heading="Keys">
+ <form class="form-horizontal">
+ <cd-table [data]="keys"
+ [columns]="keysColumns"
+ columnMode="flex"
+ selectionType="multi"
+ forceIdentifier="true"
+ (updateSelection)="updateKeysSelection($event)">
+ <div class="table-actions">
+ <div class="btn-group" dropdown>
+ <button type="button"
+ class="btn btn-sm btn-primary"
+ [disabled]="!keysSelection.hasSingleSelection"
+ (click)="showKeyModal()">
+ <i class="fa fa-eye"></i>
+ <ng-container i18n>Show</ng-container>
+ </button>
+ </div>
+ </div>
+ </cd-table>
+ </form>
+ </tab>
</tabset>
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';
HttpClientModule,
SharedModule,
TabsModule.forRoot()
+ ],
+ providers: [
+ BsModalService
]
})
.compileComponents();
-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<any>;
+ @ViewChild('secretKeyTpl') public secretKeyTpl: TemplateRef<any>;
@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) {
_.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;
}
}
}
--- /dev/null
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item"
+ i18n>Object Gateway</li>
+ <li class="breadcrumb-item">
+ <a routerLink="/rgw/user"
+ i18n>Users</a>
+ </li>
+ <li class="breadcrumb-item active"
+ aria-current="page"
+ i18n>
+ {editing, select, 1 {Edit} other {Add}}
+ </li>
+ </ol>
+</nav>
+
+<cd-loading-panel *ngIf="editing && loading && !error"
+ i18n>
+ Loading user data...
+</cd-loading-panel>
+<cd-error-panel *ngIf="editing && error"
+ (backAction)="goToListView()"
+ i18n>
+ The user data could not be loaded.
+</cd-error-panel>
+
+<div class="col-sm-12 col-lg-6"
+ *ngIf="!loading && !error">
+ <form class="form-horizontal"
+ #frm="ngForm"
+ [formGroup]="userForm"
+ novalidate>
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <h3 class="panel-title">
+ {editing, select, 1 {Edit} other {Add}} user
+ </h3>
+ </div>
+ <div class="panel-body">
+
+ <!-- Username -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (frm.submitted || userForm.controls.user_id.dirty) && userForm.controls.user_id.invalid}">
+ <label class="control-label col-sm-3"
+ for="user_id"
+ i18n>Username
+ <span class="required"
+ *ngIf="!editing">
+ </span>
+ </label>
+ <div class="col-sm-9">
+ <input id="user_id"
+ class="form-control"
+ type="text"
+ formControlName="user_id"
+ [readonly]="editing"
+ autofocus>
+ <span class="help-block"
+ *ngIf="(frm.submitted || userForm.controls.user_id.dirty) && userForm.controls.user_id.hasError('required')"
+ i18n>
+ This field is required.
+ </span>
+ <span class="help-block"
+ *ngIf="(frm.submitted || userForm.controls.user_id.dirty) && userForm.controls.user_id.hasError('userIdExists')"
+ i18n>
+ The chosen user ID is already in use.
+ </span>
+ </div>
+ </div>
+
+ <!-- Full name -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (frm.submitted || userForm.controls.display_name.dirty) && userForm.controls.display_name.invalid}">
+ <label class="control-label col-sm-3"
+ for="display_name"
+ i18n>Full name
+ <span class="required"
+ *ngIf="!editing">
+ </span>
+ </label>
+ <div class="col-sm-9">
+ <input id="display_name"
+ class="form-control"
+ type="text"
+ formControlName="display_name">
+ <span class="help-block"
+ *ngIf="(frm.submitted || userForm.controls.display_name.dirty) && userForm.controls.display_name.hasError('required')"
+ i18n>
+ This field is required.
+ </span>
+ </div>
+ </div>
+
+ <!-- Email address -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (frm.submitted || userForm.controls.email.dirty) && userForm.controls.email.invalid}">
+ <label class="control-label col-sm-3"
+ for="email"
+ i18n>Email address
+ </label>
+ <div class="col-sm-9">
+ <input id="email"
+ class="form-control"
+ type="text"
+ formControlName="email">
+ <span class="help-block"
+ *ngIf="(frm.submitted || userForm.controls.email.dirty) && userForm.controls.email.hasError('email')"
+ i18n>
+ This is not a valid email address.
+ </span>
+ </div>
+ </div>
+
+ <!-- Max. buckets -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (frm.submitted || userForm.controls.max_buckets.dirty) && userForm.controls.max_buckets.invalid}">
+ <label class="control-label col-sm-3"
+ for="max_buckets"
+ i18n>Max. buckets
+ </label>
+ <div class="col-sm-9">
+ <input id="max_buckets"
+ class="form-control"
+ type="number"
+ formControlName="max_buckets">
+ <span class="help-block"
+ *ngIf="(frm.submitted || userForm.controls.max_buckets.dirty) && userForm.controls.max_buckets.hasError('min')"
+ i18n>
+ The entered value must be >= 0.
+ </span>
+ </div>
+ </div>
+
+ <!-- Suspended -->
+ <div class="form-group">
+ <div class="col-sm-offset-3 col-sm-9">
+ <div class="checkbox checkbox-primary">
+ <input id="suspended"
+ type="checkbox"
+ formControlName="suspended">
+ <label for="suspended"
+ i18n>Suspended
+ </label>
+ </div>
+ </div>
+ </div>
+
+ <!-- S3 key -->
+ <fieldset *ngIf="!editing">
+ <legend i18n>S3 key</legend>
+
+ <!-- Auto-generate key -->
+ <div class="form-group">
+ <div class="col-sm-offset-3 col-sm-9">
+ <div class="checkbox checkbox-primary">
+ <input id="generate_key"
+ type="checkbox"
+ formControlName="generate_key">
+ <label for="generate_key"
+ i18n>Auto-generate key
+ </label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Access key -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (frm.submitted || userForm.controls.access_key.dirty) && userForm.controls.access_key.invalid}"
+ *ngIf="!editing && !userForm.controls.generate_key.value">
+ <label class="control-label col-sm-3"
+ for="access_key"
+ i18n>Access key
+ <span class="required"></span>
+ </label>
+ <div class="col-sm-9">
+ <div class="input-group">
+ <input id="access_key"
+ class="form-control"
+ type="password"
+ formControlName="access_key">
+ <span class="input-group-btn">
+ <button type="button"
+ class="btn btn-default"
+ cdPasswordButton="access_key">
+ </button>
+ <button type="button"
+ class="btn btn-default"
+ cdCopy2ClipboardButton="access_key">
+ </button>
+ </span>
+ </div>
+ <span class="help-block"
+ *ngIf="(frm.submitted || userForm.controls.access_key.dirty) && userForm.controls.access_key.hasError('required')"
+ i18n>
+ This field is required.
+ </span>
+ </div>
+ </div>
+
+ <!-- Secret key -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (frm.submitted || userForm.controls.secret_key.dirty) && userForm.controls.secret_key.invalid}"
+ *ngIf="!editing && !userForm.controls.generate_key.value">
+ <label class="control-label col-sm-3"
+ for="secret_key"
+ i18n>Secret key
+ <span class="required"></span>
+ </label>
+ <div class="col-sm-9">
+ <div class="input-group">
+ <input id="secret_key"
+ class="form-control"
+ type="password"
+ formControlName="secret_key">
+ <span class="input-group-btn">
+ <button type="button"
+ class="btn btn-default"
+ cdPasswordButton="secret_key">
+ </button>
+ <button type="button"
+ class="btn btn-default"
+ cdCopy2ClipboardButton="secret_key">
+ </button>
+ </span>
+ </div>
+ <span class="help-block"
+ *ngIf="(frm.submitted || userForm.controls.secret_key.dirty) && userForm.controls.secret_key.hasError('required')"
+ i18n>
+ This field is required.
+ </span>
+ </div>
+ </div>
+ </fieldset>
+
+ <!-- Subusers -->
+ <fieldset *ngIf="editing">
+ <legend i18n>Subusers</legend>
+
+ <div class="col-sm-offset-3 col-sm-9">
+ <span *ngIf="subusers.length === 0"
+ class="form-control no-border">
+ <span class="text-muted"
+ i18n>Empty
+ </span>
+ </span>
+
+ <span *ngFor="let subuser of subusers; let i=index;">
+ <div class="input-group">
+ <span class="input-group-addon">
+ <i class="icon-prepend fa fa-user"></i>
+ </span>
+ <input type="text"
+ class="form-control"
+ value="{{ subuser.id }}"
+ readonly>
+ <span class="input-group-addon"
+ style="border-left: 0; border-right: 0;">
+ <i class="icon-prepend fa fa-share-alt"></i>
+ </span>
+ <input type="text"
+ class="form-control"
+ value="{{ ('full-control' === subuser.permissions) ? 'full' : subuser.permissions }}"
+ readonly>
+ <span class="input-group-btn">
+ <button type="button"
+ class="btn btn-default tc_showSubuserButton"
+ i18n-tooltip
+ tooltip="Edit"
+ (click)="showSubuserModal(i)">
+ <i class="fa fa-cogs"></i>
+ </button>
+ <button type="button"
+ class="btn btn-default tc_deleteSubuserButton"
+ i18n-tooltip
+ tooltip="Delete"
+ (click)="deleteSubuser(i)">
+ <i class="fa fa-trash-o"></i>
+ </button>
+ </span>
+ </div>
+ <span class="help-block"></span>
+ </span>
+
+ <span class="form-control no-border">
+ <button type="button"
+ class="btn btn-sm btn-default btn-label pull-right tc_addSubuserButton"
+ (click)="showSubuserModal()">
+ <i class="fa fa-fw fa-plus"></i>
+ <ng-container i18n>Add subuser</ng-container>
+ </button>
+ </span>
+ </div>
+ </fieldset>
+
+ <!-- Keys -->
+ <fieldset *ngIf="editing">
+ <legend i18n>Keys</legend>
+
+ <!-- S3 keys -->
+ <label class="col-sm-3 control-label"
+ i18n>S3
+ </label>
+ <div class="col-sm-9">
+ <span *ngIf="s3Keys.length === 0"
+ class="form-control no-border">
+ <span class="text-muted"
+ i18n>Empty
+ </span>
+ </span>
+
+ <span *ngFor="let key of s3Keys; let i=index;">
+ <div class="input-group">
+ <span class="input-group-addon">
+ <i class="icon-prepend fa fa-key"></i>
+ </span>
+ <input type="text"
+ class="form-control"
+ value="{{ key.user }}"
+ readonly>
+ <span class="input-group-btn">
+ <button type="button"
+ class="btn btn-default tc_showS3KeyButton"
+ i18n-tooltip
+ tooltip="Show"
+ (click)="showS3KeyModal(i)">
+ <i class="fa fa-eye"></i>
+ </button>
+ <button type="button"
+ class="btn btn-default tc_deleteS3KeyButton"
+ i18n-tooltip
+ tooltip="Delete"
+ (click)="deleteS3Key(i)">
+ <i class="fa fa-trash-o"></i>
+ </button>
+ </span>
+ </div>
+ <span class="help-block"></span>
+ </span>
+
+ <span class="form-control no-border">
+ <button type="button"
+ class="btn btn-sm btn-default btn-label pull-right tc_addS3KeyButton"
+ (click)="showS3KeyModal()">
+ <i class="fa fa-fw fa-plus"></i>
+ <ng-container i18n>Add S3 key</ng-container>
+ </button>
+ </span>
+ <hr>
+ </div>
+
+ <!-- Swift keys -->
+ <label class="col-sm-3 control-label"
+ i18n>Swift
+ </label>
+ <div class="col-sm-9">
+ <span *ngIf="swiftKeys.length === 0"
+ class="form-control no-border">
+ <span class="text-muted"
+ i18n>Empty
+ </span>
+ </span>
+
+ <span *ngFor="let key of swiftKeys; let i=index;">
+ <div class="input-group">
+ <span class="input-group-addon">
+ <i class="icon-prepend fa fa-key"></i>
+ </span>
+ <input type="text"
+ class="form-control"
+ value="{{ key.user }}"
+ readonly>
+ <span class="input-group-btn">
+ <button type="button"
+ class="btn btn-default tc_showSwiftKeyButton"
+ i18n-tooltip
+ tooltip="Show"
+ (click)="showSwiftKeyModal(i)">
+ <i class="fa fa-eye"></i>
+ </button>
+ </span>
+ </div>
+ <span class="help-block"></span>
+ </span>
+ </div>
+ </fieldset>
+
+ <!-- Capabilities -->
+ <fieldset *ngIf="editing">
+ <legend i18n>Capabilities</legend>
+
+ <div class="col-sm-offset-3 col-sm-9">
+ <span *ngIf="capabilities.length === 0"
+ class="form-control no-border">
+ <span class="text-muted"
+ i18n>Empty
+ </span>
+ </span>
+
+ <span *ngFor="let cap of capabilities; let i=index;">
+ <div class="input-group">
+ <span class="input-group-addon">
+ <i class="icon-prepend fa fa-share-alt"></i>
+ </span>
+ <input type="text"
+ class="form-control"
+ value="{{ cap.type }}:{{ cap.perm }}"
+ readonly>
+ <span class="input-group-btn">
+ <button type="button"
+ class="btn btn-default tc_editCapButton"
+ i18n-tooltip
+ tooltip="Edit"
+ (click)="showCapabilityModal(i)">
+ <i class="fa fa-cogs"></i>
+ </button>
+ <button type="button"
+ class="btn btn-default tc_deleteCapButton"
+ i18n-tooltip
+ tooltip="Delete"
+ (click)="deleteCapability(i)">
+ <i class="fa fa-trash-o"></i>
+ </button>
+ </span>
+ </div>
+ <span class="help-block"></span>
+ </span>
+
+ <span class="form-control no-border">
+ <button type="button"
+ class="btn btn-sm btn-default btn-label pull-right tc_addCapButton"
+ (click)="showCapabilityModal()">
+ <i class="fa fa-fw fa-plus"></i>
+ <ng-container i18n>Add capability</ng-container>
+ </button>
+ </span>
+ </div>
+ </fieldset>
+
+ <!-- User quota -->
+ <fieldset>
+ <legend i18n>User quota</legend>
+
+ <!-- Enabled -->
+ <div class="form-group">
+ <div class="col-sm-offset-3 col-sm-9">
+ <div class="checkbox checkbox-primary">
+ <input id="user_quota_enabled"
+ type="checkbox"
+ formControlName="user_quota_enabled">
+ <label for="user_quota_enabled"
+ i18n>Enabled
+ </label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Unlimited size -->
+ <div class="form-group"
+ *ngIf="userForm.controls.user_quota_enabled.value">
+ <div class="col-sm-offset-3 col-sm-9">
+ <div class="checkbox checkbox-primary">
+ <input id="user_quota_max_size_unlimited"
+ type="checkbox"
+ formControlName="user_quota_max_size_unlimited">
+ <label for="user_quota_max_size_unlimited"
+ i18n>Unlimited size
+ </label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Maximum size -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (frm.submitted || userForm.controls.user_quota_max_size.dirty) && userForm.controls.user_quota_max_size.invalid}"
+ *ngIf="!userForm.controls.user_quota_max_size_unlimited.value">
+ <label class="control-label col-sm-3"
+ for="user_quota_max_size"
+ i18n>Max. size
+ <span class="required"></span>
+ </label>
+ <div class="col-sm-9">
+ <input id="user_quota_max_size"
+ class="form-control"
+ type="text"
+ formControlName="user_quota_max_size">
+ <span class="help-block"
+ *ngIf="(frm.submitted || userForm.controls.user_quota_max_size.dirty) && userForm.controls.user_quota_max_size.hasError('required')"
+ i18n>
+ This field is required.
+ </span>
+ <span class="help-block"
+ *ngIf="(frm.submitted || userForm.controls.user_quota_max_size.dirty) && userForm.controls.user_quota_max_size.hasError('quotaMaxSize')"
+ i18n>
+ The value is not valid.
+ </span>
+ </div>
+ </div>
+
+ <!-- Unlimited objects -->
+ <div class="form-group"
+ *ngIf="userForm.controls.user_quota_enabled.value">
+ <div class="col-sm-offset-3 col-sm-9">
+ <div class="checkbox checkbox-primary">
+ <input id="user_quota_max_objects_unlimited"
+ type="checkbox"
+ formControlName="user_quota_max_objects_unlimited">
+ <label for="user_quota_max_objects_unlimited"
+ i18n>Unlimited objects
+ </label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Maximum objects -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (frm.submitted || userForm.controls.user_quota_max_objects.dirty) && userForm.controls.user_quota_max_objects.invalid}"
+ *ngIf="!userForm.controls.user_quota_max_objects_unlimited.value">
+ <label class="control-label col-sm-3"
+ for="user_quota_max_objects"
+ i18n>Max. objects
+ <span class="required"></span>
+ </label>
+ <div class="col-sm-9">
+ <input id="user_quota_max_objects"
+ class="form-control"
+ type="number"
+ formControlName="user_quota_max_objects">
+ <span class="help-block"
+ *ngIf="(frm.submitted || userForm.controls.user_quota_max_objects.dirty) && userForm.controls.user_quota_max_objects.hasError('required')"
+ i18n>
+ This field is required.
+ </span>
+ <span class="help-block"
+ *ngIf="(frm.submitted || userForm.controls.user_quota_max_objects.dirty) && userForm.controls.user_quota_max_objects.hasError('min')"
+ i18n>
+ The entered value must be >= 0.
+ </span>
+ </div>
+ </div>
+ </fieldset>
+
+ <!-- Bucket quota -->
+ <fieldset>
+ <legend i18n>Bucket quota</legend>
+
+ <!-- Enabled -->
+ <div class="form-group">
+ <div class="col-sm-offset-3 col-sm-9">
+ <div class="checkbox checkbox-primary">
+ <input id="bucket_quota_enabled"
+ type="checkbox"
+ formControlName="bucket_quota_enabled">
+ <label for="bucket_quota_enabled"
+ i18n>Enabled
+ </label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Unlimited size -->
+ <div class="form-group"
+ *ngIf="userForm.controls.bucket_quota_enabled.value">
+ <div class="col-sm-offset-3 col-sm-9">
+ <div class="checkbox checkbox-primary">
+ <input id="bucket_quota_max_size_unlimited"
+ type="checkbox"
+ formControlName="bucket_quota_max_size_unlimited">
+ <label for="bucket_quota_max_size_unlimited"
+ i18n>Unlimited size
+ </label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Maximum size -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (frm.submitted || userForm.controls.bucket_quota_max_size.dirty) && userForm.controls.bucket_quota_max_size.invalid}"
+ *ngIf="!userForm.controls.bucket_quota_max_size_unlimited.value">
+ <label class="control-label col-sm-3"
+ for="bucket_quota_max_size"
+ i18n>Max. size
+ <span class="required"></span>
+ </label>
+ <div class="col-sm-9">
+ <input id="bucket_quota_max_size"
+ class="form-control"
+ type="text"
+ formControlName="bucket_quota_max_size">
+ <span class="help-block"
+ *ngIf="(frm.submitted || userForm.controls.bucket_quota_max_size.dirty) && userForm.controls.bucket_quota_max_size.hasError('required')"
+ i18n>
+ This field is required.
+ </span>
+ <span class="help-block"
+ *ngIf="(frm.submitted || userForm.controls.bucket_quota_max_size.dirty) && userForm.controls.bucket_quota_max_size.hasError('quotaMaxSize')"
+ i18n>
+ The value is not valid.
+ </span>
+ </div>
+ </div>
+
+ <!-- Unlimited objects -->
+ <div class="form-group"
+ *ngIf="userForm.controls.bucket_quota_enabled.value">
+ <div class="col-sm-offset-3 col-sm-9">
+ <div class="checkbox checkbox-primary">
+ <input id="bucket_quota_max_objects_unlimited"
+ type="checkbox"
+ formControlName="bucket_quota_max_objects_unlimited">
+ <label for="bucket_quota_max_objects_unlimited"
+ i18n>Unlimited objects
+ </label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Maximum objects -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (frm.submitted || userForm.controls.bucket_quota_max_objects.dirty) && userForm.controls.bucket_quota_max_objects.invalid}"
+ *ngIf="!userForm.controls.bucket_quota_max_objects_unlimited.value">
+ <label class="control-label col-sm-3"
+ for="bucket_quota_max_objects"
+ i18n>Max. objects
+ <span class="required"></span>
+ </label>
+ <div class="col-sm-9">
+ <input id="bucket_quota_max_objects"
+ class="form-control"
+ type="number"
+ formControlName="bucket_quota_max_objects">
+ <span class="help-block"
+ *ngIf="(frm.submitted || userForm.controls.bucket_quota_max_objects.dirty) && userForm.controls.bucket_quota_max_objects.hasError('required')"
+ i18n>
+ This field is required.
+ </span>
+ <span class="help-block"
+ *ngIf="(frm.submitted || userForm.controls.bucket_quota_max_objects.dirty) && userForm.controls.bucket_quota_max_objects.hasError('min')"
+ i18n>
+ The entered value must be >= 0.
+ </span>
+ </div>
+ </div>
+ </fieldset>
+ </div>
+
+ <div class="panel-footer">
+ <div class="button-group text-right">
+ <cd-submit-button (submitAction)="onSubmit()"
+ [form]="userForm"
+ i18n>
+ {editing, select, 1 {Update} other {Add}}
+ </cd-submit-button>
+ <button class="btn btn-sm btn-default"
+ type="button"
+ routerLink="/rgw/user"
+ i18n>
+ Back
+ </button>
+ </div>
+ </div>
+ </div>
+ </form>
+</div>
--- /dev/null
+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<RgwUserFormComponent>;
+ let queryResult: Array<string> = [];
+
+ 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();
+ });
+ }
+ });
+ });
+});
--- /dev/null
+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<Object>[] = [];
+
+ 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<ValidationErrors | null> => {
+ 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;
+ }
+}
aria-current="page">Users</li>
</ol>
</nav>
-<cd-table [data]="users"
+<cd-table #table
+ [autoReload]="false"
+ [data]="users"
[columns]="columns"
columnMode="flex"
- selectionType="single"
+ selectionType="multi"
(updateSelection)="updateSelection($event)"
identifier="user_id"
(fetchData)="getUserList()">
+ <div class="table-actions">
+ <div class="btn-group" dropdown>
+ <button type="button"
+ class="btn btn-sm btn-primary"
+ *ngIf="!selection.hasSelection"
+ routerLink="/rgw/user/add">
+ <i class="fa fa-fw fa-plus"></i>
+ <ng-container i18n>Add</ng-container>
+ </button>
+ <button type="button"
+ class="btn btn-sm btn-primary"
+ *ngIf="selection.hasSingleSelection"
+ routerLink="/rgw/user/edit/{{ selection.first()?.user_id }}">
+ <i class="fa fa-fw fa-pencil"></i>
+ <ng-container i18n>Edit</ng-container>
+ </button>
+ <button type="button"
+ class="btn btn-sm btn-primary"
+ *ngIf="selection.hasMultiSelection"
+ (click)="deleteAction()">
+ <i class="fa fa-fw fa-trash-o"></i>
+ <ng-container i18n>Delete</ng-container>
+ </button>
+ <button type="button"
+ class="btn btn-sm btn-primary dropdown-toggle dropdown-toggle-split"
+ dropdownToggle>
+ <span class="caret"></span>
+ <span class="sr-only"></span>
+ </button>
+ <ul class="dropdown-menu"
+ *dropdownMenu
+ role="menu">
+ <li role="menuitem">
+ <a class="dropdown-item"
+ routerLink="/rgw/user/add"
+ i18n>
+ <i class="fa fa-fw fa-plus"></i>
+ Add
+ </a>
+ </li>
+ <li role="menuitem"
+ [ngClass]="{'disabled': !selection.hasSingleSelection}">
+ <a class="dropdown-item"
+ routerLink="/rgw/user/edit/{{ selection.first()?.user_id }}"
+ i18n>
+ <i class="fa fa-fw fa-pencil"></i>
+ Edit
+ </a>
+ </li>
+ <li role="menuitem"
+ [ngClass]="{'disabled': !selection.hasSelection}">
+ <a class="dropdown-item"
+ (click)="deleteAction()"
+ i18n>
+ <i class="fa fa-fw fa-trash-o"></i>
+ Delete
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
<cd-rgw-user-details cdTableDetail
[selection]="selection">
</cd-rgw-user-details>
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';
],
imports: [
HttpClientModule,
+ RouterTestingModule,
BsDropdownModule.forRoot(),
TabsModule.forRoot(),
SharedModule
+ ],
+ providers: [
+ BsModalService
]
})
.compileComponents();
-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';
})
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',
updateSelection(selection: CdTableSelection) {
this.selection = selection;
}
+
+ deleteAction() {
+ const modalRef = this.bsModalService.show(DeletionModalComponent);
+ modalRef.content.setUp({
+ metaType: this.selection.hasSingleSelection ? 'user' : 'users',
+ deletionObserver: (): Observable<any> => {
+ return new Observable((observer: Subscriber<any>) => {
+ 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
+ });
+ }
}
--- /dev/null
+<div class="modal-header">
+ <h4 class="modal-title pull-left"
+ i18n>S3 key
+ </h4>
+ <button type="button"
+ class="close pull-right"
+ aria-label="Close"
+ (click)="bsModalRef.hide()">
+ <span aria-hidden="true">×</span>
+ </button>
+</div>
+<form class="form-horizontal"
+ #frm="ngForm"
+ [formGroup]="formGroup"
+ novalidate>
+ <div class="modal-body">
+
+ <!-- Username -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (frm.submitted || formGroup.controls.user.dirty) && formGroup.controls.user.invalid}">
+ <label class="control-label col-sm-3"
+ for="user"
+ i18n>Username
+ <span class="required"
+ *ngIf="!viewing">
+ </span>
+ </label>
+ <div class="col-sm-9">
+ <input id="user"
+ class="form-control"
+ type="text"
+ *ngIf="viewing"
+ [readonly]="true"
+ formControlName="user">
+ <select id="user"
+ class="form-control"
+ formControlName="user"
+ *ngIf="!viewing"
+ autofocus>
+ <option i18n
+ *ngIf="userCandidates !== null"
+ [ngValue]="null">-- Select a username --
+ </option>
+ <option *ngFor="let userCandidate of userCandidates"
+ [value]="userCandidate">{{ userCandidate }}</option>
+ </select>
+ <span class="help-block"
+ *ngIf="(frm.submitted || formGroup.controls.user.dirty) && formGroup.controls.user.hasError('required')"
+ i18n>
+ This field is required.
+ </span>
+ </div>
+ </div>
+
+ <!-- Auto-generate key -->
+ <div class="form-group"
+ *ngIf="!viewing">
+ <div class="col-sm-offset-3 col-sm-9">
+ <div class="checkbox checkbox-primary">
+ <input id="generate_key"
+ type="checkbox"
+ formControlName="generate_key">
+ <label for="generate_key"
+ i18n>Auto-generate key
+ </label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Access key -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (frm.submitted || formGroup.controls.access_key.dirty) && formGroup.controls.access_key.invalid}"
+ *ngIf="!formGroup.controls.generate_key.value">
+ <label class="control-label col-sm-3"
+ for="access_key"
+ i18n>Access key
+ <span class="required"
+ *ngIf="!viewing">
+ </span>
+ </label>
+ <div class="col-sm-9">
+ <div class="input-group">
+ <input id="access_key"
+ class="form-control"
+ type="password"
+ [readonly]="viewing"
+ formControlName="access_key">
+ <span class="input-group-btn">
+ <button type="button"
+ class="btn btn-default"
+ cdPasswordButton="access_key">
+ </button>
+ <button type="button"
+ class="btn btn-default"
+ cdCopy2ClipboardButton="access_key">
+ </button>
+ </span>
+ </div>
+ <span class="help-block"
+ *ngIf="(frm.submitted || formGroup.controls.access_key.dirty) && formGroup.controls.access_key.hasError('required')"
+ i18n>
+ This field is required.
+ </span>
+ </div>
+ </div>
+
+ <!-- Secret key -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (frm.submitted || formGroup.controls.secret_key.dirty) && formGroup.controls.secret_key.invalid}"
+ *ngIf="!formGroup.controls.generate_key.value">
+ <label class="control-label col-sm-3"
+ for="secret_key"
+ i18n>Secret key
+ <span class="required"
+ *ngIf="!viewing">
+ </span>
+ </label>
+ <div class="col-sm-9">
+ <div class="input-group">
+ <input id="secret_key"
+ class="form-control"
+ type="password"
+ [readonly]="viewing"
+ formControlName="secret_key">
+ <span class="input-group-btn">
+ <button type="button"
+ class="btn btn-default"
+ cdPasswordButton="secret_key">
+ </button>
+ <button type="button"
+ class="btn btn-default"
+ cdCopy2ClipboardButton="secret_key">
+ </button>
+ </span>
+ </div>
+ <span class="help-block"
+ *ngIf="(frm.submitted || formGroup.controls.secret_key.dirty) && formGroup.controls.secret_key.hasError('required')"
+ i18n>
+ This field is required.
+ </span>
+ </div>
+ </div>
+
+ </div>
+ <div class="modal-footer">
+ <cd-submit-button *ngIf="!viewing"
+ (submitAction)="onSubmit()"
+ [form]="formGroup"
+ i18n>Add
+ </cd-submit-button>
+ <button class="btn btn-sm"
+ type="button"
+ [ngClass]="{'btn-primary': viewing, 'btn-default': !viewing}"
+ (click)="bsModalRef.hide()"
+ i18n>Close
+ </button>
+ </div>
+</form>
--- /dev/null
+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<RgwUserS3KeyModalComponent>;
+
+ 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();
+ });
+});
--- /dev/null
+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();
+ }
+}
--- /dev/null
+<div class="modal-header">
+ <h4 class="modal-title pull-left"
+ i18n>Subuser
+ </h4>
+ <button type="button"
+ class="close pull-right"
+ aria-label="Close"
+ (click)="bsModalRef.hide()">
+ <span aria-hidden="true">×</span>
+ </button>
+</div>
+<form class="form-horizontal"
+ #frm="ngForm"
+ [formGroup]="formGroup"
+ novalidate>
+ <div class="modal-body">
+
+ <!-- Username -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (frm.submitted || formGroup.controls.uid.dirty) && formGroup.controls.uid.invalid}">
+ <label class="control-label col-sm-3"
+ for="uid"
+ i18n>Username
+ </label>
+ <div class="col-sm-9">
+ <input id="uid"
+ class="form-control"
+ type="text"
+ formControlName="uid"
+ [readonly]="true">
+ </div>
+ </div>
+
+ <!-- Subuser -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (frm.submitted || formGroup.controls.subuid.dirty) && formGroup.controls.subuid.invalid}">
+ <label class="control-label col-sm-3"
+ for="subuid"
+ i18n>Subuser
+ <span class="required"
+ *ngIf="!editing">
+ </span>
+ </label>
+ <div class="col-sm-9">
+ <input id="subuid"
+ class="form-control"
+ type="text"
+ formControlName="subuid"
+ [readonly]="editing"
+ autofocus>
+ <span class="help-block"
+ *ngIf="(frm.submitted || formGroup.controls.subuid.dirty) && formGroup.controls.subuid.hasError('required')"
+ i18n>
+ This field is required.
+ </span>
+ <span class="help-block"
+ *ngIf="(frm.submitted || formGroup.controls.subuid.dirty) && formGroup.controls.subuid.hasError('subuserIdExists')"
+ i18n>
+ The chosen subuser ID is already in use.
+ </span>
+ </div>
+ </div>
+
+ <!-- Permission -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (frm.submitted || formGroup.controls.perm.dirty) && formGroup.controls.perm.invalid}">
+ <label class="control-label col-sm-3"
+ for="perm"
+ i18n>Permission
+ <span class="required"></span>
+ </label>
+ <div class="col-sm-9">
+ <select id="perm"
+ class="form-control"
+ formControlName="perm">
+ <option i18n
+ [ngValue]="null">
+ -- Select a permission --
+ </option>
+ <option *ngFor="let perm of ['read', 'write']"
+ [value]="perm">
+ {{ perm }}
+ </option>
+ <option i18n
+ value="read-write">
+ read, write
+ </option>
+ <option i18n
+ value="full-control">
+ full
+ </option>
+ </select>
+ <span class="help-block"
+ *ngIf="(frm.submitted || formGroup.controls.perm.dirty) && formGroup.controls.perm.hasError('required')"
+ i18n>
+ This field is required.
+ </span>
+ </div>
+ </div>
+
+ <!-- Swift key -->
+ <fieldset *ngIf="!editing">
+ <legend i18n>Swift key</legend>
+
+ <!-- Auto-generate key -->
+ <div class="form-group">
+ <div class="col-sm-offset-3 col-sm-9">
+ <div class="checkbox checkbox-primary">
+ <input id="generate_secret"
+ type="checkbox"
+ formControlName="generate_secret">
+ <label for="generate_secret"
+ i18n>Auto-generate secret
+ </label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Secret key -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (frm.submitted || formGroup.controls.secret_key.dirty) && formGroup.controls.secret_key.invalid}"
+ *ngIf="!editing && !formGroup.controls.generate_secret.value">
+ <label class="control-label col-sm-3"
+ for="secret_key"
+ i18n>Secret key
+ <span class="required"></span>
+ </label>
+ <div class="col-sm-9">
+ <div class="input-group">
+ <input id="secret_key"
+ class="form-control"
+ type="password"
+ formControlName="secret_key">
+ <span class="input-group-btn">
+ <button type="button"
+ class="btn btn-default"
+ cdPasswordButton="secret_key">
+ </button>
+ <button type="button"
+ class="btn btn-default"
+ cdCopy2ClipboardButton="secret_key">
+ </button>
+ </span>
+ </div>
+ <span class="help-block"
+ *ngIf="(frm.submitted || formGroup.controls.secret_key.dirty) && formGroup.controls.secret_key.hasError('required')"
+ i18n>
+ This field is required.
+ </span>
+ </div>
+ </div>
+
+ </fieldset>
+
+ </div>
+ <div class="modal-footer">
+ <cd-submit-button (submitAction)="onSubmit()"
+ [form]="formGroup"
+ i18n>
+ {editing, select, 1 {Update} other {Add}}
+ </cd-submit-button>
+ <button class="btn btn-sm btn-default"
+ type="button"
+ (click)="bsModalRef.hide()"
+ i18n>Close
+ </button>
+ </div>
+</form>
--- /dev/null
+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<RgwUserSubuserModalComponent>;
+
+ 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();
+ });
+ });
+});
--- /dev/null
+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();
+ }
+}
--- /dev/null
+<div class="modal-header">
+ <h4 class="modal-title pull-left"
+ i18n>Swift key
+ </h4>
+ <button type="button"
+ class="close pull-right"
+ aria-label="Close"
+ (click)="bsModalRef.hide()">
+ <span aria-hidden="true">×</span>
+ </button>
+</div>
+<div class="modal-body">
+ <form class="form-horizontal"
+ novalidate>
+
+ <!-- Username -->
+ <div class="form-group">
+ <label class="control-label col-sm-3"
+ for="user"
+ i18n>Username
+ </label>
+ <div class="col-sm-9">
+ <input id="user"
+ name="user"
+ class="form-control"
+ type="text"
+ [readonly]="true"
+ [(ngModel)]="user">
+ </div>
+ </div>
+
+ <!-- Secret key -->
+ <div class="form-group">
+ <label class="control-label col-sm-3"
+ for="secret_key"
+ i18n>Secret key
+ </label>
+ <div class="col-sm-9">
+ <div class="input-group">
+ <input id="secret_key"
+ name="secret_key"
+ class="form-control"
+ type="password"
+ [(ngModel)]="secret_key"
+ [readonly]="true">
+ <span class="input-group-btn">
+ <button type="button"
+ class="btn btn-default"
+ cdPasswordButton="secret_key">
+ </button>
+ <button type="button"
+ class="btn btn-default"
+ cdCopy2ClipboardButton="secret_key">
+ </button>
+ </span>
+ </div>
+ </div>
+ </div>
+
+ </form>
+</div>
+<div class="modal-footer">
+ <button class="btn btn-sm btn-primary"
+ type="button"
+ (click)="bsModalRef.hide()"
+ i18n>Close</button>
+</div>
--- /dev/null
+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<RgwUserSwiftKeyModalComponent>;
+
+ 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();
+ });
+});
--- /dev/null
+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;
+ }
+}
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,
declarations: [
RgwDaemonListComponent,
RgwDaemonDetailsComponent,
+ RgwBucketFormComponent,
RgwBucketListComponent,
RgwBucketDetailsComponent,
RgwUserListComponent,
- RgwUserDetailsComponent
+ RgwUserDetailsComponent,
+ RgwBucketFormComponent,
+ RgwUserFormComponent,
+ RgwUserSwiftKeyModalComponent,
+ RgwUserS3KeyModalComponent,
+ RgwUserCapabilityModalComponent,
+ RgwUserSubuserModalComponent
]
})
export class RgwModule { }
-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 {
constructor(private http: HttpClient) { }
+ /**
+ * Get the list of buckets.
+ * @return {Observable<Object[]>}
+ */
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<string[]>}
+ */
+ 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) {
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<boolean>}
+ */
+ exists(bucket: string) {
+ return this.enumerate()
+ .flatMap((resp: string[]) => {
+ const index = _.indexOf(resp, bucket);
+ return Observable.of(-1 !== index);
});
}
}
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';
constructor(private http: HttpClient) { }
+ /**
+ * Get the list of users.
+ * @return {Observable<Object[]>}
+ */
list() {
return this.enumerate()
.flatMap((uids: string[]) => {
return Observable.forkJoin(
uids.map((uid: string) => {
return this.get(uid);
- })
- );
+ }));
}
return Observable.of([]);
});
}
+ /**
+ * Get the list of usernames.
+ * @return {Observable<string[]>}
+ */
enumerate() {
return this.http.get('/api/rgw/proxy/metadata/user');
}
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) {
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<boolean>}
+ */
+ exists(uid: string) {
+ return this.enumerate()
+ .flatMap((resp: string[]) => {
+ const index = _.indexOf(resp, uid);
+ return Observable.of(-1 !== index);
+ });
+ }
}
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 {
@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();
}