Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
24 changes: 20 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ Run `npm install` to install the required node_modules for this project.
Run `ng serve` to run the app in dev mode. After executing this command, you can navigate to `http://localhost:4200/` to see the app working.
The app will automatically reload if you change anything in the source files.

## Prepare your environment
# Prepare your environment

### Set environment variables
Create a file keys.ts in the path `src/enviroment` with the content pinned in our slack channel:
**1**. Create a file keys.ts in the path `src/enviroment` with the content pinned in our slack channel #time-tracker-developer:

```
export const AUTHORITY = 'XXX';
Expand All @@ -44,7 +44,14 @@ export const STACK_EXCHANGE_ID = 'XXX';
export const STACK_EXCHANGE_ACCESS_TOKEN = 'XXX';
export const AZURE_APP_CONFIGURATION_CONNECTION_STRING = 'XXX';
```

**2**. Create a second file `.keys.json` with the content pinned in the slack channel #time-tracker-developer:
```
{
"authority": 'XXX',
"client_id": 'XXX',
"scopes": ["XXX"]
}
```
### Prepare your environment for vscode
Install the following extensions:

Expand Down Expand Up @@ -82,6 +89,12 @@ Install the following extensions:
| `feat(pencil): add 'graphiteWidth' option` | ~~Minor~~ Feature Release |
| `perf(pencil): remove graphiteWidth option`<br><br>`BREAKING CHANGE: The graphiteWidth option has been removed.`<br>`The default graphite width of 10mm is always used for performance reasons.` | ~~Major~~ Breaking Release |

### Branch names format
For example if your task in Jira is **TT-48 implement semantic versioning** your branch name is:
```
TT-48-implement-semantic-versioning
```

## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.

