Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
TT-97 feat: add grant or revoke role admin to users
  • Loading branch information
PaulRC-ioet committed Jan 18, 2021
commit e9bb5fbdce88d3c1517c51c984e8fad73e861abc
1 change: 1 addition & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"./node_modules/bootstrap/dist/css/bootstrap.min.css",
"src/styles.scss",
"node_modules/ngx-toastr/toastr.css",
"./node_modules/ngx-ui-switch/ui-switch.component.css",
"node_modules/datatables.net-buttons-dt/css/buttons.dataTables.css"
],
"scripts": [
Expand Down
35 changes: 20 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@
"@angular/platform-browser": "~10.2.2",
"@angular/platform-browser-dynamic": "~10.2.2",
"@angular/router": "~10.2.2",
"@azure/app-configuration": "^1.1.0",
"@azure/identity": "^1.1.0",
"@ngrx/effects": "^10.0.1",
"@ngrx/store": "^10.0.1",
"@ngrx/store-devtools": "^10.0.1",
"@types/datatables.net-buttons": "^1.4.3",
"angular-datatables": "^9.0.2",
"@azure/app-configuration": "^1.1.0",
"@azure/identity": "^1.1.0",
"bootstrap": "^4.4.1",
"datatables.net": "^1.10.21",
"datatables.net-buttons": "^1.6.2",
Expand All @@ -46,6 +46,7 @@
"ngx-material-timepicker": "^5.5.3",
"ngx-pagination": "^5.0.0",
"ngx-toastr": "^12.0.1",
"ngx-ui-switch": "^10.0.2",
"rxjs": "~6.6.3",
"tslib": "^1.10.0",
"zone.js": "~0.10.2"
Expand Down
2 changes: 2 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import { DialogComponent } from './modules/shared/components/dialog/dialog.compo
import { LoadingBarComponent } from './modules/shared/components/loading-bar/loading-bar.component';
import { UsersComponent } from './modules/users/pages/users.component';
import { UsersListComponent } from './modules/users/components/users-list/users-list.component';
import { UiSwitchModule } from 'ngx-ui-switch';
import {NgxMaterialTimepickerModule} from 'ngx-material-timepicker';
// tslint:disable-next-line: max-line-length
import { TechnologyReportTableComponent } from './modules/technology-report/components/technology-report-table/technology-report-table.component';
Expand Down Expand Up @@ -142,6 +143,7 @@ const maskConfig: Partial<IConfig> = {
DataTablesModule,
AutocompleteLibModule,
NgxMaterialTimepickerModule,
UiSwitchModule,
StoreModule.forRoot(reducers, {
metaReducers,
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
<table *ngIf="users" class="table table-sm table-bordered table-striped mb-0" datatable [dtTrigger]="dtTrigger">
<table
*ngIf="users && (isLoading$ | async) === false"
class="table table-sm table-bordered table-striped mb-0"
datatable
[dtOptions]="dtOptions"
>
<thead class="thead-blue">
<tr class="d-flex">
<th class="col-6">User Email</th>
<th class="col-6">Names</th>
<th class="col">User Email</th>
<th class="col">Names</th>
<th class="col" *ngIf="isFlagOn">Roles</th>
</tr>
</thead>
<app-loading-bar *ngIf="isLoading$ | async"></app-loading-bar>
<tbody *ngIf="(isLoading$ | async) === false">
<tr class="d-flex" *ngFor="let user of users">
<td class="col-sm-6">{{ user.email }}</td>
<td class="col-sm-6">{{ user.name }}</td>
<td class="col">{{ user.email }}</td>
<td class="col">{{ user.name }}</td>
<td class="col text-center" *ngIf="isFlagOn">
<div>
<ui-switch size="small" (change)="revokeOrGrantRole(user.id, user.role)" [checked]="isAdmin(user.role)"></ui-switch>
Admin
</div>
<div>
<ui-switch size="small"></ui-switch>
Test
</div>
</td>
</tr>
</tbody>
</table>
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ describe('UsersListComponent', () => {

expect(component.users).toEqual(state.data);
});
/*
TODO: blocke commented on purpose so that when the tests pass and the Feature toggle is removed,
the table will be rendered again with dtInstance and not with dtOptions

it('on success load users, the data of roles should be an array and role null', () => {
const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
Expand Down Expand Up @@ -125,7 +128,7 @@ describe('UsersListComponent', () => {
actionSubject.next(action);

expect(component.dtElement.dtInstance.then).toHaveBeenCalled();
});
});*/

afterEach(() => {
component.dtTrigger.unsubscribe();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular
import { ActionsSubject, select, Store } from '@ngrx/store';
import { DataTableDirective } from 'angular-datatables';
import { Observable, Subject, Subscription } from 'rxjs';
import { delay, filter } from 'rxjs/operators';
import { delay, filter, map } from 'rxjs/operators';
import { User } from '../../models/users';
import { LoadUsers, UserActionTypes } from '../../store/user.actions';
import { GrantRoleUser, LoadUsers, RevokeRoleUser, UserActionTypes } from '../../store/user.actions';
import { getIsLoading } from '../../store/user.selectors';
import { FeatureManagerService } from 'src/app/modules/shared/feature-toggles/feature-toggle-manager.service';

@Component({
selector: 'app-users-list',
templateUrl: './users-list.component.html',
Expand All @@ -18,19 +20,42 @@ export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit {
dtTrigger: Subject<any> = new Subject();
@ViewChild(DataTableDirective, { static: false })
dtElement: DataTableDirective;
dtOptions: any = {};
isFlagOn;

constructor(private store: Store<User>, private actionsSubject$: ActionsSubject) {
constructor(
private store: Store<User>,
private actionsSubject$: ActionsSubject,
private featureManagerService: FeatureManagerService
) {
this.isLoading$ = store.pipe(delay(0), select(getIsLoading));
this.isFeatureToggleAactivated().subscribe((flag) => {
this.isFlagOn = flag;
console.log(this.isFlagOn);
});
}

ngOnInit(): void {
this.store.dispatch(new LoadUsers());
this.loadUsersSubscription = this.actionsSubject$
.pipe(filter((action: any) => action.type === UserActionTypes.LOAD_USERS_SUCCESS))
.subscribe((action) => {
this.users = action.payload;
this.rerenderDataTable();
});
this.store.dispatch(new LoadUsers());

this.loadUsersSubscription = this.actionsSubject$
.pipe(
filter(
(action: any) =>
action.type === UserActionTypes.GRANT_USER_ROLE_SUCCESS ||
action.type === UserActionTypes.REVOKE_USER_ROLE_SUCCESS
)
)
.subscribe((action) => {
this.store.dispatch(new LoadUsers());
this.rerenderDataTable();
});
}

ngAfterViewInit(): void {
Expand All @@ -52,4 +77,26 @@ export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit {
this.dtTrigger.next();
}
}

isAdmin(role) {
return role ? true : false;
}

revokeOrGrantRole(userId: string, userRole: string) {
userRole
? this.store.dispatch(new RevokeRoleUser(userId, 'admin'))
: this.store.dispatch(new GrantRoleUser(userId, 'admin'));
}

isFeatureToggleAactivated() {
return this.featureManagerService.isToggleEnabledForUser('ui-list-test-users').pipe(
map((enabled) => {
if (enabled === true) {
return true;
} else {
return false;
}
})
);
}
}
20 changes: 20 additions & 0 deletions src/app/modules/users/services/users.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,24 @@ describe('UsersService', () => {
const loadUserRequest = httpMock.expectOne(service.baseUrl);
expect(loadUserRequest.request.method).toBe('GET');
});

it('grant role to a User', () => {
const userId = 'userId';
const roleId = 'admin';

service.grantRole(userId, roleId).subscribe();

const grantRoleRequest = httpMock.expectOne(`${service.baseUrl}/${userId}/roles/${roleId}/grant`);
expect(grantRoleRequest.request.method).toBe('POST');
});

it('revoke role to a User', () => {
const userId = 'userId';
const roleId = 'admin';

service.revokeRole(userId, roleId).subscribe();

const grantRoleRequest = httpMock.expectOne(`${service.baseUrl}/${userId}/roles/${roleId}/revoke`);
expect(grantRoleRequest.request.method).toBe('POST');
});
});
10 changes: 10 additions & 0 deletions src/app/modules/users/services/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,14 @@ export class UsersService {
loadUsers(): Observable<any> {
return this.http.get(this.baseUrl);
}

grantRole(userId: string, roleId: string): Observable<any> {
const url = `${this.baseUrl}/${userId}/roles/${roleId}/grant`;
return this.http.post(url, null);
}

revokeRole(userId: string, roleId: string): Observable<any> {
const url = `${this.baseUrl}/${userId}/roles/${roleId}/revoke`;
return this.http.post(url, null);
}
}
37 changes: 37 additions & 0 deletions src/app/modules/users/store/user.actions.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as actions from './user.actions';
import { User } from '../models/users';

describe('UserActions', () => {
it('LoadUsers type is UserActionTypes.LOAD_USERS', () => {
Expand All @@ -15,4 +16,40 @@ describe('UserActions', () => {
const action = new actions.LoadUsersFail('error');
expect(action.type).toEqual(actions.UserActionTypes.LOAD_USERS_FAIL);
});

it('GrantRoleUser type is UserActionTypes.GRANT_USER_ROLE', () => {
const UserId = 'UserId';
const RoleId = 'RoleId';
const action = new actions.GrantRoleUser(UserId, RoleId);
expect(action.type).toEqual(actions.UserActionTypes.GRANT_USER_ROLE);
});

it('GrantRoleUserSuccess type is UserActionTypes.GRANT_USER_ROLE_SUCCESS', () => {
const payload: User = { id: 'id', email: 'email', name: 'name' };
const action = new actions.GrantRoleUserSuccess(payload);
expect(action.type).toEqual(actions.UserActionTypes.GRANT_USER_ROLE_SUCCESS);
});

it('GrantRoleUserFail type is UserActionTypes.GRANT_USER_ROLE_FAIL', () => {
const action = new actions.GrantRoleUserFail('error');
expect(action.type).toEqual(actions.UserActionTypes.GRANT_USER_ROLE_FAIL);
});

it('RevokeRoleUser type is UserActionTypes.REVOKE_USER_ROLE', () => {
const UserId = 'UserId';
const RoleId = 'RoleId';
const action = new actions.RevokeRoleUser(UserId, RoleId);
expect(action.type).toEqual(actions.UserActionTypes.REVOKE_USER_ROLE);
});

it('RevokeRoleUserSuccess type is UserActionTypes.REVOKE_USER_ROLE_SUCCESS', () => {
const payload: User = { id: 'id', email: 'email', name: 'name' };
const action = new actions.RevokeRoleUserSuccess(payload);
expect(action.type).toEqual(actions.UserActionTypes.REVOKE_USER_ROLE_SUCCESS);
});

it('RevokeRoleUserFail type is UserActionTypes.REVOKE_USER_ROLE_FAIL', () => {
const action = new actions.RevokeRoleUserFail('error');
expect(action.type).toEqual(actions.UserActionTypes.REVOKE_USER_ROLE_FAIL);
});
});
Loading