Skip to content

Commit e9bb5fb

Browse files
committed
TT-97 feat: add grant or revoke role admin to users
1 parent 106fb9f commit e9bb5fb

15 files changed

+396
-28
lines changed

angular.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"./node_modules/bootstrap/dist/css/bootstrap.min.css",
3030
"src/styles.scss",
3131
"node_modules/ngx-toastr/toastr.css",
32+
"./node_modules/ngx-ui-switch/ui-switch.component.css",
3233
"node_modules/datatables.net-buttons-dt/css/buttons.dataTables.css"
3334
],
3435
"scripts": [

package-lock.json

Lines changed: 20 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@
2222
"@angular/platform-browser": "~10.2.2",
2323
"@angular/platform-browser-dynamic": "~10.2.2",
2424
"@angular/router": "~10.2.2",
25+
"@azure/app-configuration": "^1.1.0",
26+
"@azure/identity": "^1.1.0",
2527
"@ngrx/effects": "^10.0.1",
2628
"@ngrx/store": "^10.0.1",
2729
"@ngrx/store-devtools": "^10.0.1",
2830
"@types/datatables.net-buttons": "^1.4.3",
2931
"angular-datatables": "^9.0.2",
30-
"@azure/app-configuration": "^1.1.0",
31-
"@azure/identity": "^1.1.0",
3232
"bootstrap": "^4.4.1",
3333
"datatables.net": "^1.10.21",
3434
"datatables.net-buttons": "^1.6.2",
@@ -46,6 +46,7 @@
4646
"ngx-material-timepicker": "^5.5.3",
4747
"ngx-pagination": "^5.0.0",
4848
"ngx-toastr": "^12.0.1",
49+
"ngx-ui-switch": "^10.0.2",
4950
"rxjs": "~6.6.3",
5051
"tslib": "^1.10.0",
5152
"zone.js": "~0.10.2"

src/app/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import { DialogComponent } from './modules/shared/components/dialog/dialog.compo
7272
import { LoadingBarComponent } from './modules/shared/components/loading-bar/loading-bar.component';
7373
import { UsersComponent } from './modules/users/pages/users.component';
7474
import { UsersListComponent } from './modules/users/components/users-list/users-list.component';
75+
import { UiSwitchModule } from 'ngx-ui-switch';
7576
import {NgxMaterialTimepickerModule} from 'ngx-material-timepicker';
7677
// tslint:disable-next-line: max-line-length
7778
import { TechnologyReportTableComponent } from './modules/technology-report/components/technology-report-table/technology-report-table.component';
@@ -142,6 +143,7 @@ const maskConfig: Partial<IConfig> = {
142143
DataTablesModule,
143144
AutocompleteLibModule,
144145
NgxMaterialTimepickerModule,
146+
UiSwitchModule,
145147
StoreModule.forRoot(reducers, {
146148
metaReducers,
147149
}),
Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,31 @@
1-
<table *ngIf="users" class="table table-sm table-bordered table-striped mb-0" datatable [dtTrigger]="dtTrigger">
1+
<table
2+
*ngIf="users && (isLoading$ | async) === false"
3+
class="table table-sm table-bordered table-striped mb-0"
4+
datatable
5+
[dtOptions]="dtOptions"
6+
>
27
<thead class="thead-blue">
38
<tr class="d-flex">
4-
<th class="col-6">User Email</th>
5-
<th class="col-6">Names</th>
9+
<th class="col">User Email</th>
10+
<th class="col">Names</th>
11+
<th class="col" *ngIf="isFlagOn">Roles</th>
612
</tr>
713
</thead>
814
<app-loading-bar *ngIf="isLoading$ | async"></app-loading-bar>
915
<tbody *ngIf="(isLoading$ | async) === false">
1016
<tr class="d-flex" *ngFor="let user of users">
11-
<td class="col-sm-6">{{ user.email }}</td>
12-
<td class="col-sm-6">{{ user.name }}</td>
17+
<td class="col">{{ user.email }}</td>
18+
<td class="col">{{ user.name }}</td>
19+
<td class="col text-center" *ngIf="isFlagOn">
20+
<div>
21+
<ui-switch size="small" (change)="revokeOrGrantRole(user.id, user.role)" [checked]="isAdmin(user.role)"></ui-switch>
22+
Admin
23+
</div>
24+
<div>
25+
<ui-switch size="small"></ui-switch>
26+
Test
27+
</div>
28+
</td>
1329
</tr>
1430
</tbody>
1531
</table>

src/app/modules/users/components/users-list/users-list.component.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ describe('UsersListComponent', () => {
6868

6969
expect(component.users).toEqual(state.data);
7070
});
71+
/*
72+
TODO: blocke commented on purpose so that when the tests pass and the Feature toggle is removed,
73+
the table will be rendered again with dtInstance and not with dtOptions
7174
7275
it('on success load users, the data of roles should be an array and role null', () => {
7376
const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
@@ -125,7 +128,7 @@ describe('UsersListComponent', () => {
125128
actionSubject.next(action);
126129
127130
expect(component.dtElement.dtInstance.then).toHaveBeenCalled();
128-
});
131+
});*/
129132

130133
afterEach(() => {
131134
component.dtTrigger.unsubscribe();

src/app/modules/users/components/users-list/users-list.component.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular
22
import { ActionsSubject, select, Store } from '@ngrx/store';
33
import { DataTableDirective } from 'angular-datatables';
44
import { Observable, Subject, Subscription } from 'rxjs';
5-
import { delay, filter } from 'rxjs/operators';
5+
import { delay, filter, map } from 'rxjs/operators';
66
import { User } from '../../models/users';
7-
import { LoadUsers, UserActionTypes } from '../../store/user.actions';
7+
import { GrantRoleUser, LoadUsers, RevokeRoleUser, UserActionTypes } from '../../store/user.actions';
88
import { getIsLoading } from '../../store/user.selectors';
9+
import { FeatureManagerService } from 'src/app/modules/shared/feature-toggles/feature-toggle-manager.service';
10+
911
@Component({
1012
selector: 'app-users-list',
1113
templateUrl: './users-list.component.html',
@@ -18,19 +20,42 @@ export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit {
1820
dtTrigger: Subject<any> = new Subject();
1921
@ViewChild(DataTableDirective, { static: false })
2022
dtElement: DataTableDirective;
23+
dtOptions: any = {};
24+
isFlagOn;
2125

22-
constructor(private store: Store<User>, private actionsSubject$: ActionsSubject) {
26+
constructor(
27+
private store: Store<User>,
28+
private actionsSubject$: ActionsSubject,
29+
private featureManagerService: FeatureManagerService
30+
) {
2331
this.isLoading$ = store.pipe(delay(0), select(getIsLoading));
32+
this.isFeatureToggleAactivated().subscribe((flag) => {
33+
this.isFlagOn = flag;
34+
console.log(this.isFlagOn);
35+
});
2436
}
2537

2638
ngOnInit(): void {
39+
this.store.dispatch(new LoadUsers());
2740
this.loadUsersSubscription = this.actionsSubject$
2841
.pipe(filter((action: any) => action.type === UserActionTypes.LOAD_USERS_SUCCESS))
2942
.subscribe((action) => {
3043
this.users = action.payload;
3144
this.rerenderDataTable();
3245
});
33-
this.store.dispatch(new LoadUsers());
46+
47+
this.loadUsersSubscription = this.actionsSubject$
48+
.pipe(
49+
filter(
50+
(action: any) =>
51+
action.type === UserActionTypes.GRANT_USER_ROLE_SUCCESS ||
52+
action.type === UserActionTypes.REVOKE_USER_ROLE_SUCCESS
53+
)
54+
)
55+
.subscribe((action) => {
56+
this.store.dispatch(new LoadUsers());
57+
this.rerenderDataTable();
58+
});
3459
}
3560

3661
ngAfterViewInit(): void {
@@ -52,4 +77,26 @@ export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit {
5277
this.dtTrigger.next();
5378
}
5479
}
80+
81+
isAdmin(role) {
82+
return role ? true : false;
83+
}
84+
85+
revokeOrGrantRole(userId: string, userRole: string) {
86+
userRole
87+
? this.store.dispatch(new RevokeRoleUser(userId, 'admin'))
88+
: this.store.dispatch(new GrantRoleUser(userId, 'admin'));
89+
}
90+
91+
isFeatureToggleAactivated() {
92+
return this.featureManagerService.isToggleEnabledForUser('ui-list-test-users').pipe(
93+
map((enabled) => {
94+
if (enabled === true) {
95+
return true;
96+
} else {
97+
return false;
98+
}
99+
})
100+
);
101+
}
55102
}

src/app/modules/users/services/users.service.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,24 @@ describe('UsersService', () => {
3131
const loadUserRequest = httpMock.expectOne(service.baseUrl);
3232
expect(loadUserRequest.request.method).toBe('GET');
3333
});
34+
35+
it('grant role to a User', () => {
36+
const userId = 'userId';
37+
const roleId = 'admin';
38+
39+
service.grantRole(userId, roleId).subscribe();
40+
41+
const grantRoleRequest = httpMock.expectOne(`${service.baseUrl}/${userId}/roles/${roleId}/grant`);
42+
expect(grantRoleRequest.request.method).toBe('POST');
43+
});
44+
45+
it('revoke role to a User', () => {
46+
const userId = 'userId';
47+
const roleId = 'admin';
48+
49+
service.revokeRole(userId, roleId).subscribe();
50+
51+
const grantRoleRequest = httpMock.expectOne(`${service.baseUrl}/${userId}/roles/${roleId}/revoke`);
52+
expect(grantRoleRequest.request.method).toBe('POST');
53+
});
3454
});

src/app/modules/users/services/users.service.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,14 @@ export class UsersService {
1313
loadUsers(): Observable<any> {
1414
return this.http.get(this.baseUrl);
1515
}
16+
17+
grantRole(userId: string, roleId: string): Observable<any> {
18+
const url = `${this.baseUrl}/${userId}/roles/${roleId}/grant`;
19+
return this.http.post(url, null);
20+
}
21+
22+
revokeRole(userId: string, roleId: string): Observable<any> {
23+
const url = `${this.baseUrl}/${userId}/roles/${roleId}/revoke`;
24+
return this.http.post(url, null);
25+
}
1626
}

src/app/modules/users/store/user.actions.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as actions from './user.actions';
2+
import { User } from '../models/users';
23

34
describe('UserActions', () => {
45
it('LoadUsers type is UserActionTypes.LOAD_USERS', () => {
@@ -15,4 +16,40 @@ describe('UserActions', () => {
1516
const action = new actions.LoadUsersFail('error');
1617
expect(action.type).toEqual(actions.UserActionTypes.LOAD_USERS_FAIL);
1718
});
19+
20+
it('GrantRoleUser type is UserActionTypes.GRANT_USER_ROLE', () => {
21+
const UserId = 'UserId';
22+
const RoleId = 'RoleId';
23+
const action = new actions.GrantRoleUser(UserId, RoleId);
24+
expect(action.type).toEqual(actions.UserActionTypes.GRANT_USER_ROLE);
25+
});
26+
27+
it('GrantRoleUserSuccess type is UserActionTypes.GRANT_USER_ROLE_SUCCESS', () => {
28+
const payload: User = { id: 'id', email: 'email', name: 'name' };
29+
const action = new actions.GrantRoleUserSuccess(payload);
30+
expect(action.type).toEqual(actions.UserActionTypes.GRANT_USER_ROLE_SUCCESS);
31+
});
32+
33+
it('GrantRoleUserFail type is UserActionTypes.GRANT_USER_ROLE_FAIL', () => {
34+
const action = new actions.GrantRoleUserFail('error');
35+
expect(action.type).toEqual(actions.UserActionTypes.GRANT_USER_ROLE_FAIL);
36+
});
37+
38+
it('RevokeRoleUser type is UserActionTypes.REVOKE_USER_ROLE', () => {
39+
const UserId = 'UserId';
40+
const RoleId = 'RoleId';
41+
const action = new actions.RevokeRoleUser(UserId, RoleId);
42+
expect(action.type).toEqual(actions.UserActionTypes.REVOKE_USER_ROLE);
43+
});
44+
45+
it('RevokeRoleUserSuccess type is UserActionTypes.REVOKE_USER_ROLE_SUCCESS', () => {
46+
const payload: User = { id: 'id', email: 'email', name: 'name' };
47+
const action = new actions.RevokeRoleUserSuccess(payload);
48+
expect(action.type).toEqual(actions.UserActionTypes.REVOKE_USER_ROLE_SUCCESS);
49+
});
50+
51+
it('RevokeRoleUserFail type is UserActionTypes.REVOKE_USER_ROLE_FAIL', () => {
52+
const action = new actions.RevokeRoleUserFail('error');
53+
expect(action.type).toEqual(actions.UserActionTypes.REVOKE_USER_ROLE_FAIL);
54+
});
1855
});

0 commit comments

Comments
 (0)