RED-3624: Enable/disable features via config file
This commit is contained in:
parent
f0d3d4a295
commit
c189c81427
@ -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],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
) {}
|
||||
|
||||
|
||||
19
apps/red-ui/src/app/guards/features-guard.service.ts
Normal file
19
apps/red-ui/src/app/guards/features-guard.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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]) => {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
40
apps/red-ui/src/app/services/features.service.ts
Normal file
40
apps/red-ui/src/app/services/features.service.ts
Normal 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);
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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())),
|
||||
|
||||
@ -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';
|
||||
|
||||
8
apps/red-ui/src/assets/config/features.json
Normal file
8
apps/red-ui/src/assets/config/features.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"features": [
|
||||
{
|
||||
"name": "DOSSIERS_ARCHIVE",
|
||||
"minVersion": "3.3.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user