]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Role management from the UI 23409/head
authorRicardo Marques <rimarques@suse.com>
Mon, 30 Jul 2018 13:27:31 +0000 (14:27 +0100)
committerRicardo Marques <rimarques@suse.com>
Wed, 5 Sep 2018 12:12:20 +0000 (13:12 +0100)
Fixes: https://tracker.ceph.com/issues/24447
Signed-off-by: Ricardo Marques <rimarques@suse.com>
31 files changed:
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form-mode.enum.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.model.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/api/role.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/role.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/scope.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/scope.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/components.enum.ts

index b8a6df65dae6bd4c121fa76c57a7dff604f67464..5de3f59300082d6006f3ef67c505286bceb58a53 100644 (file)
@@ -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
index d3498c472159c80c55f6adba653286dcb6e1f4c2..045464c859ccbb0064047344f085832067ecb7d4 100644 (file)
@@ -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 (file)
index 0000000..a91ff00
--- /dev/null
@@ -0,0 +1,32 @@
+<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>
+  </tab>
+</tabset>
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 (file)
index 0000000..f9c5b87
--- /dev/null
@@ -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 (file)
index 0000000..f0234b3
--- /dev/null
@@ -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<RoleDetailsComponent>;
+
+  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 (file)
index 0000000..5f5b27c
--- /dev/null
@@ -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<string>;
+  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 (file)
index 0000000..4f0a6f1
--- /dev/null
@@ -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 (file)
index 0000000..2366aec
--- /dev/null
@@ -0,0 +1,120 @@
+<div class="col-sm-12 col-lg-6">
+  <form name="roleForm"
+        class="form-horizontal"
+        #formDir="ngForm"
+        [formGroup]="roleForm"
+        novalidate>
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <h3 class="panel-title">
+          <span i18n>{mode, select, editing {Edit} other {Add}}</span> Role
+        </h3>
+      </div>
+      <div class="panel-body">
+
+        <!-- Name -->
+        <div class="form-group"
+             [ngClass]="{'has-error': roleForm.showError('name', formDir)}">
+          <label i18n
+                 class="control-label col-sm-3"
+                 for="name">Name
+            <span class="required"
+                  *ngIf="mode !== roleFormMode.editing"></span>
+          </label>
+          <div class="col-sm-9">
+            <input class="form-control"
+                   type="text"
+                   i18n-placeholder
+                   placeholder="Name..."
+                   id="name"
+                   name="name"
+                   formControlName="name"
+                   autofocus>
+            <span i18n
+                  class="help-block"
+                  *ngIf="roleForm.showError('name', formDir, 'required')">
+              This field is required.
+            </span>
+            <span i18n
+                  class="help-block"
+                  *ngIf="roleForm.showError('name', formDir, 'notUnique')">
+              The chosen name is already in use.
+            </span>
+          </div>
+        </div>
+
+        <!-- Description -->
+        <div class="form-group"
+             [ngClass]="{'has-error': roleForm.showError('description', formDir)}">
+          <label i18n
+                 class="control-label col-sm-3"
+                 for="name">Description
+          </label>
+          <div class="col-sm-9">
+            <input class="form-control"
+                   type="text"
+                   i18n-placeholder
+                   placeholder="Description..."
+                   id="description"
+                   name="description"
+                   formControlName="description">
+          </div>
+        </div>
+
+        <!-- Permissions -->
+        <div class="form-group">
+          <label i18n
+                 class="control-label col-sm-3"
+                 for="name">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>
+          </div>
+        </div>
+
+      </div>
+      <div class="panel-footer">
+        <div class="button-group text-right">
+          <cd-submit-button [form]="formDir"
+                            type="button"
+                            (submitAction)="submit()">
+            <span i18n>{mode, select, editing {Update} other {Create}}</span> Role
+          </cd-submit-button>
+          <button i18n
+                  type="button"
+                  class="btn btn-sm btn-default"
+                  routerLink="/user-management/users/roles">
+            Back
+          </button>
+        </div>
+      </div>
+    </div>
+  </form>
+</div>
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 (file)
index 0000000..564487d
--- /dev/null
@@ -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 (file)
index 0000000..6da5dee
--- /dev/null
@@ -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<RoleFormComponent>;
+  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 (file)
index 0000000..a3e544f
--- /dev/null
@@ -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<string>;
+
+  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<string>) => {
+      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 (file)
index 0000000..74a7323
--- /dev/null
@@ -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 (file)
index 0000000..847e84c
--- /dev/null
@@ -0,0 +1,75 @@
+<cd-user-tabs></cd-user-tabs>
+
+<cd-table [data]="roles"
+          columnMode="flex"
+          [columns]="columns"
+          identifier="name"
+          selectionType="single"
+          (fetchData)="getRoles()"
+          (updateSelection)="updateSelection($event)">
+  <div class="table-actions">
+    <div class="btn-group" dropdown>
+      <button type="button"
+              class="btn btn-sm btn-primary"
+              *ngIf="permission.create && (
+                permission.update && !selection.hasSingleSelection ||
+                !permission.update)"
+              routerLink="/user-management/roles/add">
+        <i class="fa fa-fw fa-plus"></i>
+        <span i18n>Add</span>
+      </button>
+      <button type="button"
+              class="btn btn-sm btn-primary"
+              [ngClass]="{'disabled': !selection.hasSelection || selection.first().system}"
+              *ngIf="permission.update && (!permission.create || selection.hasSingleSelection)"
+              routerLink="/user-management/roles/edit/{{ selection.first()?.name }}">
+        <i class="fa fa-fw fa-pencil"></i>
+        <span i18n>Edit</span>
+      </button>
+      <button type="button"
+              class="btn btn-sm btn-primary"
+              [ngClass]="{'disabled': !selection.hasSelection || selection.first().system}"
+              *ngIf="permission.delete && !permission.create && !permission.update"
+              (click)="deleteRoleModal()">
+        <i class="fa fa-fw fa-trash-o"></i>
+        <span i18n>Delete</span>
+      </button>
+      <button type="button"
+              dropdownToggle
+              class="btn btn-sm btn-primary dropdown-toggle dropdown-toggle-split"
+              *ngIf="((permission.create?1:0) + (permission.update?1:0) + (permission.delete?1:0)) > 1">
+        <span class="caret"></span>
+        <span class="sr-only"></span>
+      </button>
+      <ul *dropdownMenu class="dropdown-menu" role="menu">
+        <li role="menuitem"
+            *ngIf="permission.create">
+          <a class="dropdown-item" routerLink="/user-management/roles/add">
+            <i class="fa fa-fw fa-plus"></i>
+            <span i18n>Add</span>
+          </a>
+        </li>
+        <li role="menuitem"
+            *ngIf="permission.update"
+            [ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().system}">
+          <a class="dropdown-item" routerLink="/user-management/roles/edit/{{ selection.first()?.name}}">
+            <i class="fa fa-fw fa-pencil"></i>
+            <span i18n>Edit</span>
+          </a>
+        </li>
+        <li role="menuitem"
+            *ngIf="permission.delete"
+            [ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().system}">
+          <a class="dropdown-item" (click)="deleteRoleModal()">
+            <i class="fa fa-fw fa-trash-o"></i>
+            <span i18n>Delete</span>
+          </a>
+        </li>
+      </ul>
+    </div>
+  </div>
+  <cd-role-details cdTableDetail
+                   [selection]="selection"
+                   [scopes]="scopes">
+  </cd-role-details>
+</cd-table>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..cc47b84
--- /dev/null
@@ -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<RoleListComponent>;
+
+  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 (file)
index 0000000..5919d38
--- /dev/null
@@ -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<any>;
+  scopes: Array<string>;
+  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<any>, Array<string>]) => {
+        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
+    });
+  }
+}
index 5bfa2fc8bd50f9cf8c4463ffdf3605639e9a697c..a21232eb2847d7c6fbf112a15a777e4cdef2abab 100644 (file)
           <button i18n
                   type="button"
                   class="btn btn-sm btn-default"