Expand Down Expand Up @@ -127,4 +140,7 @@ To get more help on the Angular CLI use `ng help` or go check out the [Angular C
## Feature Toggles dictionary

Shared file with all the Feature Toggles we create, so we can have a history of them
[Feature Toggles dictionary](https://github.com/ioet/time-tracker-ui/wiki/Feature-Toggles-dictionary)
[Feature Toggles dictionary](https://github.com/ioet/time-tracker-ui/wiki/Feature-Toggles-dictionary)

## More information about the project
[Starting in Time Tracker](https://github.com/ioet/time-tracker-ui/wiki/Time-tracker)
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<tr class="d-flex flex-wrap">
<th class="col-4">User Email</th>
<th class="col-5">Names</th>
<th class="col-3">Roles</th>
<th class="col-3">{{ isUserGroupsToggleOn ? 'Groups' : 'Roles' }}</th>
</tr>
</thead>
<app-loading-bar *ngIf="isLoading$ | async"></app-loading-bar>
Expand All @@ -19,7 +19,22 @@
<td class="col-4 text-break">{{ user.email }}</td>
<td class="col-5 text-break">{{ user.name }}</td>
<td class="col-3 text-center">
<div>
<div *ngIf="isUserGroupsToggleOn">
<ui-switch
size="small"
(change)="switchGroup('time-tracker-admin', user)"
[checked]="user.groups.includes('time-tracker-admin')"
></ui-switch>
admin
<ui-switch
size="small"
(change)="switchGroup('time-tracker-tester', user)"
[checked]="user.groups.includes('time-tracker-tester')"
></ui-switch>
test
</div>

<div *ngIf="!isUserGroupsToggleOn">
<ui-switch
size="small"
(change)="switchRole(user.id, user.roles, 'admin', 'time-tracker-admin')"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,42 @@
import { FeatureManagerService } from 'src/app/modules/shared/feature-toggles/feature-toggle-manager.service';
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { MockStore, provideMockStore } from '@ngrx/store/testing';

import { NgxPaginationModule } from 'ngx-pagination';
import { UsersListComponent } from './users-list.component';
import { UserActionTypes, UserState, LoadUsers, GrantRoleUser, RevokeRoleUser } from '../../store';
import {
UserActionTypes,
UserState,
LoadUsers,
GrantRoleUser,
RevokeRoleUser,
AddUserToGroup,
RemoveUserFromGroup,
} from '../../store';
import { User } from '../../../user/models/user';
import { ActionsSubject } from '@ngrx/store';
import { DataTablesModule } from 'angular-datatables';
import { Observable, of } from 'rxjs';
import { FeatureToggleProvider } from 'src/app/modules/shared/feature-toggles/feature-toggle-provider.service';
import { AppConfigurationClient } from '@azure/app-configuration';
import { FeatureFilterProvider } from '../../../shared/feature-toggles/filters/feature-filter-provider.service';
import { AzureAdB2CService } from '../../../login/services/azure.ad.b2c.service';

describe('UsersListComponent', () => {
let component: UsersListComponent;
let fixture: ComponentFixture<UsersListComponent>;
let store: MockStore<UserState>;
const actionSub: ActionsSubject = new ActionsSubject();
const fakeAppConfigurationConnectionString = 'Endpoint=http://fake.foo;Id=fake.id;Secret=fake.secret';
let service: FeatureManagerService;
let fakeFeatureToggleProvider;

const state: UserState = {
data: [
{
name: 'name',
email: 'email',
roles: ['admin', 'test'],
groups: ['time-tracker-admin', 'time-tracker-tester'],
id: 'id',
tenant_id: 'tenant id',
deleted: 'delete',
Expand All @@ -30,10 +48,20 @@ describe('UsersListComponent', () => {

beforeEach(
waitForAsync(() => {
fakeFeatureToggleProvider = new FeatureToggleProvider(
new AppConfigurationClient(fakeAppConfigurationConnectionString),
new FeatureFilterProvider(new AzureAdB2CService())
);
service = new FeatureManagerService(fakeFeatureToggleProvider);

TestBed.configureTestingModule({
imports: [NgxPaginationModule, DataTablesModule],
declarations: [UsersListComponent],
providers: [provideMockStore({ initialState: state }), { provide: ActionsSubject, useValue: actionSub }],
providers: [
provideMockStore({ initialState: state }),
{ provide: ActionsSubject, useValue: actionSub },
{ provide: FeatureManagerService, useValue: service }
],
}).compileComponents();
})
);
Expand Down Expand Up @@ -91,6 +119,27 @@ describe('UsersListComponent', () => {
});
});

const actionGroupParams = [
{ actionType: UserActionTypes.ADD_USER_TO_GROUP_SUCCESS },
{ actionType: UserActionTypes.REMOVE_USER_FROM_GROUP_SUCCESS },
];

actionGroupParams.map((param) => {
it(`When action ${param.actionType} is dispatched should triggered load Users action`, () => {
spyOn(store, 'dispatch');

const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
const action = {
type: param.actionType,
payload: state.data,
};

actionSubject.next(action);

expect(store.dispatch).toHaveBeenCalledWith(new LoadUsers());
});
});

const grantRoleTypes = [
{ roleId: 'admin', roleValue: 'time-tracker-admin' },
{ roleId: 'test', roleValue: 'time-tracker-tester' },
Expand All @@ -111,6 +160,32 @@ describe('UsersListComponent', () => {
});
});

const AddGroupTypes = [
{ groupName: 'time-tracker-admin' },
{ groupName: 'time-tracker-tester' }
];

AddGroupTypes.map((param) => {
it(`When user switchGroup to ${param.groupName} and doesn't belong to any group, should add ${param.groupName} group to user`, () => {
const groupName = param.groupName;
const user = {
name: 'name',
email: 'email',
roles: [],
groups: [],
id: 'id',
tenant_id: 'tenant id',
deleted: 'delete',
} ;

spyOn(store, 'dispatch');

component.switchGroup(groupName, user);

expect(store.dispatch).toHaveBeenCalledWith(new AddUserToGroup(user.id, groupName));
});
});

const revokeRoleTypes = [
{ roleId: 'admin', roleValue: 'time-tracker-admin', userRoles: ['time-tracker-admin'] },
{ roleId: 'test', roleValue: 'time-tracker-tester', userRoles: ['time-tracker-tester'] },
Expand All @@ -131,6 +206,33 @@ describe('UsersListComponent', () => {
});
});

const removeGroupTypes = [
{ groupName: 'time-tracker-admin', userGroups: ['time-tracker-admin'] },
{ groupName: 'time-tracker-tester', userGroups: ['time-tracker-tester'] },
];

removeGroupTypes.map((param) => {
it(`When user switchGroup to ${param.groupName} and belongs to group, should remove ${param.groupName} group from user`, () => {
const groupName = param.groupName;
const user = {
name: 'name',
email: 'email',
roles: [],
groups: param.userGroups,
id: 'id',
tenant_id: 'tenant id',
deleted: 'delete',
} ;


spyOn(store, 'dispatch');

component.switchGroup(groupName, user);

expect(store.dispatch).toHaveBeenCalledWith(new RemoveUserFromGroup(user.id, groupName));
});
});

it('on success load users, the data of roles should be an array', () => {
const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
const action = {
Expand All @@ -145,6 +247,20 @@ describe('UsersListComponent', () => {
});
});

it('on success load users, the data of groups should be an array', () => {
const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
const action = {
type: UserActionTypes.LOAD_USERS_SUCCESS,
payload: state.data,
};

actionSubject.next(action);

component.users.map((user) => {
expect(user.groups).toEqual(['time-tracker-admin', 'time-tracker-tester']);
});
});

it('on success load users, the datatable should be reloaded', async () => {
const actionSubject = TestBed.inject(ActionsSubject);
const action = {
Expand All @@ -158,6 +274,27 @@ describe('UsersListComponent', () => {
expect(component.dtElement.dtInstance.then).toHaveBeenCalled();
});

it('When Component is created, should call the feature toggle method', () => {
spyOn(component, 'isFeatureToggleActivated').and.returnValue(of(true));

component.ngOnInit();

expect(component.isFeatureToggleActivated).toHaveBeenCalled();
expect(component.isUserGroupsToggleOn).toBe(true);
});

const toggleValues = [true, false];
toggleValues.map((toggleValue) => {
it(`when FeatureToggle is ${toggleValue} should return ${toggleValue}`, () => {
spyOn(service, 'isToggleEnabledForUser').and.returnValue(of(toggleValue));

const isFeatureToggleActivated: Observable<boolean> = component.isFeatureToggleActivated();

expect(service.isToggleEnabledForUser).toHaveBeenCalled();
isFeatureToggleActivated.subscribe((value) => expect(value).toEqual(toggleValue));
});
});

afterEach(() => {
component.dtTrigger.unsubscribe();
component.loadUsersSubscription.unsubscribe();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActionsSubject, select, Store } from '@ngrx/store';
import { ActionsSubject, select, Store, Action } 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 { FeatureManagerService } from 'src/app/modules/shared/feature-toggles/feature-toggle-manager.service';
import { User } from '../../models/users';
import { GrantRoleUser, LoadUsers, RevokeRoleUser, UserActionTypes } from '../../store/user.actions';
import {
GrantRoleUser,
LoadUsers,
RevokeRoleUser,
UserActionTypes,
AddUserToGroup,
RemoveUserFromGroup,
} from '../../store/user.actions';
import { getIsLoading } from '../../store/user.selectors';

@Component({
Expand All @@ -21,8 +29,15 @@ export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit {
@ViewChild(DataTableDirective, { static: false })
dtElement: DataTableDirective;
dtOptions: any = {};
switchGroupsSubscription: Subscription;
isEnableToggleSubscription: Subscription;
isUserGroupsToggleOn: boolean;

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));
}

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

this.isEnableToggleSubscription = this.isFeatureToggleActivated().subscribe((flag) => {
this.isUserGroupsToggleOn = flag;
});

this.switchGroupsSubscription = this.filterUserGroup().subscribe((action) => {
this.store.dispatch(new LoadUsers());
});

this.switchRoleSubscription = this.actionsSubject$
.pipe(
filter(
Expand All @@ -55,6 +78,7 @@ export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit {
ngOnDestroy() {
this.loadUsersSubscription.unsubscribe();
this.dtTrigger.unsubscribe();
this.isEnableToggleSubscription.unsubscribe();
}

private rerenderDataTable(): void {
Expand All @@ -73,4 +97,27 @@ export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit {
? this.store.dispatch(new RevokeRoleUser(userId, roleId))
: this.store.dispatch(new GrantRoleUser(userId, roleId));
}

switchGroup(groupName: string, user: User): void {
this.store.dispatch(
user.groups.includes(groupName)
? new RemoveUserFromGroup(user.id, groupName)
: new AddUserToGroup(user.id, groupName)
);
}

isFeatureToggleActivated(): Observable<boolean> {
return this.featureManagerService.isToggleEnabledForUser('switch-group')
.pipe(map((enabled: boolean) => enabled));
}

filterUserGroup(): Observable<Action> {
return this.actionsSubject$.pipe(
filter(
(action: Action) =>
action.type === UserActionTypes.ADD_USER_TO_GROUP_SUCCESS ||
action.type === UserActionTypes.REMOVE_USER_FROM_GROUP_SUCCESS
)
);
}
}