From dd21bf88c72bff68e9b4d609b6b11faef8dcae42 Mon Sep 17 00:00:00 2001 From: Sumanth Chinthagunta Date: Sun, 29 Dec 2019 22:50:27 -0800 Subject: [PATCH] feat(config): adding AppConfig service to load remote overlay config and featureFlags --- .prettierignore | 5 +++ PLAYBOOK.md | 1 + TODO.md | 3 ++ apps/webapp/src/assets/data/ui-config.json | 8 ++++ apps/webapp/src/environments/base.ts | 13 ++++++ .../src/environments/environment.mock.ts | 11 ++++- .../src/environments/environment.prod.ts | 11 ++++- apps/webapp/src/environments/environment.ts | 10 ++++- apps/webapp/src/environments/ienvironment.ts | 20 +++++++++ libs/core/README.md | 18 ++++++++ libs/core/src/index.ts | 2 + libs/core/src/lib/core.module.ts | 28 ++++++++++-- .../lib/{handler => handlers}/auth.handler.ts | 0 .../custom-router-state.serializer.ts | 0 .../{handler => handlers}/route.handler.ts | 0 .../src/lib/interceptors/error.interceptor.ts | 45 +++++++++++++++++++ .../src/lib/services/app-config.service.ts | 32 +++++++++++++ 17 files changed, 201 insertions(+), 6 deletions(-) create mode 100644 TODO.md create mode 100644 apps/webapp/src/assets/data/ui-config.json create mode 100644 apps/webapp/src/environments/base.ts create mode 100644 apps/webapp/src/environments/ienvironment.ts rename libs/core/src/lib/{handler => handlers}/auth.handler.ts (100%) rename libs/core/src/lib/{handler => handlers}/custom-router-state.serializer.ts (100%) rename libs/core/src/lib/{handler => handlers}/route.handler.ts (100%) create mode 100644 libs/core/src/lib/interceptors/error.interceptor.ts create mode 100644 libs/core/src/lib/services/app-config.service.ts diff --git a/.prettierignore b/.prettierignore index d0b804da..75884493 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,9 @@ # Add files here to ignore them from prettier formatting +*.js +*.js.map +*.d.ts +*.md +*.min.* /dist /coverage diff --git a/PLAYBOOK.md b/PLAYBOOK.md index 019b417f..a6729da7 100644 --- a/PLAYBOOK.md +++ b/PLAYBOOK.md @@ -504,6 +504,7 @@ ng g directive directives/mask/mask --selector=ngxMask --project=ngx-utils --mo ng g lib blog --routing --lazy --parent-module=libs/home/src/lib/home.module.ts --tags=private-module --defaults -d ng g component containers/blogList --project=blog -d ng g component containers/blog --project=blog -d +ng g service services/highlight --project=blog --skip-tests -d # generate components for `toolbar` Module ng g lib toolbar --tags=private-module --defaults -d diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..488e8fcf --- /dev/null +++ b/TODO.md @@ -0,0 +1,3 @@ +# TODO + +1. [feature flags](https://www.bennadel.com/blog/3709-loading-and-using-remote-feature-flags-in-angular-9-0-0-next-12.htm) \ No newline at end of file diff --git a/apps/webapp/src/assets/data/ui-config.json b/apps/webapp/src/assets/data/ui-config.json new file mode 100644 index 00000000..6fdbb168 --- /dev/null +++ b/apps/webapp/src/assets/data/ui-config.json @@ -0,0 +1,8 @@ +{ + "DOCS_BASE_URL": "http://sumo.com/docs", + "featureFlags": { + "feature-a": true, + "feature-b": false, + "feature-c": true + } +} diff --git a/apps/webapp/src/environments/base.ts b/apps/webapp/src/environments/base.ts new file mode 100644 index 00000000..349a3e3a --- /dev/null +++ b/apps/webapp/src/environments/base.ts @@ -0,0 +1,13 @@ +import * as packageJson from '../../../../package.json'; + +const base = document.querySelector('base'); + +export default { + appName: 'YETI', + secret: 'SECRET', + apiToken: 'SECRET_TOKEN', + baseUrl: (base && base.href) || window.location.origin + '/', + versions: { + app: packageJson.version + } +}; diff --git a/apps/webapp/src/environments/environment.mock.ts b/apps/webapp/src/environments/environment.mock.ts index 6d6ac563..55846359 100644 --- a/apps/webapp/src/environments/environment.mock.ts +++ b/apps/webapp/src/environments/environment.mock.ts @@ -1,12 +1,21 @@ -export const environment = { +import { IEnvironment } from '@env/ienvironment'; +import sharedEnvironment from './base'; + +export const environment: IEnvironment = { + ...sharedEnvironment, production: true, envName: 'mock', + + REMOTE_CONFIG_URL: '/yeti/assets/data/ui-config.json', + API_BASE_URL: 'https://api.kashmora.com/api', + plugins: [ // HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService, { // passThruUnknownUrl: true, // delay: 1000 // }) ], + auth: { clientId: '791772336084-vkt37abstm1du92ofdmhgi30vgd7t0oa.apps.googleusercontent.com', diff --git a/apps/webapp/src/environments/environment.prod.ts b/apps/webapp/src/environments/environment.prod.ts index 15dd8d33..8e9ea910 100644 --- a/apps/webapp/src/environments/environment.prod.ts +++ b/apps/webapp/src/environments/environment.prod.ts @@ -1,7 +1,16 @@ -export const environment = { +import { IEnvironment } from '@env/ienvironment'; +import sharedEnvironment from './base'; + +export const environment: IEnvironment = { + ...sharedEnvironment, production: true, envName: 'prod', + + REMOTE_CONFIG_URL: '/assets/data/ui-config.json', + API_BASE_URL: 'https://api.kashmora.com/api', + plugins: [], + auth: { clientId: '791772336084-vkt37abstm1du92ofdmhgi30vgd7t0oa.apps.googleusercontent.com', diff --git a/apps/webapp/src/environments/environment.ts b/apps/webapp/src/environments/environment.ts index 4584bb3f..e09e9b54 100644 --- a/apps/webapp/src/environments/environment.ts +++ b/apps/webapp/src/environments/environment.ts @@ -1,16 +1,24 @@ // This file can be replaced during build by using the `fileReplacements` array. // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. // The list of file replacements can be found in `angular.json`. +import { IEnvironment } from '@env/ienvironment'; import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin'; import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin'; +import sharedEnvironment from './base'; -export const environment = { +export const environment: IEnvironment = { + ...sharedEnvironment, production: false, envName: 'dev', + + REMOTE_CONFIG_URL: '/assets/data/ui-config.json', + API_BASE_URL: 'http://localhost:3000/api', + plugins: [ NgxsReduxDevtoolsPluginModule.forRoot({ maxAge: 10 }), NgxsLoggerPluginModule.forRoot() ], + auth: { clientId: '791772336084-vkt37abstm1du92ofdmhgi30vgd7t0oa.apps.googleusercontent.com', diff --git a/apps/webapp/src/environments/ienvironment.ts b/apps/webapp/src/environments/ienvironment.ts new file mode 100644 index 00000000..5a0d4b98 --- /dev/null +++ b/apps/webapp/src/environments/ienvironment.ts @@ -0,0 +1,20 @@ +// import { OidcProviderConfig } from '@ngx-starter-kit/oidc'; +import { ModuleWithProviders } from '@angular/core'; + +export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; + +export interface IEnvironment { + production: boolean; + envName: string; + REMOTE_CONFIG_URL: string; + + // Enables use of ng.profiler.timeChangeDetection(); in browser console + enableDebugTools?: boolean; + logLevel?: LogLevel; + + [key: string]: any; + plugins: ModuleWithProviders[]; + featureFlags?: { + [key: string]: boolean; + }; +} diff --git a/libs/core/README.md b/libs/core/README.md index 12e85fe4..d0f33211 100644 --- a/libs/core/README.md +++ b/libs/core/README.md @@ -2,6 +2,24 @@ This library was generated with [Nx](https://nx.dev). +### Service + +1. AppConfigService - loading remote config and featureFlags +2. PageTitleService - set page title from breadcrumbs +3. AnalyticsService - Google Analytics + +### Stores + +1. AuthState - keep track of auth state + +### Handlers + +1. RouteHandler - update page title and send page views to google analytics + +### Interceptors + +1. ErrorInterceptor - report http errors + This module should contain minimal shared `Guards`, `Services` , `State` Injectables It should not contain `Components`, `Directives` and `Pipes`. For that, use `SharedModule` diff --git a/libs/core/src/index.ts b/libs/core/src/index.ts index efe46556..652c13a9 100644 --- a/libs/core/src/index.ts +++ b/libs/core/src/index.ts @@ -1,3 +1,5 @@ export * from './lib/core.module'; export { AnalyticsService } from './lib/services/analytics.service'; export { LayoutService } from './lib/services/layout.service'; +export { AppConfigService } from './lib/services/app-config.service'; +export { PageTitleService } from './lib/services/page-title.service'; diff --git a/libs/core/src/lib/core.module.ts b/libs/core/src/lib/core.module.ts index 1e900c55..95117e66 100644 --- a/libs/core/src/lib/core.module.ts +++ b/libs/core/src/lib/core.module.ts @@ -1,3 +1,4 @@ +import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { APP_INITIALIZER, NgModule, Optional, SkipSelf } from '@angular/core'; import { environment } from '@env/environment'; import { @@ -23,10 +24,20 @@ import { } from '@ngxs/router-plugin'; import { NgxsStoragePluginModule } from '@ngxs/storage-plugin'; import { NgxsModule } from '@ngxs/store'; -import { AuthHandler } from './handler/auth.handler'; -import { CustomRouterStateSerializer } from './handler/custom-router-state.serializer'; -import { RouteHandler } from './handler/route.handler'; +import { AuthHandler } from './handlers/auth.handler'; +import { CustomRouterStateSerializer } from './handlers/custom-router-state.serializer'; +import { RouteHandler } from './handlers/route.handler'; +import { ErrorInterceptor } from './interceptors/error.interceptor'; +import { AppConfigService } from './services/app-config.service'; import { AuthState } from './state/auth.state'; + +// appConfig initializer factory function +const appConfigInitializerFn = (appConfig: AppConfigService) => { + return () => { + return appConfig.load(); + }; +}; + // Noop handler for factory function export function noop() { return () => {}; @@ -79,6 +90,17 @@ export function noop() { provide: RouterStateSerializer, useClass: CustomRouterStateSerializer }, + { + provide: HTTP_INTERCEPTORS, + useClass: ErrorInterceptor, + multi: true + }, + { + provide: APP_INITIALIZER, + useFactory: appConfigInitializerFn, + deps: [AppConfigService], + multi: true + }, { provide: APP_INITIALIZER, useFactory: noop, diff --git a/libs/core/src/lib/handler/auth.handler.ts b/libs/core/src/lib/handlers/auth.handler.ts similarity index 100% rename from libs/core/src/lib/handler/auth.handler.ts rename to libs/core/src/lib/handlers/auth.handler.ts diff --git a/libs/core/src/lib/handler/custom-router-state.serializer.ts b/libs/core/src/lib/handlers/custom-router-state.serializer.ts similarity index 100% rename from libs/core/src/lib/handler/custom-router-state.serializer.ts rename to libs/core/src/lib/handlers/custom-router-state.serializer.ts diff --git a/libs/core/src/lib/handler/route.handler.ts b/libs/core/src/lib/handlers/route.handler.ts similarity index 100% rename from libs/core/src/lib/handler/route.handler.ts rename to libs/core/src/lib/handlers/route.handler.ts diff --git a/libs/core/src/lib/interceptors/error.interceptor.ts b/libs/core/src/lib/interceptors/error.interceptor.ts new file mode 100644 index 00000000..2aa1774c --- /dev/null +++ b/libs/core/src/lib/interceptors/error.interceptor.ts @@ -0,0 +1,45 @@ +import { + HttpErrorResponse, + HttpEvent, + HttpHandler, + HttpInterceptor, + HttpRequest +} from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable, throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; +// import { Store } from '@ngxs/store'; +// import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar'; + +@Injectable({ + providedIn: 'root' +}) +export class ErrorInterceptor implements HttpInterceptor { + + constructor(/*private snackBar: MatSnackBar, private store : Store*/) {} + + intercept( + req: HttpRequest, + next: HttpHandler + ): Observable> { + return next.handle(req).pipe(catchError(this.handleError)); + } + + /* tslint:disable */ + public handleError = (errorRes: HttpErrorResponse) => { + const { + error: { status, error, message } + } = errorRes; + // Do messaging and error handling here + // this.snackBar.open( + // `Error ! ${message}`, + // '', + // ErrorInterceptor.snackBarConfig + // ); + console.error( + `Backend Error ! status: ${status}, error: ${error}, message: ${message}` + ); + + return throwError(errorRes); + }; +} diff --git a/libs/core/src/lib/services/app-config.service.ts b/libs/core/src/lib/services/app-config.service.ts new file mode 100644 index 00000000..3a4f1633 --- /dev/null +++ b/libs/core/src/lib/services/app-config.service.ts @@ -0,0 +1,32 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { environment } from '@env/environment'; +import { IEnvironment } from '@env/ienvironment'; + +/** + * In Components, inject `AppConfigService` and usage: + * this.isShowingFeatureA = appConfig.config.featureFlags[ 'feature-a' ]; + */ +@Injectable({ + providedIn: 'root' +}) +export class AppConfigService { + private configUrl = environment.REMOTE_CONFIG_URL; + private configPrivate = environment; + + constructor(private http: HttpClient) {} + + async load(configUrl = this.configUrl) { + try { + const remoteConfig = await this.http + .get(configUrl) + .toPromise(); + this.configPrivate = { ...environment, ...remoteConfig }; + } catch { + console.error(`Unable to load remote config url ${configUrl}`); + } + } + get config() { + return this.configPrivate; + } +}