-                  routerLink="/users">
+                  routerLink="/user-management/users">
             Back
           </button>
         </div>
index b9ffbd970eb266643fe6c9fca0fda04caf0858ac..b7a7f8a5f8885e0f34c422e6be70b5692964be9a 100644 (file)
@@ -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']);
     });
   });
 });
index 02ff0340b3aeb6b59c33900632b590f7cf6025b8..dfc41ab7ed6536d9a2a25b0c619d4c99cdf69cc1 100644 (file)
@@ -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<UserFormRoleModel>) => {
@@ -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']);
         }
       },
       () => {
index e4368fd94add11f2f8811a957a65ea2ed544b438..817cca4729b1edbbcf18aab7fc94fd690cf9e55b 100644 (file)
@@ -1,3 +1,5 @@
+<cd-user-tabs></cd-user-tabs>
+
 <cd-table [data]="users"
           columnMode="flex"
           [columns]="columns"
@@ -12,7 +14,7 @@
               *ngIf="permission.create && (
                 permission.update && !selection.hasSingleSelection ||
                 !permission.update)"
-              routerLink="/users/add">
+              routerLink="/user-management/users/add">
         <i class="fa fa-fw fa-plus"></i>
         <span i18n>Add</span>
       </button>
@@ -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 }}">
         <i class="fa fa-fw fa-pencil"></i>
         <span i18n>Edit</span>
       </button>
