RED-3624: Enable/disable features via config file

This commit is contained in:
Adina Țeudan 2022-03-15 18:42:07 +02:00
parent f0d3d4a295
commit c189c81427
12 changed files with 123 additions and 10 deletions

View File

@ -9,6 +9,8 @@ import { DownloadsListScreenComponent } from '@components/downloads-list-screen/
import { DossierTemplatesGuard } from '@guards/dossier-templates.guard';
import { DossiersGuard } from '@guards/dossiers.guard';
import { ACTIVE_DOSSIERS_SERVICE, ARCHIVED_DOSSIERS_SERVICE } from './tokens';
import { FeaturesGuard } from '@guards/features-guard.service';
import { DOSSIERS_ARCHIVE } from '@utils/constants';
const routes: Routes = [
{
@ -48,9 +50,10 @@ const routes: Routes = [
loadChildren: () => import('./modules/archive/archive.module').then(m => m.ArchiveModule),
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, DossierTemplatesGuard, DossiersGuard],
routeGuards: [FeaturesGuard, AuthGuard, RedRoleGuard, DossierTemplatesGuard, DossiersGuard],
requiredRoles: ['RED_USER', 'RED_MANAGER'],
dossiersService: ARCHIVED_DOSSIERS_SERVICE,
features: [DOSSIERS_ARCHIVE],
},
},
{

View File

@ -46,6 +46,7 @@ import { UserPreferenceService } from '@services/user-preference.service';
import { UserService } from '@services/user.service';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import { ArchivedDossiersService } from '@services/dossiers/archived-dossiers.service';
import { FeaturesService } from '@services/features.service';
export function httpLoaderFactory(httpClient: HttpClient, configService: ConfigService): PruningTranslationLoader {
return new PruningTranslationLoader(httpClient, '/assets/i18n/', `.json?version=${configService.values.FRONTEND_APP_VERSION}`);
@ -126,7 +127,16 @@ const components = [AppComponent, AuthErrorComponent, NotificationsComponent, Sp
provide: APP_INITIALIZER,
multi: true,
useFactory: configurationInitializer,
deps: [KeycloakService, Title, ConfigService, GeneralSettingsService, LanguageService, UserService, UserPreferenceService],
deps: [
KeycloakService,
Title,
ConfigService,
FeaturesService,
GeneralSettingsService,
LanguageService,
UserService,
UserPreferenceService,
],
},
{
provide: MissingTranslationHandler,

View File

@ -9,6 +9,8 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { filter, map, startWith } from 'rxjs/operators';
import { shareDistinctLast } from '@iqser/common-ui';
import { BreadcrumbsService } from '@services/breadcrumbs.service';
import { FeaturesService } from '@services/features.service';
import { DOSSIERS_ARCHIVE } from '@utils/constants';
interface MenuItem {
readonly name: string;
@ -59,6 +61,7 @@ export class BaseScreenComponent {
{
text: this._translateService.instant('search.active-dossiers'),
icon: 'red:enter',
hide: () => !this._featuresService.isEnabled(DOSSIERS_ARCHIVE),
action: (query): void => this._search(query, [], true),
},
{
@ -76,11 +79,12 @@ export class BaseScreenComponent {
readonly isSearchScreen$ = this._navigationStart$.pipe(map(isSearchScreen));
constructor(
private readonly _router: Router,
private readonly _translateService: TranslateService,
private readonly _featuresService: FeaturesService,
readonly userService: UserService,
readonly userPreferenceService: UserPreferenceService,
readonly titleService: Title,
private readonly _router: Router,
private readonly _translateService: TranslateService,
readonly breadcrumbsService: BreadcrumbsService,
) {}

View File

@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { FeaturesService } from '@services/features.service';
@Injectable({ providedIn: 'root' })
export class FeaturesGuard implements CanActivate {
constructor(private readonly _router: Router, private readonly _featuresService: FeaturesService) {}
async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
const features: string[] = route.data.features || [];
if (features.find(feature => !this._featuresService.isEnabled(feature))) {
await this._router.navigate(['']);
return false;
}
return true;
}
}

View File

@ -10,7 +10,7 @@ import {
SortingOrders,
TableColumnConfig,
} from '@iqser/common-ui';
import { combineLatest, Observable } from 'rxjs';
import { combineLatest, Observable, of } from 'rxjs';
import { debounceTime, map, startWith, switchMap, tap } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@ -22,6 +22,8 @@ import { FilesMapService } from '@services/entity-services/files-map.service';
import { PlatformSearchService } from '@services/entity-services/platform-search.service';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import { ArchivedDossiersService } from '@services/dossiers/archived-dossiers.service';
import { FeaturesService } from '@services/features.service';
import { DOSSIERS_ARCHIVE } from '@utils/constants';
@Component({
templateUrl: './search-screen.component.html',
@ -69,6 +71,7 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
private readonly _translateService: TranslateService,
private readonly _filesMapService: FilesMapService,
private readonly _platformSearchService: PlatformSearchService,
private readonly _featuresService: FeaturesService,
) {
super(_injector);
this.searchService.skip = true;
@ -90,7 +93,9 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
};
this.filterService.addFilterGroups([dossierNameFilter]);
const onlyActiveLabel = this._translateService.instant('search-screen.filters.only-active');
this.filterService.addSingleFilter({ id: 'onlyActiveDossiers', label: onlyActiveLabel, checked: this._routeOnlyActive });
if (this.#enabledArchive) {
this.filterService.addSingleFilter({ id: 'onlyActiveDossiers', label: onlyActiveLabel, checked: this._routeOnlyActive });
}
}
private get _routeDossierIds(): string[] {
@ -113,8 +118,14 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
);
}
get #enabledArchive(): boolean {
return this._featuresService.isEnabled(DOSSIERS_ARCHIVE);
}
private get _filtersChanged$(): Observable<[string[], boolean]> {
const onlyActiveDossiers$ = this.filterService.getSingleFilter('onlyActiveDossiers').pipe(map(f => !!f.checked));
const onlyActiveDossiers$ = this.#enabledArchive
? this.filterService.getSingleFilter('onlyActiveDossiers').pipe(map(f => !!f.checked))
: of(true);
const filterGroups$ = this.filterService.filterGroups$;
return combineLatest([filterGroups$, onlyActiveDossiers$]).pipe(
map(([groups, onlyActive]) => {

View File

@ -6,9 +6,10 @@ import { filter, pluck } from 'rxjs/operators';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { TranslateService } from '@ngx-translate/core';
import { BreadcrumbTypes } from '@red/domain';
import { DOSSIER_ID, FILE_ID } from '@utils/constants';
import { DOSSIER_ID, DOSSIERS_ARCHIVE, FILE_ID } from '@utils/constants';
import { DossiersService } from '@services/dossiers/dossiers.service';
import { dossiersServiceResolver } from '@services/entity-services/dossiers.service.provider';
import { FeaturesService } from '@services/features.service';
export type RouterLinkActiveOptions = { exact: boolean } | IsActiveMatchOptions;
export type BreadcrumbDisplayType = 'text' | 'dropdown';
@ -39,6 +40,7 @@ export class BreadcrumbsService {
private readonly _router: Router,
private readonly _translateService: TranslateService,
private readonly _filesMapService: FilesMapService,
private readonly _featuresService: FeaturesService,
) {
this.breadcrumbs$ = this._store$.asObservable();
@ -95,7 +97,7 @@ export class BreadcrumbsService {
const breadcrumbs = route.data.breadcrumbs || [];
if (breadcrumbs.length === 1) {
if (breadcrumbs.length === 1 && this._featuresService.isEnabled(DOSSIERS_ARCHIVE)) {
if (breadcrumbs[0] === BreadcrumbTypes.main) {
this._addMainDropdownBreadcrumb('active');
return;

View File

@ -6,6 +6,8 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { ActiveDossiersService } from './active-dossiers.service';
import { DossiersService } from './dossiers.service';
import { FilesMapService } from '../entity-services/files-map.service';
import { FeaturesService } from '@services/features.service';
import { DOSSIERS_ARCHIVE } from '@utils/constants';
@Injectable({ providedIn: 'root' })
export class ArchivedDossiersService extends DossiersService {
@ -13,6 +15,7 @@ export class ArchivedDossiersService extends DossiersService {
protected readonly _injector: Injector,
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _filesMapService: FilesMapService,
private readonly _featuresService: FeaturesService,
) {
super(_injector, 'archived-dossiers', 'archive');
}
@ -31,6 +34,10 @@ export class ArchivedDossiersService extends DossiersService {
);
}
loadAll(): Observable<Dossier[]> {
return this._featuresService.isEnabled(DOSSIERS_ARCHIVE) ? super.loadAll() : of([]);
}
#removeFromActiveDossiers(archivedDossiersIds: string[]): void {
const remainingEntities = this._activeDossiersService.all.filter(d => !archivedDossiersIds.includes(d.dossierId));
this._activeDossiersService.setEntities(remainingEntities);

View File

@ -0,0 +1,40 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
import { ConfigService } from './config.service';
interface Feature {
name: string;
minVersion: string;
}
function transform(version: string): number {
const [major, minor, patch, release] = version.split(/[.-]/).map(x => parseInt(x, 10));
return major * 1e7 + minor * 1e5 + patch * 1e3 + (release || 0);
}
@Injectable({
providedIn: 'root',
})
export class FeaturesService {
private _features = new Map<string, boolean>();
constructor(private readonly _httpClient: HttpClient, private readonly _configService: ConfigService) {}
isEnabled(feature: string): boolean {
// If feature is not defined in config object return true
return this._features.get(feature) !== false;
}
loadConfig(): Observable<any> {
return this._httpClient.get('/assets/config/features.json').pipe(
tap((config: { features: Feature[] }) => {
const BACKEND_APP_VERSION = transform(this._configService.values.BACKEND_APP_VERSION as string);
config.features.forEach(feature => {
this._features.set(feature.name, transform(feature.minVersion) <= BACKEND_APP_VERSION);
});
}),
);
}
}

View File

@ -5,6 +5,8 @@ import { DossiersService } from '@services/dossiers/dossiers.service';
import { ActivatedRoute } from '@angular/router';
import { dossiersServiceResolver } from '@services/entity-services/dossiers.service.provider';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { FeaturesService } from '@services/features.service';
import { DOSSIERS_ARCHIVE } from '@utils/constants';
@Injectable({ providedIn: 'root' })
export class PermissionsService {
@ -13,6 +15,7 @@ export class PermissionsService {
private readonly _route: ActivatedRoute,
private readonly _injector: Injector,
private readonly _filesMapService: FilesMapService,
private readonly _featuresService: FeaturesService,
) {}
private get _dossiersService(): DossiersService {
@ -185,7 +188,9 @@ export class PermissionsService {
}
canArchiveDossier(dossier: Dossier): boolean {
return dossier.isActive && dossier.ownerId === this._userService.currentUser.id;
return (
this._featuresService.isEnabled(DOSSIERS_ARCHIVE) && dossier.isActive && dossier.ownerId === this._userService.currentUser.id
);
}
canEditDossier(dossier: Dossier, user = this._userService.currentUser): boolean {

View File

@ -7,11 +7,13 @@ import { GeneralSettingsService } from '@services/general-settings.service';
import { LanguageService } from '@i18n/language.service';
import { UserPreferenceService } from '@services/user-preference.service';
import { UserService } from '@services/user.service';
import { FeaturesService } from '@services/features.service';
export function configurationInitializer(
keycloakService: KeycloakService,
title: Title,
configService: ConfigService,
featuresService: FeaturesService,
generalSettingsService: GeneralSettingsService,
languageService: LanguageService,
userService: UserService,
@ -21,6 +23,7 @@ export function configurationInitializer(
firstValueFrom(
keycloakService.keycloakEvents$.pipe(
filter(event => event.type === KeycloakEventType.OnReady),
switchMap(() => from(featuresService.loadConfig())),
switchMap(() => from(keycloakService.isLoggedIn())),
switchMap(loggedIn => (!loggedIn ? throwError('Not Logged In') : of({}))),
switchMap(() => from(userService.loadCurrentUser())),

View File

@ -4,3 +4,4 @@ export const DOSSIER_ID = 'dossierId';
export const FILE_ID = 'fileId';
export const DOSSIER_TEMPLATE_ID = 'dossierTemplateId';
export const DICTIONARY_TYPE = 'dictionary';
export const DOSSIERS_ARCHIVE = 'DOSSIERS_ARCHIVE';

View File

@ -0,0 +1,8 @@
{
"features": [
{
"name": "DOSSIERS_ARCHIVE",
"minVersion": "3.3.0"
}
]
}