From 5d2b0eb685b912e97980a3c676e4130508c4110f Mon Sep 17 00:00:00 2001 From: Ricardo Marques Date: Mon, 30 Jul 2018 14:27:31 +0100 Subject: [PATCH] mgr/dashboard: Role management from the UI Fixes: https://tracker.ceph.com/issues/24447 Signed-off-by: Ricardo Marques --- .../frontend/src/app/app-routing.module.ts | 34 +++- .../frontend/src/app/core/auth/auth.module.ts | 15 +- .../role-details/role-details.component.html | 32 ++++ .../role-details/role-details.component.scss | 12 ++ .../role-details.component.spec.ts | 37 ++++ .../role-details/role-details.component.ts | 24 +++ .../auth/role-form/role-form-mode.enum.ts | 3 + .../auth/role-form/role-form.component.html | 120 +++++++++++++ .../auth/role-form/role-form.component.scss | 5 + .../role-form/role-form.component.spec.ts | 160 ++++++++++++++++++ .../auth/role-form/role-form.component.ts | 144 ++++++++++++++++ .../core/auth/role-form/role-form.model.ts | 5 + .../auth/role-list/role-list.component.html | 75 ++++++++ .../auth/role-list/role-list.component.scss | 0 .../role-list/role-list.component.spec.ts | 38 +++++ .../auth/role-list/role-list.component.ts | 102 +++++++++++ .../auth/user-form/user-form.component.html | 2 +- .../user-form/user-form.component.spec.ts | 8 +- .../auth/user-form/user-form.component.ts | 6 +- .../auth/user-list/user-list.component.html | 10 +- .../user-list/user-list.component.spec.ts | 12 +- .../auth/user-tabs/user-tabs.component.html | 12 ++ .../auth/user-tabs/user-tabs.component.scss | 0 .../user-tabs/user-tabs.component.spec.ts | 37 ++++ .../auth/user-tabs/user-tabs.component.ts | 22 +++ .../administration.component.html | 4 +- .../src/app/shared/api/role.service.spec.ts | 34 ++++ .../src/app/shared/api/role.service.ts | 31 ++++ .../src/app/shared/api/scope.service.spec.ts | 34 ++++ .../src/app/shared/api/scope.service.ts | 15 ++ .../src/app/shared/enum/components.enum.ts | 1 + 31 files changed, 1011 insertions(+), 23 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form-mode.enum.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.model.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/scope.service.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/scope.service.ts diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index b8a6df65dae6b..5de3f59300082 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -20,6 +20,8 @@ import { RgwDaemonListComponent } from './ceph/rgw/rgw-daemon-list/rgw-daemon-li 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 { RoleFormComponent } from './core/auth/role-form/role-form.component'; +import { RoleListComponent } from './core/auth/role-list/role-list.component'; import { UserFormComponent } from './core/auth/user-form/user-form.component'; import { UserListComponent } from './core/auth/user-list/user-list.component'; import { ForbiddenComponent } from './core/forbidden/forbidden.component'; @@ -177,16 +179,36 @@ const routes: Routes = [ } ] }, - // Administration + // Dashboard Settings { - path: 'users', + path: 'user-management', canActivate: [AuthGuardService], canActivateChild: [AuthGuardService], - data: { breadcrumbs: 'Administration/Users' }, + data: { breadcrumbs: 'User management', path: null }, children: [ - { path: '', component: UserListComponent }, - { path: 'add', component: UserFormComponent, data: { breadcrumbs: 'Add' } }, - { path: 'edit/:username', component: UserFormComponent, data: { breadcrumbs: 'Edit' } } + { + path: '', + redirectTo: 'users', + pathMatch: 'full' + }, + { + path: 'users', + data: { breadcrumbs: 'Users' }, + children: [ + { path: '', component: UserListComponent }, + { path: 'add', component: UserFormComponent, data: { breadcrumbs: 'Add' } }, + { path: 'edit/:username', component: UserFormComponent, data: { breadcrumbs: 'Edit' } } + ] + }, + { + path: 'roles', + data: { breadcrumbs: 'Roles' }, + children: [ + { path: '', component: RoleListComponent }, + { path: 'add', component: RoleFormComponent, data: { breadcrumbs: 'Add' } }, + { path: 'edit/:name', component: RoleFormComponent, data: { breadcrumbs: 'Edit' } } + ] + } ] }, // System diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts index d3498c472159c..045464c859ccb 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts @@ -8,8 +8,12 @@ import { BsDropdownModule, PopoverModule, TabsModule } from 'ngx-bootstrap'; import { SharedModule } from '../../shared/shared.module'; import { LoginComponent } from './login/login.component'; import { LogoutComponent } from './logout/logout.component'; +import { RoleDetailsComponent } from './role-details/role-details.component'; +import { RoleFormComponent } from './role-form/role-form.component'; +import { RoleListComponent } from './role-list/role-list.component'; import { UserFormComponent } from './user-form/user-form.component'; import { UserListComponent } from './user-list/user-list.component'; +import { UserTabsComponent } from './user-tabs/user-tabs.component'; @NgModule({ imports: [ @@ -22,7 +26,16 @@ import { UserListComponent } from './user-list/user-list.component'; TabsModule.forRoot(), RouterModule ], - declarations: [LoginComponent, LogoutComponent, UserListComponent, UserFormComponent], + declarations: [ + LoginComponent, + LogoutComponent, + RoleDetailsComponent, + RoleFormComponent, + RoleListComponent, + UserTabsComponent, + UserListComponent, + UserFormComponent + ], exports: [LogoutComponent] }) export class AuthModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.html new file mode 100644 index 0000000000000..a91ff0061bea6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + +
ReadCreateUpdateDelete
+ {{ scope }} + + + + + + + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.scss new file mode 100644 index 0000000000000..f9c5b87572aaa --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.scss @@ -0,0 +1,12 @@ +@import '../../../../defaults'; + +thead { + background-color: $color-table-header-bg; +} + +.fa { + font-size: large; + &.fa-square-o { + color: $color-light-gray; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.spec.ts new file mode 100644 index 0000000000000..f0234b33b4719 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.spec.ts @@ -0,0 +1,37 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { ToastModule } from 'ng2-toastr'; +import { TabsModule } from 'ngx-bootstrap'; + +import { SharedModule } from '../../../shared/shared.module'; +import { RoleDetailsComponent } from './role-details.component'; + +describe('RoleDetailsComponent', () => { + let component: RoleDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + SharedModule, + ToastModule.forRoot(), + TabsModule.forRoot(), + RouterTestingModule, + HttpClientTestingModule + ], + declarations: [RoleDetailsComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RoleDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.ts new file mode 100644 index 0000000000000..5f5b27ce0725f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.ts @@ -0,0 +1,24 @@ +import { Component, Input, OnChanges } from '@angular/core'; + +import { CdTableSelection } from '../../../shared/models/cd-table-selection'; + +@Component({ + selector: 'cd-role-details', + templateUrl: './role-details.component.html', + styleUrls: ['./role-details.component.scss'] +}) +export class RoleDetailsComponent implements OnChanges { + @Input() + selection: CdTableSelection; + @Input() + scopes: Array; + selectedItem: any; + + constructor() {} + + ngOnChanges() { + if (this.selection.hasSelection) { + this.selectedItem = this.selection.first(); + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form-mode.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form-mode.enum.ts new file mode 100644 index 0000000000000..4f0a6f11fbf0a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form-mode.enum.ts @@ -0,0 +1,3 @@ +export enum RoleFormMode { + editing = 'editing' +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.html new file mode 100644 index 0000000000000..2366aec323740 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.html @@ -0,0 +1,120 @@ +
+
+
+
+

+ {mode, select, editing {Edit} other {Add}} Role +

+
+
+ + +
+ +
+ + + This field is required. + + + The chosen name is already in use. + +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + + + + + + + + + + + + + + + +
ReadCreateUpdateDelete
+ {{ scope }} + +
+ + +
+
+
+
+ +
+ +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.scss new file mode 100644 index 0000000000000..564487d0e7633 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.scss @@ -0,0 +1,5 @@ +@import '../../../../defaults'; + +thead { + background-color: $color-table-header-bg; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.spec.ts new file mode 100644 index 0000000000000..6da5dee4361e0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.spec.ts @@ -0,0 +1,160 @@ +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { Router, Routes } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { ToastModule } from 'ng2-toastr'; +import { of } from 'rxjs'; + +import { configureTestBed } from '../../../../testing/unit-test-helper'; +import { RoleService } from '../../../shared/api/role.service'; +import { ScopeService } from '../../../shared/api/scope.service'; +import { ComponentsModule } from '../../../shared/components/components.module'; +import { CdFormGroup } from '../../../shared/forms/cd-form-group'; +import { NotificationService } from '../../../shared/services/notification.service'; +import { SharedModule } from '../../../shared/shared.module'; +import { RoleFormComponent } from './role-form.component'; +import { RoleFormModel } from './role-form.model'; + +describe('RoleFormComponent', () => { + let component: RoleFormComponent; + let form: CdFormGroup; + let fixture: ComponentFixture; + let httpTesting: HttpTestingController; + let roleService: RoleService; + let router: Router; + const setUrl = (url) => Object.defineProperty(router, 'url', { value: url }); + + @Component({ selector: 'cd-fake', template: '' }) + class FakeComponent {} + + const routes: Routes = [{ path: 'roles', component: FakeComponent }]; + + configureTestBed( + { + imports: [ + [RouterTestingModule.withRoutes(routes)], + HttpClientTestingModule, + ReactiveFormsModule, + RouterTestingModule, + ComponentsModule, + ToastModule.forRoot(), + SharedModule + ], + declarations: [RoleFormComponent, FakeComponent] + }, + true + ); + + beforeEach(() => { + fixture = TestBed.createComponent(RoleFormComponent); + component = fixture.componentInstance; + form = component.roleForm; + httpTesting = TestBed.get(HttpTestingController); + roleService = TestBed.get(RoleService); + router = TestBed.get(Router); + spyOn(router, 'navigate'); + fixture.detectChanges(); + const notify = TestBed.get(NotificationService); + spyOn(notify, 'show'); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + expect(form).toBeTruthy(); + }); + + describe('create mode', () => { + beforeEach(() => { + setUrl('/user-management/roles/add'); + component.ngOnInit(); + }); + + it('should not disable fields', () => { + ['name', 'description', 'scopes_permissions'].forEach((key) => + expect(form.get(key).disabled).toBeFalsy() + ); + }); + + it('should validate name required', () => { + form.get('name').setValue(''); + expect(form.get('name').hasError('required')).toBeTruthy(); + }); + + it('should set mode', () => { + expect(component.mode).toBeUndefined(); + }); + + it('should submit', () => { + const role: RoleFormModel = { + name: 'role1', + description: 'Role 1', + scopes_permissions: { osd: ['read'] } + }; + Object.keys(role).forEach((k) => form.get(k).setValue(role[k])); + component.submit(); + const roleReq = httpTesting.expectOne('api/role'); + expect(roleReq.request.method).toBe('POST'); + expect(roleReq.request.body).toEqual(role); + roleReq.flush({}); + expect(router.navigate).toHaveBeenCalledWith(['/user-management/roles']); + }); + }); + + describe('edit mode', () => { + const role: RoleFormModel = { + name: 'role1', + description: 'Role 1', + scopes_permissions: { osd: ['read', 'create'] } + }; + const scopes = ['osd', 'user']; + beforeEach(() => { + spyOn(roleService, 'get').and.callFake(() => of(role)); + spyOn(TestBed.get(ScopeService), 'list').and.callFake(() => of(scopes)); + setUrl('/user-management/roles/edit/role1'); + component.ngOnInit(); + const reqScopes = httpTesting.expectOne('ui-api/scope'); + expect(reqScopes.request.method).toBe('GET'); + reqScopes.flush(scopes); + }); + + afterEach(() => { + httpTesting.verify(); + }); + + it('should disable fields if editing', () => { + expect(form.get('name').disabled).toBeTruthy(); + ['description', 'scopes_permissions'].forEach((key) => + expect(form.get(key).disabled).toBeFalsy() + ); + }); + + it('should set control values', () => { + ['name', 'description', 'scopes_permissions'].forEach((key) => + expect(form.getValue(key)).toBe(role[key]) + ); + }); + + it('should set mode', () => { + expect(component.mode).toBe('editing'); + }); + + it('should submit', () => { + 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({ + name: 'role1', + description: 'Role 1', + scopes_permissions: { osd: ['read', 'update'], user: ['read'] } + }); + roleReq.flush({}); + expect(router.navigate).toHaveBeenCalledWith(['/user-management/roles']); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.ts new file mode 100644 index 0000000000000..a3e544f8b7810 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.ts @@ -0,0 +1,144 @@ +import { Component, OnInit } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { BsModalRef } from 'ngx-bootstrap'; + +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 { NotificationService } from '../../../shared/services/notification.service'; +import { RoleFormMode } from './role-form-mode.enum'; +import { RoleFormModel } from './role-form.model'; + +@Component({ + selector: 'cd-role-form', + templateUrl: './role-form.component.html', + styleUrls: ['./role-form.component.scss'] +}) +export class RoleFormComponent implements OnInit { + modalRef: BsModalRef; + + roleForm: CdFormGroup; + response: RoleFormModel; + scopes: Array; + + roleFormMode = RoleFormMode; + mode: RoleFormMode; + + constructor( + private route: ActivatedRoute, + private router: Router, + private roleService: RoleService, + private scopeService: ScopeService, + private notificationService: NotificationService + ) { + this.createForm(); + } + + createForm() { + this.roleForm = new CdFormGroup({ + name: new FormControl('', { + validators: [Validators.required], + asyncValidators: [CdValidators.unique(this.roleService.exists, this.roleService)] + }), + description: new FormControl(''), + scopes_permissions: new FormControl({}) + }); + } + + ngOnInit() { + if (this.router.url.startsWith('/user-management/roles/edit')) { + this.mode = this.roleFormMode.editing; + } + this.scopeService.list().subscribe((scopes: Array) => { + this.scopes = scopes; + }); + if (this.mode === this.roleFormMode.editing) { + this.initEdit(); + } + } + + initEdit() { + this.disableForEdit(); + this.route.params.subscribe((params: { name: string }) => { + const name = params.name; + this.roleService.get(name).subscribe((roleFormModel: RoleFormModel) => { + this.setResponse(roleFormModel); + }); + }); + } + + disableForEdit() { + this.roleForm.get('name').disable(); + } + + setResponse(response: RoleFormModel) { + ['name', 'description', 'scopes_permissions'].forEach((key) => + this.roleForm.get(key).setValue(response[key]) + ); + } + + hadlePermissionClick(scope: string, permission: string) { + const permissions = this.roleForm.getValue('scopes_permissions'); + if (!permissions[scope]) { + permissions[scope] = []; + } + const index = permissions[scope].indexOf(permission); + if (index === -1) { + permissions[scope].push(permission); + } else { + permissions[scope].splice(index, 1); + } + } + + getRequest(): RoleFormModel { + const roleFormModel = new RoleFormModel(); + ['name', 'description', 'scopes_permissions'].forEach( + (key) => (roleFormModel[key] = this.roleForm.get(key).value) + ); + return roleFormModel; + } + + createAction() { + const roleFormModel = this.getRequest(); + this.roleService.create(roleFormModel).subscribe( + () => { + this.notificationService.show( + NotificationType.success, + `Created role '${roleFormModel.name}'` + ); + this.router.navigate(['/user-management/roles']); + }, + () => { + this.roleForm.setErrors({ cdSubmitButton: true }); + } + ); + } + + editAction() { + const roleFormModel = this.getRequest(); + this.roleService.update(roleFormModel).subscribe( + () => { + this.notificationService.show( + NotificationType.success, + `Updated role '${roleFormModel.name}'` + ); + this.router.navigate(['/user-management/roles']); + }, + () => { + this.roleForm.setErrors({ cdSubmitButton: true }); + } + ); + } + + submit() { + if (this.mode === this.roleFormMode.editing) { + this.editAction(); + } else { + this.createAction(); + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.model.ts new file mode 100644 index 0000000000000..74a7323bea0ab --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.model.ts @@ -0,0 +1,5 @@ +export class RoleFormModel { + name: string; + description: string; + scopes_permissions: any; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.html new file mode 100644 index 0000000000000..847e84ca91e53 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.html @@ -0,0 +1,75 @@ + + + +
+
+ + + + + +
+
+ + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.spec.ts new file mode 100644 index 0000000000000..cc47b8486d196 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.spec.ts @@ -0,0 +1,38 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { ToastModule } from 'ng2-toastr'; +import { TabsModule } from 'ngx-bootstrap'; + +import { configureTestBed } from '../../../../testing/unit-test-helper'; +import { SharedModule } from '../../../shared/shared.module'; +import { RoleDetailsComponent } from '../role-details/role-details.component'; +import { UserTabsComponent } from '../user-tabs/user-tabs.component'; +import { RoleListComponent } from './role-list.component'; + +describe('RoleListComponent', () => { + let component: RoleListComponent; + let fixture: ComponentFixture; + + configureTestBed({ + declarations: [RoleListComponent, RoleDetailsComponent, UserTabsComponent], + imports: [ + SharedModule, + ToastModule.forRoot(), + TabsModule.forRoot(), + RouterTestingModule, + HttpClientTestingModule + ] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RoleListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.ts new file mode 100644 index 0000000000000..5919d38f32b01 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.ts @@ -0,0 +1,102 @@ +import { Component, OnInit } from '@angular/core'; + +import { BsModalRef, BsModalService } from 'ngx-bootstrap'; +import { forkJoin } from 'rxjs'; + +import { RoleService } from '../../../shared/api/role.service'; +import { ScopeService } from '../../../shared/api/scope.service'; +import { DeletionModalComponent } from '../../../shared/components/deletion-modal/deletion-modal.component'; +import { EmptyPipe } from '../../../shared/empty.pipe'; +import { CellTemplate } from '../../../shared/enum/cell-template.enum'; +import { NotificationType } from '../../../shared/enum/notification-type.enum'; +import { CdTableColumn } from '../../../shared/models/cd-table-column'; +import { CdTableSelection } from '../../../shared/models/cd-table-selection'; +import { Permission } from '../../../shared/models/permissions'; +import { AuthStorageService } from '../../../shared/services/auth-storage.service'; +import { NotificationService } from '../../../shared/services/notification.service'; + +@Component({ + selector: 'cd-role-list', + templateUrl: './role-list.component.html', + styleUrls: ['./role-list.component.scss'] +}) +export class RoleListComponent implements OnInit { + permission: Permission; + columns: CdTableColumn[]; + roles: Array; + scopes: Array; + selection = new CdTableSelection(); + + modalRef: BsModalRef; + + constructor( + private roleService: RoleService, + private scopeService: ScopeService, + private emptyPipe: EmptyPipe, + private authStorageService: AuthStorageService, + private modalService: BsModalService, + private notificationService: NotificationService + ) { + this.permission = this.authStorageService.getPermissions().user; + } + + ngOnInit() { + this.columns = [ + { + name: 'Name', + prop: 'name', + flexGrow: 3 + }, + { + name: 'Description', + prop: 'description', + flexGrow: 5, + pipe: this.emptyPipe + }, + { + name: 'System Role', + prop: 'system', + cellClass: 'text-center', + flexGrow: 1, + cellTransformation: CellTemplate.checkIcon + } + ]; + } + + getRoles() { + forkJoin([this.roleService.list(), this.scopeService.list()]).subscribe( + (data: [Array, Array]) => { + this.roles = data[0]; + this.scopes = data[1]; + } + ); + } + + updateSelection(selection: CdTableSelection) { + this.selection = selection; + } + + deleteRole(role: string) { + this.roleService.delete(role).subscribe( + () => { + this.getRoles(); + this.modalRef.hide(); + this.notificationService.show(NotificationType.success, `Deleted role '${role}'`); + }, + () => { + this.modalRef.content.stopLoadingSpinner(); + } + ); + } + + deleteRoleModal() { + this.modalRef = this.modalService.show(DeletionModalComponent); + const name = this.selection.first().name; + this.modalRef.content.setUp({ + metaType: 'Role', + pattern: `${name}`, + deletionMethod: () => this.deleteRole(name), + modalRef: this.modalRef + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html index 5bfa2fc8bd50f..a21232eb2847d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html @@ -169,7 +169,7 @@ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.spec.ts index b9ffbd970eb26..b7a7f8a5f8885 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.spec.ts @@ -75,7 +75,7 @@ describe('UserFormComponent', () => { describe('create mode', () => { beforeEach(() => { - setUrl('/users/add'); + setUrl('/user-management/users/add'); component.ngOnInit(); }); @@ -128,7 +128,7 @@ describe('UserFormComponent', () => { expect(userReq.request.method).toBe('POST'); expect(userReq.request.body).toEqual(user); userReq.flush({}); - expect(router.navigate).toHaveBeenCalledWith(['/users']); + expect(router.navigate).toHaveBeenCalledWith(['/user-management/users']); }); }); @@ -167,7 +167,7 @@ describe('UserFormComponent', () => { beforeEach(() => { spyOn(userService, 'get').and.callFake(() => of(user)); spyOn(TestBed.get(RoleService), 'list').and.callFake(() => of(roles)); - setUrl('/users/edit/user1'); + setUrl('/user-management/users/edit/user1'); component.ngOnInit(); const req = httpTesting.expectOne('api/role'); expect(req.request.method).toBe('GET'); @@ -240,7 +240,7 @@ describe('UserFormComponent', () => { roles: ['administrator'] }); userReq.flush({}); - expect(router.navigate).toHaveBeenCalledWith(['/users']); + expect(router.navigate).toHaveBeenCalledWith(['/user-management/users']); }); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts index 02ff0340b3aeb..dfc41ab7ed653 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts @@ -75,7 +75,7 @@ export class UserFormComponent implements OnInit { } ngOnInit() { - if (this.router.url.startsWith('/users/edit')) { + if (this.router.url.startsWith('/user-management/users/edit')) { this.mode = this.userFormMode.editing; } this.roleService.list().subscribe((roles: Array) => { @@ -135,7 +135,7 @@ export class UserFormComponent implements OnInit { `User "${userFormModel.username}" has been created.`, 'Create User' ); - this.router.navigate(['/users']); + this.router.navigate(['/user-management/users']); }, () => { this.userForm.setErrors({ cdSubmitButton: true }); @@ -212,7 +212,7 @@ export class UserFormComponent implements OnInit { `User "${userFormModel.username}" has been updated.`, 'Edit User' ); - this.router.navigate(['/users']); + this.router.navigate(['/user-management/users']); } }, () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.html index e4368fd94add1..817cca4729b1e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.html @@ -1,3 +1,5 @@ + + + routerLink="/user-management/users/add"> Add @@ -20,7 +22,7 @@ class="btn btn-sm btn-primary" [ngClass]="{'disabled': !selection.hasSelection}" *ngIf="permission.update && (!permission.create || selection.hasSingleSelection)" - routerLink="/users/edit/{{ selection.first()?.username }}"> + routerLink="/user-management/users/edit/{{ selection.first()?.username }}"> Edit @@ -42,7 +44,7 @@