@@ -42,7 +44,7 @@
       <ul *dropdownMenu class="dropdown-menu" role="menu">
         <li role="menuitem"
             *ngIf="permission.create">
-          <a class="dropdown-item" routerLink="/users/add">
+          <a class="dropdown-item" routerLink="/user-management/users/add">
             <i class="fa fa-fw fa-plus"></i>
             <span i18n>Add</span>
           </a>
@@ -50,7 +52,7 @@
         <li role="menuitem"
             *ngIf="permission.update"
             [ngClass]="{'disabled': !selection.hasSingleSelection}">
-          <a class="dropdown-item" routerLink="/users/edit/{{ selection.first()?.username}}">
+          <a class="dropdown-item" routerLink="/user-management/users/edit/{{ selection.first()?.username}}">
             <i class="fa fa-fw fa-pencil"></i>
             <span i18n>Edit</span>
           </a>
index 8511e46c55cc81f92c9ef41e0bfa45fbca304bc2..d497dfa7ad0577b112a6ead8d5e07e14547d9e89 100644 (file)
@@ -3,8 +3,10 @@ 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 { UserTabsComponent } from '../user-tabs/user-tabs.component';
 import { UserListComponent } from './user-list.component';
 
 describe('UserListComponent', () => {
@@ -13,8 +15,14 @@ describe('UserListComponent', () => {
 
   beforeEach(async(() => {
     TestBed.configureTestingModule({
-      imports: [SharedModule, ToastModule.forRoot(), RouterTestingModule, HttpClientTestingModule],
-      declarations: [UserListComponent]
+      imports: [
+        SharedModule,
+        ToastModule.forRoot(),
+        TabsModule.forRoot(),
+        RouterTestingModule,
+        HttpClientTestingModule
+      ],
+      declarations: [UserListComponent, UserTabsComponent]
     }).compileComponents();
   }));
 
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.html
new file mode 100644 (file)
index 0000000..4e0684d
--- /dev/null
@@ -0,0 +1,12 @@
+<tabset>
+  <tab heading="Users"
+       i18n-heading
+       [active]="url === '/user-management/users'"
+       (select)="navigateTo('/user-management/users')">
+  </tab>
+  <tab heading="Roles"
+       i18n-heading
+       [active]="url === '/user-management/roles'"
+       (select)="navigateTo('/user-management/roles')">
+  </tab>
+</tabset>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.spec.ts
new file mode 100644 (file)
index 0000000..475df8c
--- /dev/null
@@ -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 { UserTabsComponent } from './user-tabs.component';
+
+describe('UserTabsComponent', () => {
+  let component: UserTabsComponent;
+  let fixture: ComponentFixture<UserTabsComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [
+        SharedModule,
+        ToastModule.forRoot(),
+        TabsModule.forRoot(),
+        RouterTestingModule,
+        HttpClientTestingModule
+      ],
+      declarations: [UserTabsComponent]
+    }).compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(UserTabsComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-tabs/user-tabs.component.ts
new file mode 100644 (file)
index 0000000..16a3094
--- /dev/null
@@ -0,0 +1,22 @@
+import { Component, OnInit } from '@angular/core';
+
+import { Router } from '@angular/router';
+
+@Component({
+  selector: 'cd-user-tabs',
+  templateUrl: './user-tabs.component.html',
+  styleUrls: ['./user-tabs.component.scss']
+})
+export class UserTabsComponent implements OnInit {
+  url: string;
+
+  constructor(private router: Router) {}
+
+  ngOnInit() {
+    this.url = this.router.url;
+  }
+
+  navigateTo(url) {
+    this.router.navigate([url]);
+  }
+}
index 934c003d22b057b2961593854ee17fb2bc1c4758..696136280e571ddfd8f3b494b45deefee79de959 100644 (file)
@@ -7,11 +7,11 @@
     <span class="caret"></span>
   </a>
   <ul *dropdownMenu
-      class="dropdown-menu">
+      class="dropdown-menu dropdown-menu-right">
     <li *ngIf="userPermission.read">
       <a i18n
          class="dropdown-item"
-         routerLink="/users">Users
+         routerLink="/user-management">User management
       </a>
     </li>
   </ul>
index 6e1daa0407627781b843288581f8e35fbc5dd5ab..d3290157340e4705d838d7dc544b9a8dd6711277 100644 (file)
@@ -31,4 +31,38 @@ describe('RoleService', () => {
     const req = httpTesting.expectOne('api/role');
     expect(req.request.method).toBe('GET');
   });
+
+  it('should call delete', () => {
+    service.delete('role1').subscribe();
+    const req = httpTesting.expectOne('api/role/role1');
+    expect(req.request.method).toBe('DELETE');
+  });
+
+  it('should call get', () => {
+    service.get('role1').subscribe();
+    const req = httpTesting.expectOne('api/role/role1');
+    expect(req.request.method).toBe('GET');
+  });
+
+  it('should check if role name exists', () => {
+    let exists: boolean;
+    service.exists('role1').subscribe((res: boolean) => {
+      exists = res;
+    });
+    const req = httpTesting.expectOne('api/role');
+    expect(req.request.method).toBe('GET');
+    req.flush([{ name: 'role0' }, { name: 'role1' }]);
+    expect(exists).toBeTruthy();
+  });
+
+  it('should check if role name does not exist', () => {
+    let exists: boolean;
+    service.exists('role2').subscribe((res: boolean) => {
+      exists = res;
+    });
+    const req = httpTesting.expectOne('api/role');
+    expect(req.request.method).toBe('GET');
+    req.flush([{ name: 'role0' }, { name: 'role1' }]);
+    expect(exists).toBeFalsy();
+  });
 });
index 9c828b4255fa24fd7c613bb4cb61970491b6b4df..974ee5afb841de28c78db5e02924ceb328a59f9f 100644 (file)
@@ -1,6 +1,10 @@
 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 
+import { Observable, of as observableOf } from 'rxjs';
+import { mergeMap } from 'rxjs/operators';
+
+import { RoleFormModel } from '../../core/auth/role-form/role-form.model';
 import { ApiModule } from './api.module';
 
 @Injectable({
@@ -12,4 +16,31 @@ export class RoleService {
   list() {
     return this.http.get('api/role');
   }
+
+  delete(role: string) {
+    return this.http.delete(`api/role/${role}`);
+  }
+
+  get(name) {
+    return this.http.get(`api/role/${name}`);
+  }
+
+  create(role: RoleFormModel) {
+    return this.http.post(`api/role`, role);
+  }
+
+  update(role: RoleFormModel) {
+    return this.http.put(`api/role/${role.name}`, role);
+  }
+
+  exists(name: string): Observable<boolean> {
+    return this.list().pipe(
+      mergeMap((roles: Array<RoleFormModel>) => {
+        const exists = roles.some((currentRole: RoleFormModel) => {
+          return currentRole.name === name;
+        });
+        return observableOf(exists);
+      })
+    );
+  }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/scope.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/scope.service.spec.ts
new file mode 100644 (file)
index 0000000..2016ff5
--- /dev/null
@@ -0,0 +1,34 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '../../../testing/unit-test-helper';
+import { ScopeService } from './scope.service';
+
+describe('ScopeService', () => {
+  let service: ScopeService;
+  let httpTesting: HttpTestingController;
+
+  configureTestBed({
+    providers: [ScopeService],
+    imports: [HttpClientTestingModule]
+  });
+
+  beforeEach(() => {
+    service = TestBed.get(ScopeService);
+    httpTesting = TestBed.get(HttpTestingController);
+  });
+
+  afterEach(() => {
+    httpTesting.verify();
+  });
+
+  it('should be created', () => {
+    expect(service).toBeTruthy();
+  });
+
+  it('should call list', () => {
+    service.list().subscribe();
+    const req = httpTesting.expectOne('ui-api/scope');
+    expect(req.request.method).toBe('GET');
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/scope.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/scope.service.ts
new file mode 100644 (file)
index 0000000..8808f21
--- /dev/null
@@ -0,0 +1,15 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { ApiModule } from './api.module';
+
+@Injectable({
+  providedIn: ApiModule
+})
+export class ScopeService {
+  constructor(private http: HttpClient) {}
+
+  list() {
+    return this.http.get('ui-api/scope');
+  }
+}
index 39b2228e08b36ee522163b120a57e359b7f731b7..bf8daafc2b13353c08fcead7d46185e24ed5471f 100644 (file)
@@ -4,5 +4,6 @@ export enum Components {
   rbd = 'RBD',
   pool = 'Pool',
   osd = 'OSD',
+  role = 'Role',
   user = 'User'
 }