Skip to content

Commit d198dd0

Browse files
authored
Tt 97 grant or revoke roles to users (#626)
* TT-97 feat: add grant or revoke role admin to users * TT-97 fix: refactor code * fix: TT-97 add some test to feature * fix: TT-97 Add new tests and fix a typo errors * fix: TT-97 fix typo errors * fix: TT-97 Remove a function failed
1 parent 106fb9f commit d198dd0

15 files changed

+519
-37
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: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,37 @@
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="isUserRoleToggleOn">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="isUserRoleToggleOn">
20+
<div>
21+
<ui-switch
22+
size="small"
23+
(change)="switchRole(user.id, user.roles, 'admin', 'time-tracker-admin')"
24+
[checked]="user.roles.includes('time-tracker-admin')"
25+
></ui-switch>
26+
admin
27+
<ui-switch
28+
size="small"
29+
(change)="switchRole(user.id, user.roles, 'test', 'time-tracker-tester')"
30+
[checked]="user.roles.includes('time-tracker-tester')"
31+
></ui-switch>
32+
test
33+
</div>
34+
</td>
1335
</tr>
1436
</tbody>
1537
</table>

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

Lines changed: 102 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@ import { MockStore, provideMockStore } from '@ngrx/store/testing';
33

44
import { NgxPaginationModule } from 'ngx-pagination';
55
import { UsersListComponent } from './users-list.component';
6-
import { UserActionTypes, UserState, LoadUsers } from '../../store';
6+
import { UserActionTypes, UserState, LoadUsers, GrantRoleUser, RevokeRoleUser } from '../../store';
7+
import { FeatureManagerService } from 'src/app/modules/shared/feature-toggles/feature-toggle-manager.service';
78
import { ActionsSubject } from '@ngrx/store';
89
import { DataTablesModule } from 'angular-datatables';
10+
import { Observable, of } from 'rxjs';
911

1012
describe('UsersListComponent', () => {
1113
let component: UsersListComponent;
1214
let fixture: ComponentFixture<UsersListComponent>;
1315
let store: MockStore<UserState>;
16+
let featureManagerService: FeatureManagerService;
1417
const actionSub: ActionsSubject = new ActionsSubject();
1518

1619
const state: UserState = {
@@ -29,13 +32,16 @@ describe('UsersListComponent', () => {
2932
message: '',
3033
};
3134

32-
beforeEach(waitForAsync(() => {
33-
TestBed.configureTestingModule({
34-
imports: [NgxPaginationModule, DataTablesModule],
35-
declarations: [UsersListComponent],
36-
providers: [provideMockStore({ initialState: state }), { provide: ActionsSubject, useValue: actionSub }],
37-
}).compileComponents();
38-
}));
35+
beforeEach(
36+
waitForAsync(() => {
37+
TestBed.configureTestingModule({
38+
imports: [NgxPaginationModule, DataTablesModule],
39+
declarations: [UsersListComponent],
40+
providers: [provideMockStore({ initialState: state }), { provide: ActionsSubject, useValue: actionSub }],
41+
}).compileComponents();
42+
featureManagerService = TestBed.inject(FeatureManagerService);
43+
})
44+
);
3945

4046
beforeEach(() => {
4147
fixture = TestBed.createComponent(UsersListComponent);
@@ -69,6 +75,76 @@ describe('UsersListComponent', () => {
6975
expect(component.users).toEqual(state.data);
7076
});
7177

78+
it('When Component is created, should call the feature toggle method', () => {
79+
spyOn(component, 'isFeatureToggleActivated').and.returnValue(of(true));
80+
81+
component.ngOnInit();
82+
83+
expect(component.isFeatureToggleActivated).toHaveBeenCalled();
84+
expect(component.isUserRoleToggleOn).toBe(true);
85+
});
86+
87+
const actionsParams = [
88+
{ actionType: UserActionTypes.GRANT_USER_ROLE_SUCCESS },
89+
{ actionType: UserActionTypes.REVOKE_USER_ROLE_SUCCESS },
90+
];
91+
92+
actionsParams.map((param) => {
93+
it(`When action ${param.actionType} is dispatched should triggered load Users action`, () => {
94+
spyOn(store, 'dispatch');
95+
96+
const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
97+
const action = {
98+
type: param.actionType,
99+
payload: state.data,
100+
};
101+
102+
actionSubject.next(action);
103+
104+
expect(store.dispatch).toHaveBeenCalledWith(new LoadUsers());
105+
});
106+
});
107+
108+
const grantRoleTypes = [
109+
{ roleId: 'admin', roleValue: 'time-tracker-admin' },
110+
{ roleId: 'test', roleValue: 'time-tracker-tester' },
111+
];
112+
113+
grantRoleTypes.map((param) => {
114+
it(`When user switchRole to ${param.roleId} and don't have any role, should grant ${param.roleValue} Role`, () => {
115+
const roleId = param.roleId;
116+
const roleValue = param.roleValue;
117+
const userRoles = [];
118+
const userId = 'userId';
119+
120+
spyOn(store, 'dispatch');
121+
122+
component.switchRole(userId, userRoles, roleId, roleValue);
123+
124+
expect(store.dispatch).toHaveBeenCalledWith(new GrantRoleUser(userId, roleId));
125+
});
126+
});
127+
128+
const revokeRoleTypes = [
129+
{ roleId: 'admin', roleValue: 'time-tracker-admin', userRoles: ['time-tracker-admin'] },
130+
{ roleId: 'test', roleValue: 'time-tracker-tester', userRoles: ['time-tracker-tester'] },
131+
];
132+
133+
revokeRoleTypes.map((param) => {
134+
it(`When user switchRole to ${param.roleId} and have that rol asigned, should revoke ${param.roleValue} Role`, () => {
135+
const roleId = param.roleId;
136+
const roleValue = param.roleValue;
137+
const userRoles = param.userRoles;
138+
const userId = 'userId';
139+
140+
spyOn(store, 'dispatch');
141+
142+
component.switchRole(userId, userRoles, roleId, roleValue);
143+
144+
expect(store.dispatch).toHaveBeenCalledWith(new RevokeRoleUser(userId, roleId));
145+
});
146+
});
147+
72148
it('on success load users, the data of roles should be an array and role null', () => {
73149
const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
74150
const action = {
@@ -114,7 +190,23 @@ describe('UsersListComponent', () => {
114190
});
115191
});
116192

117-
it('on success load users, the datatable should be reloaded', async () => {
193+
const toggleValues = [true, false];
194+
toggleValues.map((toggleValue) => {
195+
it(`when FeatureToggle is ${toggleValue} should return ${toggleValue}`, () => {
196+
spyOn(featureManagerService, 'isToggleEnabledForUser').and.returnValue(of(toggleValue));
197+
198+
const isFeatureToggleActivated: Observable<boolean> = component.isFeatureToggleActivated();
199+
200+
expect(featureManagerService.isToggleEnabledForUser).toHaveBeenCalled();
201+
isFeatureToggleActivated.subscribe((value) => expect(value).toEqual(toggleValue));
202+
});
203+
});
204+
205+
/*
206+
TODO: block commented on purpose so that when the tests pass and the Feature toggle is removed,
207+
the table will be rendered again with dtInstance and not with dtOptions
208+
209+
it('on success load users, the datatable should be reloaded', async () => {
118210
const actionSubject = TestBed.inject(ActionsSubject);
119211
const action = {
120212
type: UserActionTypes.LOAD_USERS_SUCCESS,
@@ -125,7 +217,7 @@ describe('UsersListComponent', () => {
125217
actionSubject.next(action);
126218
127219
expect(component.dtElement.dtInstance.then).toHaveBeenCalled();
128-
});
220+
});*/
129221

130222
afterEach(() => {
131223
component.dtTrigger.unsubscribe();

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

Lines changed: 43 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',
@@ -15,22 +17,45 @@ export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit {
1517
users: User[] = [];
1618
isLoading$: Observable<boolean>;
1719
loadUsersSubscription: Subscription;
20+
switchRoleSubscription: Subscription;
1821
dtTrigger: Subject<any> = new Subject();
1922
@ViewChild(DataTableDirective, { static: false })
2023
dtElement: DataTableDirective;
24+
dtOptions: any = {};
25+
isUserRoleToggleOn;
2126

22-
constructor(private store: Store<User>, private actionsSubject$: ActionsSubject) {
27+
constructor(
28+
private store: Store<User>,
29+
private actionsSubject$: ActionsSubject,
30+
private featureManagerService: FeatureManagerService
31+
) {
2332
this.isLoading$ = store.pipe(delay(0), select(getIsLoading));
2433
}
2534

2635
ngOnInit(): void {
36+
this.isFeatureToggleActivated().subscribe((flag) => {
37+
this.isUserRoleToggleOn = flag;
38+
});
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.switchRoleSubscription = 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,18 @@ export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit {
5277
this.dtTrigger.next();
5378
}
5479
}
80+
81+
switchRole(userId: string, userRoles: string[], roleId: string, roleValue: string) {
82+
userRoles.includes(roleValue)
83+
? this.store.dispatch(new RevokeRoleUser(userId, roleId))
84+
: this.store.dispatch(new GrantRoleUser(userId, roleId));
85+
}
86+
87+
isFeatureToggleActivated() {
88+
return this.featureManagerService.isToggleEnabledForUser('ui-list-test-users').pipe(
89+
map((enabled) => {
90+
return enabled === true ? true : false;
91+
})
92+
);
93+
}
5594
}

0 commit comments

Comments
 (0)