From 1d2e800c48e26b8a6e4a12900a25a697ef38e58a Mon Sep 17 00:00:00 2001 From: Juan Gabriel Guzman Date: Mon, 21 Sep 2020 09:35:40 -0500 Subject: [PATCH 1/3] feat: #60 Adding support for ring deployments --- package-lock.json | 4 +- src/app/app.component.html | 2 +- .../services/azure.ad.b2c.service.spec.ts | 21 ++++- .../login/services/azure.ad.b2c.service.ts | 13 ++- .../reports/pages/reports.component.ts | 10 +- .../feature-toggle-configuration.ts | 10 ++ .../feature-toggle-manager.service.spec.ts | 94 +++++++++++++++++++ .../feature-toggle-manager.service.ts | 27 ++++++ .../feature-toggle-provider.service.spec.ts | 80 ++++++++++++++++ .../feature-toggle-provider.service.ts | 40 ++++++++ .../feature-toggles/feature-toggle.model.ts | 9 ++ .../filters/feature-filter-configuration.ts | 4 + .../feature-filter-provider.service.spec.ts | 26 +++++ .../feature-filter-provider.service.ts | 33 +++++++ .../filters/feature-filter-types.ts | 3 + .../filters/feature-filter.model.ts | 14 +++ .../targeting-feature-filter-parameters.ts | 9 ++ .../targeting-feature-filter.model.spec.ts | 38 ++++++++ .../targeting-feature-filter.model.ts | 19 ++++ .../targeting/targeting-filter-app-context.ts | 9 ++ 20 files changed, 450 insertions(+), 15 deletions(-) create mode 100644 src/app/modules/shared/feature-toggles/feature-toggle-configuration.ts create mode 100644 src/app/modules/shared/feature-toggles/feature-toggle-manager.service.spec.ts create mode 100644 src/app/modules/shared/feature-toggles/feature-toggle-manager.service.ts create mode 100644 src/app/modules/shared/feature-toggles/feature-toggle-provider.service.spec.ts create mode 100644 src/app/modules/shared/feature-toggles/feature-toggle-provider.service.ts create mode 100644 src/app/modules/shared/feature-toggles/feature-toggle.model.ts create mode 100644 src/app/modules/shared/feature-toggles/filters/feature-filter-configuration.ts create mode 100644 src/app/modules/shared/feature-toggles/filters/feature-filter-provider.service.spec.ts create mode 100644 src/app/modules/shared/feature-toggles/filters/feature-filter-provider.service.ts create mode 100644 src/app/modules/shared/feature-toggles/filters/feature-filter-types.ts create mode 100644 src/app/modules/shared/feature-toggles/filters/feature-filter.model.ts create mode 100644 src/app/modules/shared/feature-toggles/filters/targeting/targeting-feature-filter-parameters.ts create mode 100644 src/app/modules/shared/feature-toggles/filters/targeting/targeting-feature-filter.model.spec.ts create mode 100644 src/app/modules/shared/feature-toggles/filters/targeting/targeting-feature-filter.model.ts create mode 100644 src/app/modules/shared/feature-toggles/filters/targeting/targeting-filter-app-context.ts diff --git a/package-lock.json b/package-lock.json index c8ea09871..0df7fad84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13227,7 +13227,7 @@ }, "npm-user-validate": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/npm-user-validate/-/npm-user-validate-1.0.0.tgz", + "resolved": false, "integrity": "sha1-jOyg9c6gTU6TUZ73LQVXp1Ei6VE=", "dev": true }, @@ -20749,7 +20749,7 @@ "find-cache-dir": "^2.1.0", "is-wsl": "^1.1.0", "schema-utils": "^1.0.0", - "serialize-javascript": "^4.0.0", + "serialize-javascript": "^3.1.0", "source-map": "^0.6.1", "terser": "^4.1.2", "webpack-sources": "^1.4.0", diff --git a/src/app/app.component.html b/src/app/app.component.html index 90c6b6463..0680b43f9 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/app/modules/login/services/azure.ad.b2c.service.spec.ts b/src/app/modules/login/services/azure.ad.b2c.service.spec.ts index b395d3bdd..97c7f35c9 100644 --- a/src/app/modules/login/services/azure.ad.b2c.service.spec.ts +++ b/src/app/modules/login/services/azure.ad.b2c.service.spec.ts @@ -1,6 +1,7 @@ -import { TestBed, inject } from '@angular/core/testing'; +import { inject, TestBed } from '@angular/core/testing'; +import { Account, UserAgentApplication } from 'msal'; import { AzureAdB2CService } from './azure.ad.b2c.service'; -import { UserAgentApplication, Account } from 'msal'; + describe('AzureAdB2CService', () => { let service: AzureAdB2CService; @@ -130,4 +131,20 @@ describe('AzureAdB2CService', () => { expect(sessionStorage.getItem).toHaveBeenCalled(); expect(resp).toEqual(token); }); + + it('should get email from UserAgentApplication', () => { + spyOn(UserAgentApplication.prototype, 'getAccount').and.returnValues(account); + + const name = service.getName(); + + expect(UserAgentApplication.prototype.getAccount).toHaveBeenCalled(); + }); + + it('should group from UserAgentApplication', () => { + spyOn(UserAgentApplication.prototype, 'getAccount').and.returnValues(account); + + const name = service.getUserGroup(); + + expect(UserAgentApplication.prototype.getAccount).toHaveBeenCalled(); + }); }); diff --git a/src/app/modules/login/services/azure.ad.b2c.service.ts b/src/app/modules/login/services/azure.ad.b2c.service.ts index 2a0239fd6..b6945f9de 100644 --- a/src/app/modules/login/services/azure.ad.b2c.service.ts +++ b/src/app/modules/login/services/azure.ad.b2c.service.ts @@ -1,7 +1,8 @@ import { Injectable } from '@angular/core'; -import { Observable, from } from 'rxjs'; -import { CLIENT_ID, AUTHORITY, SCOPES } from '../../../../environments/environment'; import { UserAgentApplication } from 'msal'; +import { from, Observable } from 'rxjs'; + +import { AUTHORITY, CLIENT_ID, SCOPES } from '../../../../environments/environment'; @Injectable({ providedIn: 'root', @@ -55,4 +56,12 @@ export class AzureAdB2CService { getBearerToken(): string { return sessionStorage.getItem('msal.idtoken'); } + + getUserEmail(): string { + return this.msal.getAccount().idToken?.emails[0]; + } + + getUserGroup(): string { + return this.msal.getAccount().idToken?.extension_role; + } } diff --git a/src/app/modules/reports/pages/reports.component.ts b/src/app/modules/reports/pages/reports.component.ts index bb276a460..dbdc3e14c 100644 --- a/src/app/modules/reports/pages/reports.component.ts +++ b/src/app/modules/reports/pages/reports.component.ts @@ -1,15 +1,9 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; @Component({ selector: 'app-reports', templateUrl: './reports.component.html', styleUrls: ['./reports.component.scss'] }) -export class ReportsComponent implements OnInit { - - constructor() { } - - ngOnInit(): void { - } - +export class ReportsComponent { } diff --git a/src/app/modules/shared/feature-toggles/feature-toggle-configuration.ts b/src/app/modules/shared/feature-toggles/feature-toggle-configuration.ts new file mode 100644 index 000000000..865a833b8 --- /dev/null +++ b/src/app/modules/shared/feature-toggles/feature-toggle-configuration.ts @@ -0,0 +1,10 @@ +import { FeatureFilterConfiguration } from './filters/feature-filter-configuration'; + +export interface FeatureToggleConfiguration { + id: string; + enabled: boolean; + description: string; + conditions: { + client_filters: FeatureFilterConfiguration[]; + }; +} diff --git a/src/app/modules/shared/feature-toggles/feature-toggle-manager.service.spec.ts b/src/app/modules/shared/feature-toggles/feature-toggle-manager.service.spec.ts new file mode 100644 index 000000000..471d10f62 --- /dev/null +++ b/src/app/modules/shared/feature-toggles/feature-toggle-manager.service.spec.ts @@ -0,0 +1,94 @@ +import { AppConfigurationClient } from '@azure/app-configuration'; +import { of } from 'rxjs'; +import { AzureAdB2CService } from '../../login/services/azure.ad.b2c.service'; +import { FeatureManagerService } from './feature-toggle-manager.service'; +import { FeatureToggleProvider } from './feature-toggle-provider.service'; +import { FeatureToggleModel } from './feature-toggle.model'; +import { FeatureFilterProvider } from './filters/feature-filter-provider.service'; +import { TargetingFeatureFilterModel } from './filters/targeting/targeting-feature-filter.model'; + + +describe('FeatureToggleManager', () => { + const featureToggleKey = 'foo'; + const featureToggleLabel = 'dev'; + const fakeAppConfigurationConnectionString = 'Endpoint=http://fake.foo;Id=fake.id;Secret=fake.secret'; + const aFeatureToggle = new FeatureToggleModel('any-id', true, []); + let service: FeatureManagerService; + let fakeFeatureToggleProvider; + + describe('Features without filters', () => { + beforeEach(() => { + + fakeFeatureToggleProvider = new FeatureToggleProvider( + new AppConfigurationClient(fakeAppConfigurationConnectionString), + new FeatureFilterProvider(new AzureAdB2CService()) + ); + spyOn(fakeFeatureToggleProvider, 'getFeatureToggle').and.returnValue(of(aFeatureToggle)); + service = new FeatureManagerService(fakeFeatureToggleProvider); + }); + it('manager uses feature provider to build feature toggle model', async () => { + service.isToggleEnabled(featureToggleKey, featureToggleLabel).subscribe((value) => { + + expect(fakeFeatureToggleProvider).toHaveBeenCalledWith(featureToggleKey, featureToggleLabel); + }); + }); + + it('manager extracts enabled attribute from feature toggle model', async () => { + service.isToggleEnabled(featureToggleKey, featureToggleLabel).subscribe((value) => { + expect(value).toEqual(aFeatureToggle.enabled); + }); + }); + }); + + + describe('Features with filters', () => { + const anyMatchingFilter = new TargetingFeatureFilterModel( + { Audience: { Groups: ['group-a'], Users: ['user-a'] } }, + { username: 'user-b', group: 'group-a' } + ); + const anyNotMatchingFilter = new TargetingFeatureFilterModel( + { Audience: { Groups: ['a-group'], Users: ['user-a'] } }, + { username: 'user-b', group: 'b-group' } + ); + + let aToggleWithFilters; + let getFeatureToggleSpy; + + beforeEach(() => { + aToggleWithFilters = new FeatureToggleModel('any-other-id', true, [anyMatchingFilter]); + fakeFeatureToggleProvider = new FeatureToggleProvider( + new AppConfigurationClient(fakeAppConfigurationConnectionString), + new FeatureFilterProvider(new AzureAdB2CService()) + ); + getFeatureToggleSpy = spyOn(fakeFeatureToggleProvider, 'getFeatureToggle').and.returnValue(of(aToggleWithFilters)); + service = new FeatureManagerService(fakeFeatureToggleProvider); + }); + + it('manager uses feature provider to build feature toggle model', async () => { + service.isToggleEnabledForUser(featureToggleKey, featureToggleLabel).subscribe((value) => { + expect(getFeatureToggleSpy).toHaveBeenCalledWith(featureToggleKey, featureToggleLabel); + }); + }); + + it('given a feature toggle with filters which match the verification, then the response is true', async () => { + service.isToggleEnabledForUser(featureToggleKey, featureToggleLabel).subscribe((value) => { + expect(value).toEqual(true); + }); + }); + + it('given a feature toggle with filters which do not match the verification, then the response is false', async () => { + + aToggleWithFilters = new FeatureToggleModel('any-other-id', true, [anyNotMatchingFilter]); + fakeFeatureToggleProvider = new FeatureToggleProvider( + new AppConfigurationClient(fakeAppConfigurationConnectionString), + new FeatureFilterProvider(new AzureAdB2CService()) + ); + spyOn(fakeFeatureToggleProvider, 'getFeatureToggle').and.returnValue(of(aToggleWithFilters)); + service = new FeatureManagerService(fakeFeatureToggleProvider); + + service.isToggleEnabledForUser(featureToggleKey, featureToggleLabel).subscribe((value) => { + expect(value).toEqual(false); + }); + }); + }); +}); diff --git a/src/app/modules/shared/feature-toggles/feature-toggle-manager.service.ts b/src/app/modules/shared/feature-toggles/feature-toggle-manager.service.ts new file mode 100644 index 000000000..263b40e6c --- /dev/null +++ b/src/app/modules/shared/feature-toggles/feature-toggle-manager.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { FeatureToggleProvider } from './feature-toggle-provider.service'; + + +@Injectable({ + providedIn: 'root', +}) +export class FeatureManagerService { + + constructor(private featureToggleProvider: FeatureToggleProvider) { } + + public isToggleEnabled(toggleName: string, toggleLabel?: string): Observable { + return this.featureToggleProvider.getFeatureToggle(toggleName, toggleLabel).pipe( + map(featureToggle => featureToggle.enabled) + ); + } + + public isToggleEnabledForUser(toggleName: string, toggleLabel?: string): Observable { + return this.featureToggleProvider.getFeatureToggle(toggleName, toggleLabel).pipe( + map(featureToggle => featureToggle.filters), + map(filters => filters.map(filter => filter.evaluate())), + map(filterEvaluations => filterEvaluations.includes(true)) + ); + } +} diff --git a/src/app/modules/shared/feature-toggles/feature-toggle-provider.service.spec.ts b/src/app/modules/shared/feature-toggles/feature-toggle-provider.service.spec.ts new file mode 100644 index 000000000..5c6d9b5e4 --- /dev/null +++ b/src/app/modules/shared/feature-toggles/feature-toggle-provider.service.spec.ts @@ -0,0 +1,80 @@ +import { AppConfigurationClient, GetConfigurationSettingResponse } from '@azure/app-configuration'; +import { of } from 'rxjs'; +import { AzureAdB2CService } from '../../login/services/azure.ad.b2c.service'; +import { FeatureToggleConfiguration } from './feature-toggle-configuration'; +import { FeatureToggleProvider } from './feature-toggle-provider.service'; +import { FeatureToggleModel } from './feature-toggle.model'; +import { FeatureFilterProvider } from './filters/feature-filter-provider.service'; +import { TargetingFeatureFilterModel } from './filters/targeting/targeting-feature-filter.model'; + + +describe('FeatureToggleProvider', () => { + const anyToggleResponse: FeatureToggleConfiguration = { + id: '1', + enabled: true, + description: 'any description', + conditions: { + client_filters: [{ name: 'any-name', parameters: {} }] + }, + }; + + const fakeAppConfigurationConnectionString = 'Endpoint=http://fake.foo;Id=fake.id;Secret=fake.secret'; + const featureToggleKey = 'foo'; + const featureToggleLabel = 'dev'; + const fakeResponse: GetConfigurationSettingResponse = { + isReadOnly: true, + key: featureToggleKey, + _response: { + request: null, + bodyAsText: 'any-response', + headers: null, + parsedHeaders: null, + status: 200 + }, + statusCode: 200, + value: JSON.stringify(anyToggleResponse) + }; + const anyFilter = new TargetingFeatureFilterModel( + { Audience: { Groups: ['a-group'], Users: ['any-user'] } }, + { username: 'fakeuser@ioet.com', group: 'fake-group' } + ); + let fakeConfigurationClient; + let fakeGetConfigurationSetting; + let fakeFeatureFilterProvider; + let getFilterConfigurationSpy; + let service; + + + + beforeEach(() => { + fakeConfigurationClient = new AppConfigurationClient(fakeAppConfigurationConnectionString); + fakeGetConfigurationSetting = spyOn(fakeConfigurationClient, 'getConfigurationSetting').and.callFake( + () => of(fakeResponse).toPromise()); + + fakeFeatureFilterProvider = new FeatureFilterProvider(new AzureAdB2CService()); + getFilterConfigurationSpy = spyOn(fakeFeatureFilterProvider, 'getFilterFromConfiguration').and. + returnValue(anyFilter); + service = new FeatureToggleProvider(fakeConfigurationClient, fakeFeatureFilterProvider); + }); + + it('toggles are read using azure configuration client', async () => { + service.getFeatureToggle(featureToggleKey, featureToggleLabel).subscribe((value) => { + + expect(fakeGetConfigurationSetting).toHaveBeenCalledWith( + { key: `.appconfig.featureflag/${featureToggleKey}`, label: featureToggleLabel } + ); + }); + }); + + it('filters are built using the filterProvider', async () => { + service.getFeatureToggle(featureToggleKey, featureToggleLabel).subscribe((value) => { + expect(getFilterConfigurationSpy).toHaveBeenCalled(); + }); + }); + + it('toggle model is built', async () => { + service.getFeatureToggle(featureToggleKey, featureToggleLabel).subscribe((value) => { + expect(value).toEqual(new FeatureToggleModel(anyToggleResponse.id, anyToggleResponse.enabled, [anyFilter])); + }); + }); +}); diff --git a/src/app/modules/shared/feature-toggles/feature-toggle-provider.service.ts b/src/app/modules/shared/feature-toggles/feature-toggle-provider.service.ts new file mode 100644 index 000000000..953cbfe1b --- /dev/null +++ b/src/app/modules/shared/feature-toggles/feature-toggle-provider.service.ts @@ -0,0 +1,40 @@ +import { Inject, Injectable, InjectionToken } from '@angular/core'; +import { AppConfigurationClient } from '@azure/app-configuration'; +import { from, Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { AZURE_APP_CONFIGURATION_CONNECTION_STRING } from 'src/environments/environment'; +import { FeatureToggleConfiguration } from './feature-toggle-configuration'; +import { FeatureToggleModel } from './feature-toggle.model'; +import { FeatureFilterProvider } from './filters/feature-filter-provider.service'; + + +const APP_CONFIGURATION_CLIENT = new InjectionToken('Azure configuration client', { + providedIn: 'root', + factory: () => new AppConfigurationClient(AZURE_APP_CONFIGURATION_CONNECTION_STRING) +}); +@Injectable({ + providedIn: 'root', + +}) +export class FeatureToggleProvider { + constructor( + @Inject(APP_CONFIGURATION_CLIENT) + private client: AppConfigurationClient, + private featureFilterProvider: FeatureFilterProvider + ) { } + + public getFeatureToggle(toggleName: string, toggleLabel?: string): Observable { + return from(this.client.getConfigurationSetting({ key: `.appconfig.featureflag/${toggleName}`, label: toggleLabel })).pipe( + map(featureToggleResponse => JSON.parse(featureToggleResponse.value) as FeatureToggleConfiguration), + map(featureToggleConfiguration => { + const filters = featureToggleConfiguration.conditions.client_filters.map(filterConfiguration => + this.featureFilterProvider.getFilterFromConfiguration(filterConfiguration)); + return new FeatureToggleModel( + featureToggleConfiguration.id, + featureToggleConfiguration.enabled, + filters); + } + ) + ); + } +} diff --git a/src/app/modules/shared/feature-toggles/feature-toggle.model.ts b/src/app/modules/shared/feature-toggles/feature-toggle.model.ts new file mode 100644 index 000000000..a33a74ff3 --- /dev/null +++ b/src/app/modules/shared/feature-toggles/feature-toggle.model.ts @@ -0,0 +1,9 @@ +import { FeatureFilterModel } from './filters/feature-filter.model'; + +export class FeatureToggleModel { + constructor( + public readonly name: string, + public readonly enabled: boolean, + public readonly filters: FeatureFilterModel[] + ) { } +} diff --git a/src/app/modules/shared/feature-toggles/filters/feature-filter-configuration.ts b/src/app/modules/shared/feature-toggles/filters/feature-filter-configuration.ts new file mode 100644 index 000000000..1326c138f --- /dev/null +++ b/src/app/modules/shared/feature-toggles/filters/feature-filter-configuration.ts @@ -0,0 +1,4 @@ +export interface FeatureFilterConfiguration { + name: string; + parameters: any; +} diff --git a/src/app/modules/shared/feature-toggles/filters/feature-filter-provider.service.spec.ts b/src/app/modules/shared/feature-toggles/filters/feature-filter-provider.service.spec.ts new file mode 100644 index 000000000..fb1e21d4f --- /dev/null +++ b/src/app/modules/shared/feature-toggles/filters/feature-filter-provider.service.spec.ts @@ -0,0 +1,26 @@ +import { AzureAdB2CService } from 'src/app/modules/login/services/azure.ad.b2c.service'; +import { FeatureFilterConfiguration } from './feature-filter-configuration'; +import { FeatureFilterProvider } from './feature-filter-provider.service'; +import { FeatureFilterTypes } from './feature-filter-types'; +import { TargetingFeatureFilterModel } from './targeting/targeting-feature-filter.model'; + + +describe('FeatureFilterProvider', () => { + let fakeUserService: AzureAdB2CService; + let service: FeatureFilterProvider; + let featureFilterConfiguration: FeatureFilterConfiguration; + + beforeEach(() => { + fakeUserService = new AzureAdB2CService(); + spyOn(fakeUserService, 'getUserEmail').and.returnValue('any-user-email'); + spyOn(fakeUserService, 'getUserGroup').and.returnValue('any-user-group'); + service = new FeatureFilterProvider(fakeUserService); + featureFilterConfiguration = { name: FeatureFilterTypes.TARGETING, parameters: {} }; + }); + + it('filter model type is created based on the filter configuration', () => { + const filter = service.getFilterFromConfiguration(featureFilterConfiguration); + + expect(filter.constructor.name).toBe(TargetingFeatureFilterModel.name); + }); +}); diff --git a/src/app/modules/shared/feature-toggles/filters/feature-filter-provider.service.ts b/src/app/modules/shared/feature-toggles/filters/feature-filter-provider.service.ts new file mode 100644 index 000000000..9b4f57bf6 --- /dev/null +++ b/src/app/modules/shared/feature-toggles/filters/feature-filter-provider.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { AzureAdB2CService } from 'src/app/modules/login/services/azure.ad.b2c.service'; +import { FeatureFilterConfiguration } from './feature-filter-configuration'; +import { FeatureFilterTypes } from './feature-filter-types'; +import { FeatureFilterModel } from './feature-filter.model'; +import { TargetingFilterParameters } from './targeting/targeting-feature-filter-parameters'; +import { TargetingFeatureFilterModel } from './targeting/targeting-feature-filter.model'; + + +@Injectable({ + providedIn: 'root', +}) +export class FeatureFilterProvider { + + constructor(private userService: AzureAdB2CService) { } + + getFilterFromConfiguration(featureFilterConfiguration: FeatureFilterConfiguration): FeatureFilterModel { + const featureName = featureFilterConfiguration.name; + switch (featureName) { + case FeatureFilterTypes.TARGETING: { + const appContext = { + username: this.userService.getUserEmail(), + group: this.userService.getUserGroup() + }; + const filter = new TargetingFeatureFilterModel(featureFilterConfiguration.parameters as TargetingFilterParameters, appContext); + return filter; + } + default: { + break; + } + } + } +} diff --git a/src/app/modules/shared/feature-toggles/filters/feature-filter-types.ts b/src/app/modules/shared/feature-toggles/filters/feature-filter-types.ts new file mode 100644 index 000000000..517c617b5 --- /dev/null +++ b/src/app/modules/shared/feature-toggles/filters/feature-filter-types.ts @@ -0,0 +1,3 @@ +export enum FeatureFilterTypes { + TARGETING = 'Microsoft.Targeting' +} diff --git a/src/app/modules/shared/feature-toggles/filters/feature-filter.model.ts b/src/app/modules/shared/feature-toggles/filters/feature-filter.model.ts new file mode 100644 index 000000000..3a0037c89 --- /dev/null +++ b/src/app/modules/shared/feature-toggles/filters/feature-filter.model.ts @@ -0,0 +1,14 @@ + +export interface FeatureFilterModel { + name: string; + parameters: FeatureFilterParameters; + appContext: FeatureFilterAppContext; + evaluate(): boolean; + +} + +// tslint:disable-next-line:no-empty-interface +export interface FeatureFilterParameters { } +// tslint:disable-next-line:no-empty-interface +export interface FeatureFilterAppContext { } + diff --git a/src/app/modules/shared/feature-toggles/filters/targeting/targeting-feature-filter-parameters.ts b/src/app/modules/shared/feature-toggles/filters/targeting/targeting-feature-filter-parameters.ts new file mode 100644 index 000000000..50f624ed1 --- /dev/null +++ b/src/app/modules/shared/feature-toggles/filters/targeting/targeting-feature-filter-parameters.ts @@ -0,0 +1,9 @@ +import { FeatureFilterParameters } from '../feature-filter.model'; + +export interface TargetingFilterParameters extends FeatureFilterParameters { + Audience: { + Users: string[]; + Groups: string[]; + }; +} + diff --git a/src/app/modules/shared/feature-toggles/filters/targeting/targeting-feature-filter.model.spec.ts b/src/app/modules/shared/feature-toggles/filters/targeting/targeting-feature-filter.model.spec.ts new file mode 100644 index 000000000..bba414199 --- /dev/null +++ b/src/app/modules/shared/feature-toggles/filters/targeting/targeting-feature-filter.model.spec.ts @@ -0,0 +1,38 @@ +import { TargetingFilterParameters } from './targeting-feature-filter-parameters'; +import { TargetingFeatureFilterModel } from './targeting-feature-filter.model'; +import { TargetingFilterAppContext } from './targeting-filter-app-context'; + +describe('TargetingFeatureFilterModel', () => { + + it('targeting feature is true when username matches', () => { + const aUsername = 'user-a'; + const aFilterConfiguration: TargetingFilterParameters = { Audience: { Groups: ['group-x'], Users: [aUsername] } }; + const appContext: TargetingFilterAppContext = { group: 'group-y', username: aUsername }; + const targetingFeatureFilter: TargetingFeatureFilterModel = new TargetingFeatureFilterModel(aFilterConfiguration, appContext); + + const filterEvaluation = targetingFeatureFilter.evaluate(); + + expect(filterEvaluation).toEqual(true); + }); + + it('targeting feature is true when group matches', () => { + const aGroup = 'group-a'; + const aFilterConfiguration: TargetingFilterParameters = { Audience: { Groups: [aGroup], Users: ['user-a'] } }; + const appContext: TargetingFilterAppContext = { group: aGroup, username: 'user-b' }; + const targetingFeatureFilter: TargetingFeatureFilterModel = new TargetingFeatureFilterModel(aFilterConfiguration, appContext); + + const filterEvaluation = targetingFeatureFilter.evaluate(); + + expect(filterEvaluation).toEqual(true); + }); + + it('targeting feature is false when neither group nor username match ', () => { + const aFilterConfiguration: TargetingFilterParameters = { Audience: { Groups: ['group-a'], Users: ['user-a'] } }; + const appContext: TargetingFilterAppContext = { group: 'group-b', username: 'user-b' }; + const targetingFeatureFilter: TargetingFeatureFilterModel = new TargetingFeatureFilterModel(aFilterConfiguration, appContext); + + const filterEvaluation = targetingFeatureFilter.evaluate(); + + expect(filterEvaluation).toEqual(false); + }); +}); diff --git a/src/app/modules/shared/feature-toggles/filters/targeting/targeting-feature-filter.model.ts b/src/app/modules/shared/feature-toggles/filters/targeting/targeting-feature-filter.model.ts new file mode 100644 index 000000000..9f5ed5faf --- /dev/null +++ b/src/app/modules/shared/feature-toggles/filters/targeting/targeting-feature-filter.model.ts @@ -0,0 +1,19 @@ +import { FeatureFilterTypes } from '../feature-filter-types'; +import { FeatureFilterModel } from '../feature-filter.model'; +import { TargetingFilterParameters } from './targeting-feature-filter-parameters'; +import { TargetingFilterAppContext } from './targeting-filter-app-context'; + +export class TargetingFeatureFilterModel implements FeatureFilterModel { + + name = FeatureFilterTypes.TARGETING; + constructor(public readonly parameters: TargetingFilterParameters, public readonly appContext: TargetingFilterAppContext) { + } + + evaluate(): boolean { + const userCoincidence = this.parameters.Audience.Users.includes(this.appContext.username); + const groupCoincidence = this.parameters.Audience.Groups.includes(this.appContext.group); + return userCoincidence || groupCoincidence; + } +} + + diff --git a/src/app/modules/shared/feature-toggles/filters/targeting/targeting-filter-app-context.ts b/src/app/modules/shared/feature-toggles/filters/targeting/targeting-filter-app-context.ts new file mode 100644 index 000000000..05d66efb8 --- /dev/null +++ b/src/app/modules/shared/feature-toggles/filters/targeting/targeting-filter-app-context.ts @@ -0,0 +1,9 @@ +import { FeatureFilterAppContext } from '../feature-filter.model'; + +export interface TargetingFilterAppContext extends FeatureFilterAppContext { + username; + group; +} + + + From 0ac7344fc355b36c1193ab6f3de1aa06196e7658 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 5 Nov 2020 15:15:42 +0000 Subject: [PATCH 2/3] chore(release): 1.27.0 [skip ci]nn --- package-lock.json | 10 ++-------- package.json | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 62eff1ac7..54e2e0496 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "time-tracker", - "version": "1.26.1", + "version": "1.27.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -14801,12 +14801,6 @@ "path-key": "^2.0.0" } }, - "npm-user-validate": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-user-validate/-/npm-user-validate-1.0.1.tgz", - "integrity": "sha512-uQwcd/tY+h1jnEaze6cdX/LrhWhoBxfSknxentoqmIuStxUExxjWd3ULMLFPiFUrZKbOVMowH6Jq2FRWfmhcEw==", - "dev": true - }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -20762,7 +20756,7 @@ "find-cache-dir": "^2.1.0", "is-wsl": "^1.1.0", "schema-utils": "^1.0.0", - "serialize-javascript": "^3.1.0", + "serialize-javascript": "^4.0.0", "source-map": "^0.6.1", "terser": "^4.1.2", "webpack-sources": "^1.4.0", diff --git a/package.json b/package.json index 1d16ec61a..29337f0a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "time-tracker", - "version": "1.26.1", + "version": "1.27.0", "scripts": { "preinstall": "npx npm-force-resolutions", "ng": "ng", From 1cdc1ed272bc086256edcff358c5ae750c355f8c Mon Sep 17 00:00:00 2001 From: PaulRC-ioet Date: Thu, 5 Nov 2020 09:50:08 -0500 Subject: [PATCH 3/3] feat: #562 add IDs on the list --- .../components/activity-list/activity-list.component.html | 6 ++++-- .../components/create-customer/create-customer.html | 2 +- .../components/customer-list/customer-list.component.html | 6 ++++-- .../project-type-list/project-type-list.component.html | 6 ++++-- .../components/project-list/project-list.component.html | 6 ++++-- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/app/modules/activities-management/components/activity-list/activity-list.component.html b/src/app/modules/activities-management/components/activity-list/activity-list.component.html index a0788a575..a4582ed10 100644 --- a/src/app/modules/activities-management/components/activity-list/activity-list.component.html +++ b/src/app/modules/activities-management/components/activity-list/activity-list.component.html @@ -2,14 +2,16 @@ - + + - + + - + + - + + - + + - + +
ActivityActivity IDActivity
{{ activity.name }}{{ activity.id }}{{ activity.name }}
NameCustomer IDName Options
{{ customer.name }}{{ customer.id }}{{ customer.name }}
Project typeProject Type IDProject type
{{ projectType.name }}{{ projectType.id }}{{ projectType.name }}