<tabset *ngIf="selection?.hasSingleSelection">
<tab heading="Details" i18n-heading>
- <table class="table table-bordered table-hover">
- <thead>
- <tr>
- <th></th>
- <th class="text-center">Read</th>
- <th class="text-center">Create</th>
- <th class="text-center">Update</th>
- <th class="text-center">Delete</th>
- </tr>
- </thead>
- <tbody>
- <tr *ngFor="let scope of scopes">
- <td i18n
- class="bold col-sm-3">
- {{ scope }}
- </td>
- <td class="col-sm-2 text-center"
- *ngFor="let column of ['read', 'create', 'update', 'delete']">
- <span *ngIf="selectedItem.scopes_permissions[scope] && selectedItem.scopes_permissions[scope].indexOf(column) !== -1">
- <i class="fa fa-check-square-o" aria-hidden="true"></i>
- </span>
- <span *ngIf="!selectedItem.scopes_permissions[scope] || selectedItem.scopes_permissions[scope].indexOf(column) === -1">
- <i class="fa fa-square-o" aria-hidden="true"></i>
- </span>
- </td>
- </tr>
- </tbody>
- </table>
+ <cd-table [data]="scopes_permissions"
+ [columns]="columns"
+ columnMode="flex"
+ [toolHeader]="false"
+ [autoReload]="false"
+ [autoSave]="false"
+ [footer]="false"
+ [limit]="0">
+ </cd-table>
</tab>
</tabset>
import { ToastModule } from 'ng2-toastr';
import { TabsModule } from 'ngx-bootstrap';
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
import { SharedModule } from '../../../shared/shared.module';
import { RoleDetailsComponent } from './role-details.component';
it('should create', () => {
expect(component).toBeTruthy();
});
+
+ it('should create scopes permissions [1/2]', () => {
+ component.scopes = ['log', 'rgw'];
+ component.selection = new CdTableSelection();
+ component.selection.selected = [
+ {
+ description: 'RGW Manager',
+ name: 'rgw-manager',
+ scopes_permissions: {
+ rgw: ['read', 'create', 'update', 'delete']
+ },
+ system: true
+ }
+ ];
+ component.selection.update();
+ expect(component.scopes_permissions.length).toBe(0);
+ component.ngOnChanges();
+ expect(component.scopes_permissions).toEqual([
+ { scope: 'log', read: false, create: false, update: false, delete: false },
+ { scope: 'rgw', read: true, create: true, update: true, delete: true }
+ ]);
+ });
+
+ it('should create scopes permissions [2/2]', () => {
+ component.scopes = ['cephfs', 'log', 'rgw'];
+ component.selection = new CdTableSelection();
+ component.selection.selected = [
+ {
+ description: 'Test',
+ name: 'test',
+ scopes_permissions: {
+ log: ['read', 'update'],
+ rgw: ['read', 'create', 'update']
+ },
+ system: false
+ }
+ ];
+ component.selection.update();
+ expect(component.scopes_permissions.length).toBe(0);
+ component.ngOnChanges();
+ expect(component.scopes_permissions).toEqual([
+ { scope: 'cephfs', read: false, create: false, update: false, delete: false },
+ { scope: 'log', read: true, create: false, update: true, delete: false },
+ { scope: 'rgw', read: true, create: true, update: true, delete: false }
+ ]);
+ });
});
-import { Component, Input, OnChanges } from '@angular/core';
+import { Component, Input, OnChanges, OnInit } from '@angular/core';
+import * as _ from 'lodash';
+
+import { CellTemplate } from '../../../shared/enum/cell-template.enum';
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
import { CdTableSelection } from '../../../shared/models/cd-table-selection';
@Component({
templateUrl: './role-details.component.html',
styleUrls: ['./role-details.component.scss']
})
-export class RoleDetailsComponent implements OnChanges {
+export class RoleDetailsComponent implements OnChanges, OnInit {
@Input()
selection: CdTableSelection;
@Input()
scopes: Array<string>;
selectedItem: any;
+ columns: CdTableColumn[];
+ scopes_permissions: Array<any> = [];
+
constructor() {}
+ ngOnInit() {
+ this.columns = [
+ {
+ prop: 'scope',
+ name: 'Scope',
+ flexGrow: 2
+ },
+ {
+ prop: 'read',
+ name: 'Read',
+ flexGrow: 1,
+ cellClass: 'text-center',
+ cellTransformation: CellTemplate.checkIcon
+ },
+ {
+ prop: 'create',
+ name: 'Create',
+ flexGrow: 1,
+ cellClass: 'text-center',
+ cellTransformation: CellTemplate.checkIcon
+ },
+ {
+ prop: 'update',
+ name: 'Update',
+ flexGrow: 1,
+ cellClass: 'text-center',
+ cellTransformation: CellTemplate.checkIcon
+ },
+ {
+ prop: 'delete',
+ name: 'Delete',
+ flexGrow: 1,
+ cellClass: 'text-center',
+ cellTransformation: CellTemplate.checkIcon
+ }
+ ];
+ }
+
ngOnChanges() {
if (this.selection.hasSelection) {
this.selectedItem = this.selection.first();
+ // Build the scopes/permissions data used by the data table.
+ const scopes_permissions = [];
+ _.each(this.scopes, (scope) => {
+ const scope_permission = { read: false, create: false, update: false, delete: false };
+ scope_permission['scope'] = scope;
+ if (scope in this.selectedItem['scopes_permissions']) {
+ _.each(this.selectedItem['scopes_permissions'][scope], (permission) => {
+ scope_permission[permission] = true;
+ });
+ }
+ scopes_permissions.push(scope_permission);
+ });
+ this.scopes_permissions = scopes_permissions;
}
}
}
[ngClass]="{'has-error': roleForm.showError('description', formDir)}">
<label i18n
class="control-label col-sm-3"
- for="name">Description
+ for="description">Description
</label>
<div class="col-sm-9">
<input class="form-control"
<!-- Permissions -->
<div class="form-group">
<label i18n
- class="control-label col-sm-3"
- for="name">Permissions
+ class="control-label col-sm-3">Permissions
</label>
<div class="col-sm-9">
- <table class="table table-bordered table-hover">
- <thead>
- <tr>
- <th></th>
- <th class="text-center">Read</th>
- <th class="text-center">Create</th>
- <th class="text-center">Update</th>
- <th class="text-center">Delete</th>
- </tr>
- </thead>
- <tbody>
- <tr *ngFor="let scope of scopes">
- <td i18n
- class="bold col-sm-3">
- {{ scope }}
- </td>
- <td class="col-sm-2 text-center clickable"
- *ngFor="let column of ['read', 'create', 'update', 'delete']">
- <div class="checkbox checkbox-primary">
- <input type="checkbox"
- [checked]="roleForm.getValue('scopes_permissions')[scope] && roleForm.getValue('scopes_permissions')[scope].indexOf(column) !== -1"
- (change)="hadlePermissionClick(scope, column)">
- <label></label>
- </div>
- </td>
- </tr>
- </tbody>
- </table>
+ <cd-table [data]="scopes_permissions"
+ [columns]="columns"
+ columnMode="flex"
+ [toolHeader]="false"
+ [autoReload]="false"
+ [autoSave]="false"
+ [footer]="false"
+ [limit]="0">
+ </cd-table>
</div>
</div>
<button i18n
type="button"
class="btn btn-sm btn-default"
- routerLink="/user-management/users/roles">
+ routerLink="/user-management/roles">
Back
</button>
</div>
</div>
</form>
</div>
+
+<ng-template #cellScopeCheckboxTpl
+ let-column="column"
+ let-row="row"
+ let-value="value">
+ <div class="checkbox checkbox-primary">
+ <input id="scope_{{ row.scope }}"
+ type="checkbox"
+ [checked]="isRowChecked(row.scope)"
+ (change)="onClickCellCheckbox(row.scope, column.prop, $event)">
+ <label class="datatable-permissions-scope-cell-label"
+ for="scope_{{ row.scope }}">{{ value }}</label>
+ </div>
+</ng-template>
+
+<ng-template #cellPermissionCheckboxTpl
+ let-column="column"
+ let-row="row"
+ let-value="value">
+ <div class="checkbox checkbox-primary">
+ <input type="checkbox"
+ [checked]="value"
+ (change)="onClickCellCheckbox(row.scope, column.prop, $event)">
+ <label></label>
+ </div>
+</ng-template>
+
+<ng-template #headerPermissionCheckboxTpl
+ let-column="column">
+ <div class="checkbox checkbox-primary">
+ <input id="header_{{ column.prop }}"
+ type="checkbox"
+ [checked]="isHeaderChecked(column.prop)"
+ (change)="onClickHeaderCheckbox(column.prop, $event)">
+ <label class="datatable-permissions-header-cell-label"
+ for="header_{{ column.prop }}">{{ column.name }}</label>
+ </div>
+</ng-template>
-@import '../../../../defaults';
-
-thead {
- background-color: $color-table-header-bg;
+.datatable-permissions-header-cell-label,
+.datatable-permissions-scope-cell-label {
+ font-weight: bold;
}
roleReq.flush({});
expect(router.navigate).toHaveBeenCalledWith(['/user-management/roles']);
});
+
+ it('should check all perms for a scope', () => {
+ form.get('scopes_permissions').setValue({ cephfs: ['read'] });
+ component.onClickCellCheckbox('grafana', 'scope');
+ const scopes_permissions = form.getValue('scopes_permissions');
+ expect(Object.keys(scopes_permissions)).toContain('grafana');
+ expect(scopes_permissions['grafana']).toEqual(['create', 'delete', 'read', 'update']);
+ });
+
+ it('should uncheck all perms for a scope', () => {
+ form.get('scopes_permissions').setValue({ cephfs: ['read', 'create', 'update', 'delete'] });
+ component.onClickCellCheckbox('cephfs', 'scope');
+ const scopes_permissions = form.getValue('scopes_permissions');
+ expect(Object.keys(scopes_permissions)).not.toContain('cephfs');
+ });
+
+ it('should uncheck all scopes and perms', () => {
+ component.scopes = ['cephfs', 'grafana'];
+ form.get('scopes_permissions').setValue({ cephfs: ['read', 'delete'], grafana: ['update'] });
+ component.onClickHeaderCheckbox('scope', { target: { checked: false } });
+ const scopes_permissions = form.getValue('scopes_permissions');
+ expect(scopes_permissions).toEqual({});
+ });
+
+ it('should check all scopes and perms', () => {
+ component.scopes = ['cephfs', 'grafana'];
+ form
+ .get('scopes_permissions')
+ .setValue({ cephfs: ['create', 'update'], grafana: ['delete'] });
+ component.onClickHeaderCheckbox('scope', { target: { checked: true } });
+ const scopes_permissions = form.getValue('scopes_permissions');
+ const keys = Object.keys(scopes_permissions);
+ expect(keys).toEqual(['cephfs', 'grafana']);
+ keys.forEach((key) => {
+ expect(scopes_permissions[key].sort()).toEqual(['create', 'delete', 'read', 'update']);
+ });
+ });
+
+ it('should check if column is checked', () => {
+ component.scopes_permissions = [
+ { scope: 'a', read: true, create: true, update: true, delete: true },
+ { scope: 'b', read: false, create: true, update: false, delete: true }
+ ];
+ expect(component.isRowChecked('a')).toBeTruthy();
+ expect(component.isRowChecked('b')).toBeFalsy();
+ expect(component.isRowChecked('c')).toBeFalsy();
+ });
+
+ it('should check if header is checked', () => {
+ component.scopes_permissions = [
+ { scope: 'a', read: true, create: true, update: false, delete: true },
+ { scope: 'b', read: false, create: true, update: false, delete: true }
+ ];
+ expect(component.isHeaderChecked('read')).toBeFalsy();
+ expect(component.isHeaderChecked('create')).toBeTruthy();
+ expect(component.isHeaderChecked('update')).toBeFalsy();
+ });
});
describe('edit mode', () => {
component.ngOnInit();
const reqScopes = httpTesting.expectOne('ui-api/scope');
expect(reqScopes.request.method).toBe('GET');
- reqScopes.flush(scopes);
});
afterEach(() => {
});
it('should submit', () => {
+ component.onClickCellCheckbox('osd', 'update');
+ component.onClickCellCheckbox('osd', 'create');
+ component.onClickCellCheckbox('user', 'read');
component.submit();
- component.hadlePermissionClick('osd', 'update');
- component.hadlePermissionClick('osd', 'create');
- component.hadlePermissionClick('user', 'read');
const roleReq = httpTesting.expectOne(`api/role/${role.name}`);
expect(roleReq.request.method).toBe('PUT');
expect(roleReq.request.body).toEqual({
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
+import * as _ from 'lodash';
import { BsModalRef } from 'ngx-bootstrap';
+import { forkJoin as observableForkJoin } from 'rxjs';
import { RoleService } from '../../../shared/api/role.service';
import { ScopeService } from '../../../shared/api/scope.service';
import { NotificationType } from '../../../shared/enum/notification-type.enum';
import { CdFormGroup } from '../../../shared/forms/cd-form-group';
import { CdValidators } from '../../../shared/forms/cd-validators';
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
import { NotificationService } from '../../../shared/services/notification.service';
import { RoleFormMode } from './role-form-mode.enum';
import { RoleFormModel } from './role-form.model';
styleUrls: ['./role-form.component.scss']
})
export class RoleFormComponent implements OnInit {
+ @ViewChild('headerPermissionCheckboxTpl')
+ headerPermissionCheckboxTpl: TemplateRef<any>;
+ @ViewChild('cellScopeCheckboxTpl')
+ cellScopeCheckboxTpl: TemplateRef<any>;
+ @ViewChild('cellPermissionCheckboxTpl')
+ cellPermissionCheckboxTpl: TemplateRef<any>;
+
modalRef: BsModalRef;
roleForm: CdFormGroup;
response: RoleFormModel;
- scopes: Array<string>;
+
+ columns: CdTableColumn[];
+ scopes: Array<string> = [];
+ scopes_permissions: Array<any> = [];
roleFormMode = RoleFormMode;
mode: RoleFormMode;
private notificationService: NotificationService
) {
this.createForm();
+ this.listenToChanges();
}
createForm() {
}
ngOnInit() {
+ this.columns = [
+ {
+ prop: 'scope',
+ name: 'All',
+ flexGrow: 2,
+ cellTemplate: this.cellScopeCheckboxTpl,
+ headerTemplate: this.headerPermissionCheckboxTpl
+ },
+ {
+ prop: 'read',
+ name: 'Read',
+ flexGrow: 1,
+ cellClass: 'text-center',
+ cellTemplate: this.cellPermissionCheckboxTpl,
+ headerTemplate: this.headerPermissionCheckboxTpl
+ },
+ {
+ prop: 'create',
+ name: 'Create',
+ flexGrow: 1,
+ cellClass: 'text-center',
+ cellTemplate: this.cellPermissionCheckboxTpl,
+ headerTemplate: this.headerPermissionCheckboxTpl
+ },
+ {
+ prop: 'update',
+ name: 'Update',
+ flexGrow: 1,
+ cellClass: 'text-center',
+ cellTemplate: this.cellPermissionCheckboxTpl,
+ headerTemplate: this.headerPermissionCheckboxTpl
+ },
+ {
+ prop: 'delete',
+ name: 'Delete',
+ flexGrow: 1,
+ cellClass: 'text-center',
+ cellTemplate: this.cellPermissionCheckboxTpl,
+ headerTemplate: this.headerPermissionCheckboxTpl
+ }
+ ];
if (this.router.url.startsWith('/user-management/roles/edit')) {
this.mode = this.roleFormMode.editing;
}
- this.scopeService.list().subscribe((scopes: Array<string>) => {
- this.scopes = scopes;
- });
if (this.mode === this.roleFormMode.editing) {
this.initEdit();
+ } else {
+ this.initCreate();
}
}
+ initCreate() {
+ // Load the scopes and initialize the default scopes/permissions data.
+ this.scopeService.list().subscribe((scopes: Array<string>) => {
+ this.scopes = scopes;
+ this.roleForm.get('scopes_permissions').setValue({});
+ });
+ }
+
initEdit() {
- this.disableForEdit();
+ // Disable the 'Name' input field.
+ this.roleForm.get('name').disable();
+ // Load the scopes and the role data.
this.route.params.subscribe((params: { name: string }) => {
- const name = params.name;
- this.roleService.get(name).subscribe((roleFormModel: RoleFormModel) => {
- this.setResponse(roleFormModel);
+ const observables = [];
+ observables.push(this.scopeService.list());
+ observables.push(this.roleService.get(params.name));
+ observableForkJoin(observables).subscribe((resp: any[]) => {
+ this.scopes = resp[0];
+ ['name', 'description', 'scopes_permissions'].forEach((key) =>
+ this.roleForm.get(key).setValue(resp[1][key])
+ );
});
});
}
- disableForEdit() {
- this.roleForm.get('name').disable();
+ listenToChanges() {
+ // Create/Update the data which is used by the data table to display the
+ // scopes/permissions every time the form field value has been changed.
+ this.roleForm.get('scopes_permissions').valueChanges.subscribe((value) => {
+ const scopes_permissions = [];
+ _.each(this.scopes, (scope) => {
+ // Set the defaults values.
+ const scope_permission = { read: false, create: false, update: false, delete: false };
+ scope_permission['scope'] = scope;
+ // Apply settings from the given value if they exist.
+ if (scope in value) {
+ _.each(value[scope], (permission) => {
+ scope_permission[permission] = true;
+ });
+ }
+ scopes_permissions.push(scope_permission);
+ });
+ this.scopes_permissions = scopes_permissions;
+ });
}
- setResponse(response: RoleFormModel) {
- ['name', 'description', 'scopes_permissions'].forEach((key) =>
- this.roleForm.get(key).setValue(response[key])
+ /**
+ * Checks if the specified row checkbox needs to be rendered as checked.
+ * @param {string} scope The scope to be checked, e.g. 'cephfs', 'grafana',
+ * 'osd', 'pool' ...
+ * @return Returns true if all permissions (read, create, update, delete)
+ * are checked for the specified scope, otherwise false.
+ */
+ isRowChecked(scope: string) {
+ const scope_permission = _.find(this.scopes_permissions, (o) => {
+ return o['scope'] === scope;
+ });
+ if (_.isUndefined(scope_permission)) {
+ return false;
+ }
+ return (
+ scope_permission['read'] &&
+ scope_permission['create'] &&
+ scope_permission['update'] &&
+ scope_permission['delete']
);
}
- hadlePermissionClick(scope: string, permission: string) {
- const permissions = this.roleForm.getValue('scopes_permissions');
- if (!permissions[scope]) {
- permissions[scope] = [];
+ /**
+ * Checks if the specified header checkbox needs to be rendered as checked.
+ * @param {string} property The property/permission (read, create,
+ * update, delete) to be checked. If 'scope' is given, all permissions
+ * are checked.
+ * @return Returns true if specified property/permission is selected
+ * for all scopes, otherwise false.
+ */
+ isHeaderChecked(property: string) {
+ let permissions = [property];
+ if ('scope' === property) {
+ permissions = ['read', 'create', 'update', 'delete'];
}
- const index = permissions[scope].indexOf(permission);
- if (index === -1) {
- permissions[scope].push(permission);
+ return permissions.every((permission) => {
+ return this.scopes_permissions.every((scope_permission) => {
+ return scope_permission[permission];
+ });
+ });
+ }
+
+ onClickCellCheckbox(scope: string, property: string, event: Event = null) {
+ // Use a copy of the form field data to do not trigger the redrawing of the
+ // data table with every change.
+ const scopes_permissions = _.cloneDeep(this.roleForm.getValue('scopes_permissions'));
+ let permissions = [property];
+ if ('scope' === property) {
+ permissions = ['read', 'create', 'update', 'delete'];
+ }
+ if (!(scope in scopes_permissions)) {
+ scopes_permissions[scope] = [];
+ }
+ // Add or remove the given permission(s) depending on the click event or if no
+ // click event is given then add/remove them if they are absent/exist.
+ if (
+ (event && event.target['checked']) ||
+ !_.isEqual(permissions.sort(), _.intersection(scopes_permissions[scope], permissions).sort())
+ ) {
+ scopes_permissions[scope] = _.union(scopes_permissions[scope], permissions);
} else {
- permissions[scope].splice(index, 1);
+ scopes_permissions[scope] = _.difference(scopes_permissions[scope], permissions);
+ if (_.isEmpty(scopes_permissions[scope])) {
+ _.unset(scopes_permissions, scope);
+ }
+ }
+ this.roleForm.get('scopes_permissions').setValue(scopes_permissions);
+ }
+
+ onClickHeaderCheckbox(property: 'scope' | 'read' | 'create' | 'update' | 'delete', event: Event) {
+ // Use a copy of the form field data to do not trigger the redrawing of the
+ // data table with every change.
+ const scopes_permissions = _.cloneDeep(this.roleForm.getValue('scopes_permissions'));
+ let permissions = [property];
+ if ('scope' === property) {
+ permissions = ['read', 'create', 'update', 'delete'];
}
+ _.each(permissions, (permission) => {
+ _.each(this.scopes, (scope) => {
+ if (event.target['checked']) {
+ scopes_permissions[scope] = _.union(scopes_permissions[scope], [permission]);
+ } else {
+ scopes_permissions[scope] = _.difference(scopes_permissions[scope], [permission]);
+ if (_.isEmpty(scopes_permissions[scope])) {
+ _.unset(scopes_permissions, scope);
+ }
+ }
+ });
+ });
+ this.roleForm.get('scopes_permissions').setValue(scopes_permissions);
}
getRequest(): RoleFormModel {