Skip to content

Commit 03ff558

Browse files
LEON12699thegreatyamoriwobravo
authored
feat: TT-190 use add remove groups endpoints users (#654)
* feat: TT-188 add & remove groups to user service * feat: TT-188 add ngrx flow & test * refactor: TT-188 refactor some names * refactor: TT-188 refactor 'removeTo' to 'removeFrom' references * fix: TT-190 use add remove groups * feat: TT-190 Use add/remove groups endpoints in users section in UI * feat: TT-190 resolve coments Co-authored-by: thegreatyamori <[email protected]> Co-authored-by: wobravo <[email protected]>
1 parent 14945aa commit 03ff558

File tree

3 files changed

+208
-9
lines changed

3 files changed

+208
-9
lines changed

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<tr class="d-flex flex-wrap">
1111
<th class="col-4">User Email</th>
1212
<th class="col-5">Names</th>
13-
<th class="col-3">Roles</th>
13+
<th class="col-3">{{ isUserGroupsToggleOn ? 'Groups' : 'Roles' }}</th>
1414
</tr>
1515
</thead>
1616
<app-loading-bar *ngIf="isLoading$ | async"></app-loading-bar>
@@ -19,7 +19,22 @@
1919
<td class="col-4 text-break">{{ user.email }}</td>
2020
<td class="col-5 text-break">{{ user.name }}</td>
2121
<td class="col-3 text-center">
22-
<div>
22+
<div *ngIf="isUserGroupsToggleOn">
23+
<ui-switch
24+
size="small"
25+
(change)="switchGroup('time-tracker-admin', user)"
26+
[checked]="user.groups.includes('time-tracker-admin')"
27+
></ui-switch>
28+
admin
29+
<ui-switch
30+
size="small"
31+
(change)="switchGroup('time-tracker-tester', user)"
32+
[checked]="user.groups.includes('time-tracker-tester')"
33+
></ui-switch>
34+
test
35+
</div>
36+
37+
<div *ngIf="!isUserGroupsToggleOn">
2338
<ui-switch
2439
size="small"
2540
(change)="switchRole(user.id, user.roles, 'admin', 'time-tracker-admin')"

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

Lines changed: 140 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,42 @@
1+
import { FeatureManagerService } from 'src/app/modules/shared/feature-toggles/feature-toggle-manager.service';
12
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
23
import { MockStore, provideMockStore } from '@ngrx/store/testing';
3-
44
import { NgxPaginationModule } from 'ngx-pagination';
55
import { UsersListComponent } from './users-list.component';
6-
import { UserActionTypes, UserState, LoadUsers, GrantRoleUser, RevokeRoleUser } from '../../store';
6+
import {
7+
UserActionTypes,
8+
UserState,
9+
LoadUsers,
10+
GrantRoleUser,
11+
RevokeRoleUser,
12+
AddUserToGroup,
13+
RemoveUserFromGroup,
14+
} from '../../store';
15+
import { User } from '../../../user/models/user';
716
import { ActionsSubject } from '@ngrx/store';
817
import { DataTablesModule } from 'angular-datatables';
18+
import { Observable, of } from 'rxjs';
19+
import { FeatureToggleProvider } from 'src/app/modules/shared/feature-toggles/feature-toggle-provider.service';
20+
import { AppConfigurationClient } from '@azure/app-configuration';
21+
import { FeatureFilterProvider } from '../../../shared/feature-toggles/filters/feature-filter-provider.service';
22+
import { AzureAdB2CService } from '../../../login/services/azure.ad.b2c.service';
923

1024
describe('UsersListComponent', () => {
1125
let component: UsersListComponent;
1226
let fixture: ComponentFixture<UsersListComponent>;
1327
let store: MockStore<UserState>;
1428
const actionSub: ActionsSubject = new ActionsSubject();
29+
const fakeAppConfigurationConnectionString = 'Endpoint=http://fake.foo;Id=fake.id;Secret=fake.secret';
30+
let service: FeatureManagerService;
31+
let fakeFeatureToggleProvider;
1532

1633
const state: UserState = {
1734
data: [
1835
{
1936
name: 'name',
2037
email: 'email',
2138
roles: ['admin', 'test'],
39+
groups: ['time-tracker-admin', 'time-tracker-tester'],
2240
id: 'id',
2341
tenant_id: 'tenant id',
2442
deleted: 'delete',
@@ -30,10 +48,20 @@ describe('UsersListComponent', () => {
3048

3149
beforeEach(
3250
waitForAsync(() => {
51+
fakeFeatureToggleProvider = new FeatureToggleProvider(
52+
new AppConfigurationClient(fakeAppConfigurationConnectionString),
53+
new FeatureFilterProvider(new AzureAdB2CService())
54+
);
55+
service = new FeatureManagerService(fakeFeatureToggleProvider);
56+
3357
TestBed.configureTestingModule({
3458
imports: [NgxPaginationModule, DataTablesModule],
3559
declarations: [UsersListComponent],
36-
providers: [provideMockStore({ initialState: state }), { provide: ActionsSubject, useValue: actionSub }],
60+
providers: [
61+
provideMockStore({ initialState: state }),
62+
{ provide: ActionsSubject, useValue: actionSub },
63+
{ provide: FeatureManagerService, useValue: service }
64+
],
3765
}).compileComponents();
3866
})
3967
);
@@ -91,6 +119,27 @@ describe('UsersListComponent', () => {
91119
});
92120
});
93121

122+
const actionGroupParams = [
123+
{ actionType: UserActionTypes.ADD_USER_TO_GROUP_SUCCESS },
124+
{ actionType: UserActionTypes.REMOVE_USER_FROM_GROUP_SUCCESS },
125+
];
126+
127+
actionGroupParams.map((param) => {
128+
it(`When action ${param.actionType} is dispatched should triggered load Users action`, () => {
129+
spyOn(store, 'dispatch');
130+
131+
const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
132+
const action = {
133+
type: param.actionType,
134+
payload: state.data,
135+
};
136+
137+
actionSubject.next(action);
138+
139+
expect(store.dispatch).toHaveBeenCalledWith(new LoadUsers());
140+
});
141+
});
142+
94143
const grantRoleTypes = [
95144
{ roleId: 'admin', roleValue: 'time-tracker-admin' },
96145
{ roleId: 'test', roleValue: 'time-tracker-tester' },
@@ -111,6 +160,32 @@ describe('UsersListComponent', () => {
111160
});
112161
});
113162

163+
const AddGroupTypes = [
164+
{ groupName: 'time-tracker-admin' },
165+
{ groupName: 'time-tracker-tester' }
166+
];
167+
168+
AddGroupTypes.map((param) => {
169+
it(`When user switchGroup to ${param.groupName} and doesn't belong to any group, should add ${param.groupName} group to user`, () => {
170+
const groupName = param.groupName;
171+
const user = {
172+
name: 'name',
173+
email: 'email',
174+
roles: [],
175+
groups: [],
176+
id: 'id',
177+
tenant_id: 'tenant id',
178+
deleted: 'delete',
179+
} ;
180+
181+
spyOn(store, 'dispatch');
182+
183+
component.switchGroup(groupName, user);
184+
185+
expect(store.dispatch).toHaveBeenCalledWith(new AddUserToGroup(user.id, groupName));
186+
});
187+
});
188+
114189
const revokeRoleTypes = [
115190
{ roleId: 'admin', roleValue: 'time-tracker-admin', userRoles: ['time-tracker-admin'] },
116191
{ roleId: 'test', roleValue: 'time-tracker-tester', userRoles: ['time-tracker-tester'] },
@@ -131,6 +206,33 @@ describe('UsersListComponent', () => {
131206
});
132207
});
133208

209+
const removeGroupTypes = [
210+
{ groupName: 'time-tracker-admin', userGroups: ['time-tracker-admin'] },
211+
{ groupName: 'time-tracker-tester', userGroups: ['time-tracker-tester'] },
212+
];
213+
214+
removeGroupTypes.map((param) => {
215+
it(`When user switchGroup to ${param.groupName} and belongs to group, should remove ${param.groupName} group from user`, () => {
216+
const groupName = param.groupName;
217+
const user = {
218+
name: 'name',
219+
email: 'email',
220+
roles: [],
221+
groups: param.userGroups,
222+
id: 'id',
223+
tenant_id: 'tenant id',
224+
deleted: 'delete',
225+
} ;
226+
227+
228+
spyOn(store, 'dispatch');
229+
230+
component.switchGroup(groupName, user);
231+
232+
expect(store.dispatch).toHaveBeenCalledWith(new RemoveUserFromGroup(user.id, groupName));
233+
});
234+
});
235+
134236
it('on success load users, the data of roles should be an array', () => {
135237
const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
136238
const action = {
@@ -145,6 +247,20 @@ describe('UsersListComponent', () => {
145247
});
146248
});
147249

250+
it('on success load users, the data of groups should be an array', () => {
251+
const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
252+
const action = {
253+
type: UserActionTypes.LOAD_USERS_SUCCESS,
254+
payload: state.data,
255+
};
256+
257+
actionSubject.next(action);
258+
259+
component.users.map((user) => {
260+
expect(user.groups).toEqual(['time-tracker-admin', 'time-tracker-tester']);
261+
});
262+
});
263+
148264
it('on success load users, the datatable should be reloaded', async () => {
149265
const actionSubject = TestBed.inject(ActionsSubject);
150266
const action = {
@@ -158,6 +274,27 @@ describe('UsersListComponent', () => {
158274
expect(component.dtElement.dtInstance.then).toHaveBeenCalled();
159275
});
160276

277+
it('When Component is created, should call the feature toggle method', () => {
278+
spyOn(component, 'isFeatureToggleActivated').and.returnValue(of(true));
279+
280+
component.ngOnInit();
281+
282+
expect(component.isFeatureToggleActivated).toHaveBeenCalled();
283+
expect(component.isUserGroupsToggleOn).toBe(true);
284+
});
285+
286+
const toggleValues = [true, false];
287+
toggleValues.map((toggleValue) => {
288+
it(`when FeatureToggle is ${toggleValue} should return ${toggleValue}`, () => {
289+
spyOn(service, 'isToggleEnabledForUser').and.returnValue(of(toggleValue));
290+
291+
const isFeatureToggleActivated: Observable<boolean> = component.isFeatureToggleActivated();
292+
293+
expect(service.isToggleEnabledForUser).toHaveBeenCalled();
294+
isFeatureToggleActivated.subscribe((value) => expect(value).toEqual(toggleValue));
295+
});
296+
});
297+
161298
afterEach(() => {
162299
component.dtTrigger.unsubscribe();
163300
component.loadUsersSubscription.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
@@ -1,10 +1,18 @@
11
import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
2-
import { ActionsSubject, select, Store } from '@ngrx/store';
2+
import { ActionsSubject, select, Store, Action } 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';
6+
import { FeatureManagerService } from 'src/app/modules/shared/feature-toggles/feature-toggle-manager.service';
67
import { User } from '../../models/users';
7-
import { GrantRoleUser, LoadUsers, RevokeRoleUser, UserActionTypes } from '../../store/user.actions';
8+
import {
9+
GrantRoleUser,
10+
LoadUsers,
11+
RevokeRoleUser,
12+
UserActionTypes,
13+
AddUserToGroup,
14+
RemoveUserFromGroup,
15+
} from '../../store/user.actions';
816
import { getIsLoading } from '../../store/user.selectors';
917

1018
@Component({
@@ -21,8 +29,15 @@ export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit {
2129
@ViewChild(DataTableDirective, { static: false })
2230
dtElement: DataTableDirective;
2331
dtOptions: any = {};
32+
switchGroupsSubscription: Subscription;
33+
isEnableToggleSubscription: Subscription;
34+
isUserGroupsToggleOn: boolean;
2435

25-
constructor(private store: Store<User>, private actionsSubject$: ActionsSubject) {
36+
constructor(
37+
private store: Store<User>,
38+
private actionsSubject$: ActionsSubject,
39+
private featureManagerService: FeatureManagerService
40+
) {
2641
this.isLoading$ = store.pipe(delay(0), select(getIsLoading));
2742
}
2843

@@ -35,6 +50,14 @@ export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit {
3550
this.rerenderDataTable();
3651
});
3752

53+
this.isEnableToggleSubscription = this.isFeatureToggleActivated().subscribe((flag) => {
54+
this.isUserGroupsToggleOn = flag;
55+
});
56+
57+
this.switchGroupsSubscription = this.filterUserGroup().subscribe((action) => {
58+
this.store.dispatch(new LoadUsers());
59+
});
60+
3861
this.switchRoleSubscription = this.actionsSubject$
3962
.pipe(
4063
filter(
@@ -55,6 +78,7 @@ export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit {
5578
ngOnDestroy() {
5679
this.loadUsersSubscription.unsubscribe();
5780
this.dtTrigger.unsubscribe();
81+
this.isEnableToggleSubscription.unsubscribe();
5882
}
5983

6084
private rerenderDataTable(): void {
@@ -73,4 +97,27 @@ export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit {
7397
? this.store.dispatch(new RevokeRoleUser(userId, roleId))
7498
: this.store.dispatch(new GrantRoleUser(userId, roleId));
7599
}
100+
101+
switchGroup(groupName: string, user: User): void {
102+
this.store.dispatch(
103+
user.groups.includes(groupName)
104+
? new RemoveUserFromGroup(user.id, groupName)
105+
: new AddUserToGroup(user.id, groupName)
106+
);
107+
}
108+
109+
isFeatureToggleActivated(): Observable<boolean> {
110+
return this.featureManagerService.isToggleEnabledForUser('switch-group')
111+
.pipe(map((enabled: boolean) => enabled));
112+
}
113+
114+
filterUserGroup(): Observable<Action> {
115+
return this.actionsSubject$.pipe(
116+
filter(
117+
(action: Action) =>
118+
action.type === UserActionTypes.ADD_USER_TO_GROUP_SUCCESS ||
119+
action.type === UserActionTypes.REMOVE_USER_FROM_GROUP_SUCCESS
120+
)
121+
);
122+
}
76123
}

0 commit comments

Comments
 (0)