Skip to content

Commit

Permalink
Merge pull request #2409 from IDEMSInternational/feat/runtime-deploym…
Browse files Browse the repository at this point in the history
…ent-frontend

Feat: runtime deployment frontend
  • Loading branch information
chrismclarke authored Sep 12, 2024
2 parents 1511013 + 1a47cb3 commit a3a6185
Show file tree
Hide file tree
Showing 22 changed files with 136 additions and 98 deletions.
12 changes: 6 additions & 6 deletions packages/data-models/deployment.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const DEPLOYMENT_CONFIG_VERSION = 20240910.0;

/** Configuration settings available to runtime application */
export interface IDeploymentRuntimeConfig {
/** version of open-app-builder used to compile */
/** version of open-app-builder used to compile, read from builder repo package.json */
_app_builder_version: string;
/** tag of content version provided by content git repo*/
_content_version: string;
Expand Down Expand Up @@ -59,6 +59,10 @@ export interface IDeploymentRuntimeConfig {
url?: string;
publicApiKey?: string;
};
web: {
/** Relative path of custom favicon asset to load from app_data assets */
favicon_asset?: string;
};
}

/** Deployment settings not available at runtime */
Expand Down Expand Up @@ -124,10 +128,6 @@ interface IDeploymentCoreConfig {
/** translated string for import. Default `./app_data/translations_source/translated_strings */
translated_strings_path?: string;
};
web: {
/** Relative path of custom favicon asset to load from app_data assets */
favicon_asset?: string;
};
workflows: {
/** path to custom workflow files to include */
custom_ts_files: string[];
Expand Down Expand Up @@ -169,6 +169,7 @@ export const DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS: IDeploymentRuntimeConfig = {
supabase: {
enabled: false,
},
web: {},
};

/** Full example of just all config once merged with defaults */
Expand Down Expand Up @@ -198,7 +199,6 @@ export const DEPLOYMENT_CONFIG_EXAMPLE_DEFAULTS: IDeploymentConfig = {
source_strings_path: "./app_data/translations_source/source_strings",
translated_strings_path: "./app_data/translations_source/translated_strings",
},
web: {},
workflows: {
custom_ts_files: [],
task_cache_path: "./tasks",
Expand Down
21 changes: 12 additions & 9 deletions src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,19 @@
<ion-toolbar color="primary">
<ion-title>{{ sideMenuDefaults.title }}</ion-title>
<div class="app-version" slot="end">
<span *ngIf="sideMenuDefaults.should_show_version && CONTENT_VERSION">
<abbr [title]="APP_VERSION" tabindex="1">
{{ CONTENT_VERSION }}
</abbr>
</span>
<span *ngIf="sideMenuDefaults.should_show_version && !CONTENT_VERSION"
>{{ APP_VERSION }}
</span>
@if (sideMenuDefaults.should_show_version) {
@if (deploymentConfig._content_version; as CONTENT_VERSION) {
<span>
<abbr [title]="deploymentConfig._app_builder_version" tabindex="1">
{{ CONTENT_VERSION }}
</abbr>
</span>
} @else {
<span>{{ deploymentConfig._app_builder_version }} </span>
}
}
<span *ngIf="sideMenuDefaults.should_show_deployment_name" style="margin-left: 16px"
>({{ DEPLOYMENT_NAME }})</span
>({{ deploymentConfig.name }})</span
>
</div>
</ion-toolbar>
Expand Down
21 changes: 14 additions & 7 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,25 @@ import { SeoService } from "./shared/services/seo/seo.service";
import { FeedbackService } from "./feature/feedback/feedback.service";
import { ShareService } from "./shared/services/share/share.service";
import { LocalStorageService } from "./shared/services/local-storage/local-storage.service";
import { DeploymentService } from "./shared/services/deployment/deployment.service";

@Component({
selector: "app-root",
templateUrl: "app.component.html",
styleUrls: ["app.component.scss"],
})
export class AppComponent {
APP_VERSION = environment.version;
CONTENT_VERSION = environment.deploymentConfig.git.content_tag_latest;
DEPLOYMENT_NAME = environment.deploymentName;
appConfig: IAppConfig;
appAuthenticationDefaults: IAppConfig["APP_AUTHENTICATION_DEFAULTS"];
sideMenuDefaults: IAppConfig["APP_SIDEMENU_DEFAULTS"];
footerDefaults: IAppConfig["APP_FOOTER_DEFAULTS"];
/** Track when app ready to render sidebar and route templates */
public renderAppTemplates = false;

public get deploymentConfig() {
return this.deploymentService.config;
}

/**
* A space-separated list of values, hierarchically representing the current platform,
* e.g. on iPhone the value would be "mobile ios iphone".
Expand All @@ -66,6 +69,9 @@ export class AppComponent {
platforms: string;

constructor(
// Component UI
private deploymentService: DeploymentService,

// 3rd Party Services
private platform: Platform,
private cdr: ChangeDetectorRef,
Expand Down Expand Up @@ -148,9 +154,10 @@ export class AppComponent {
}
/** Populate contact fields that may be used by other services during initialisation */
private async populateAppInitFields() {
this.localStorageService.setProtected("DEPLOYMENT_NAME", this.DEPLOYMENT_NAME);
this.localStorageService.setProtected("APP_VERSION", this.APP_VERSION);
this.localStorageService.setProtected("CONTENT_VERSION", this.CONTENT_VERSION);
const { _content_version, _app_builder_version, name } = this.deploymentService.config;
this.localStorageService.setProtected("DEPLOYMENT_NAME", name);
this.localStorageService.setProtected("APP_VERSION", _app_builder_version);
this.localStorageService.setProtected("CONTENT_VERSION", _content_version);
// HACK - ensure first_app_launch migrated from event service
if (!this.localStorageService.getProtected("APP_FIRST_LAUNCH")) {
await this.appEventService.ready();
Expand All @@ -164,7 +171,7 @@ export class AppComponent {
* Currently only run on native where specified (but can comment out for testing locally)
*/
private async loadAuthConfig() {
const { firebase } = environment.deploymentConfig;
const { firebase } = this.deploymentService.config;
const { enforceLogin } = this.appAuthenticationDefaults;
const ensureLogin = firebase.config && enforceLogin && Capacitor.isNativePlatform();
if (ensureLogin) {
Expand Down
12 changes: 11 additions & 1 deletion src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ErrorHandler, NgModule } from "@angular/core";
import { APP_INITIALIZER, ErrorHandler, NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { FormsModule } from "@angular/forms";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
Expand All @@ -25,6 +25,7 @@ import { TemplateComponentsModule } from "./shared/components/template/template.
import { ContextMenuModule } from "./shared/modules/context-menu/context-menu.module";
import { TourModule } from "./feature/tour/tour.module";
import { ErrorHandlerService } from "./shared/services/error-handler/error-handler.service";
import { DeploymentService } from "./shared/services/deployment/deployment.service";

// Note we need a separate function as it's required
// by the AOT compiler.
Expand Down Expand Up @@ -58,6 +59,15 @@ export function lottiePlayerFactory() {
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
HTTP,
Device,
// ensure deployment service initialized before app component load
{
provide: APP_INITIALIZER,
multi: true,
useFactory: (deploymentService: DeploymentService) => {
return () => deploymentService.ready();
},
deps: [DeploymentService],
},
httpInterceptorProviders,
{ provide: ErrorHandler, useClass: ErrorHandlerService },
],
Expand Down
10 changes: 6 additions & 4 deletions src/app/feature/feedback/feedback.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
import { UserMetaService } from "src/app/shared/services/userMeta/userMeta.service";
import { TemplateService } from "src/app/shared/components/template/services/template.service";
import { generateTimestamp } from "src/app/shared/utils";
import { environment } from "src/environments/environment";
import { DbService } from "src/app/shared/services/db/db.service";
import { DBSyncService } from "src/app/shared/services/db/db-sync.service";
import {
Expand All @@ -38,6 +37,7 @@ import {
} from "src/app/shared/components/template/services/instance/template-action.registry";
import { SyncServiceBase } from "src/app/shared/services/syncService.base";
import { LocalStorageService } from "src/app/shared/services/local-storage/local-storage.service";
import { DeploymentService } from "src/app/shared/services/deployment/deployment.service";

@Injectable({
providedIn: "root",
Expand Down Expand Up @@ -80,7 +80,8 @@ export class FeedbackService extends SyncServiceBase {
private router: Router,
private themeService: ThemeService,
private skinService: SkinService,
private templateActionRegistry: TemplateActionRegistry
private templateActionRegistry: TemplateActionRegistry,
private deploymentService: DeploymentService
) {
super("Feedback");
this.subscribeToAppConfigChanges();
Expand Down Expand Up @@ -367,13 +368,14 @@ export class FeedbackService extends SyncServiceBase {
* device info and user uuid
*/
public generateFeedbackMetadata() {
const { _app_builder_version, name } = this.deploymentService.config;
const metadata: IFeedbackMetadata = {
deviceInfo: this.deviceInfo,
pathname: location.pathname,
uuid: this.userMetaService.getUserMeta("uuid"),
timestamp: generateTimestamp(),
app_version: environment.version,
app_deployment_name: environment.deploymentName,
app_version: _app_builder_version,
app_deployment_name: name,
app_theme: this.themeService.getCurrentTheme(),
app_skin: this.skinService.getActiveSkinName(),
};
Expand Down
13 changes: 5 additions & 8 deletions src/app/shared/services/app-config/app-config.service.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { Injectable } from "@angular/core";
import { getDefaultAppConfig, IAppConfig, IAppConfigOverride } from "data-models";
import { BehaviorSubject } from "rxjs";
import { environment } from "src/environments/environment";
import { deepMergeObjects, RecursivePartial, trackObservableObjectChanges } from "../../utils";
import clone from "clone";
import { SyncServiceBase } from "../syncService.base";
import { startWith } from "rxjs/operators";
import { Observable } from "rxjs";
import { DeploymentService } from "../deployment/deployment.service";

@Injectable({
providedIn: "root",
})
export class AppConfigService extends SyncServiceBase {
deploymentOverrides: IAppConfigOverride = (environment.deploymentConfig as any).app_config || {};
/** List of constants provided by data-models combined with deployment-specific overrides and skin-specific overrides */
appConfig$ = new BehaviorSubject<IAppConfig>(undefined as any);

Expand Down Expand Up @@ -42,19 +41,17 @@ export class AppConfigService extends SyncServiceBase {
return this.changes$.pipe(startWith(this.value));
}

constructor() {
constructor(private deploymentService: DeploymentService) {
super("AppConfig");
this.initialise();
}

private initialise() {
const deploymentOverrides: IAppConfigOverride = this.deploymentService.config.app_config || {};
this.APP_CONFIG = getDefaultAppConfig();
// Store app config with deployment overrides applied, to be merged with additional overrides when applied
this.deploymentAppConfig = this.applyAppConfigOverrides(
this.APP_CONFIG,
this.deploymentOverrides
);
this.updateAppConfig(this.deploymentOverrides);
this.deploymentAppConfig = this.applyAppConfigOverrides(this.APP_CONFIG, deploymentOverrides);
this.updateAppConfig(deploymentOverrides);
}

public updateAppConfig(overrides: IAppConfigOverride) {
Expand Down
7 changes: 4 additions & 3 deletions src/app/shared/services/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { Injectable } from "@angular/core";
import { FirebaseAuthentication, User } from "@capacitor-firebase/authentication";
import { BehaviorSubject, firstValueFrom } from "rxjs";
import { filter } from "rxjs/operators";
import { environment } from "src/environments/environment";
import { SyncServiceBase } from "../syncService.base";
import { TemplateActionRegistry } from "../../components/template/services/instance/template-action.registry";
import { FirebaseService } from "../firebase/firebase.service";
import { LocalStorageService } from "../local-storage/local-storage.service";
import { DeploymentService } from "../deployment/deployment.service";

@Injectable({
providedIn: "root",
Expand All @@ -18,13 +18,14 @@ export class AuthService extends SyncServiceBase {
constructor(
private templateActionRegistry: TemplateActionRegistry,
private firebaseService: FirebaseService,
private localStorageService: LocalStorageService
private localStorageService: LocalStorageService,
private deploymentService: DeploymentService
) {
super("Auth");
this.initialise();
}
private initialise() {
const { firebase } = environment.deploymentConfig;
const { firebase } = this.deploymentService.config;
if (firebase?.auth?.enabled && this.firebaseService.app) {
this.addAuthListeners();
this.registerTemplateActionHandlers();
Expand Down
6 changes: 3 additions & 3 deletions src/app/shared/services/crashlytics/crashlytics.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { FirebaseCrashlytics } from "@capacitor-firebase/crashlytics";
import { Capacitor } from "@capacitor/core";
import { Device } from "@capacitor/device";
import { AsyncServiceBase } from "../asyncService.base";
import { environment } from "src/environments/environment";
import { DeploymentService } from "../deployment/deployment.service";

@Injectable({
providedIn: "root",
Expand All @@ -14,13 +14,13 @@ import { environment } from "src/environments/environment";
* https://github.com/capawesome-team/capacitor-firebase/tree/main/packages/crashlytics
*/
export class CrashlyticsService extends AsyncServiceBase {
constructor() {
constructor(private deploymentService: DeploymentService) {
super("Crashlytics");
this.registerInitFunction(this.initialise);
}
private async initialise() {
if (Capacitor.isNativePlatform()) {
const { firebase } = environment.deploymentConfig;
const { firebase } = this.deploymentService.config;
// Crashlytics is still supported on native device without firebase config (uses google-services.json)
// so use config property to toggle enabled instead
await this.setEnabled({ enabled: firebase?.crashlytics?.enabled });
Expand Down
9 changes: 6 additions & 3 deletions src/app/shared/services/db/db-sync.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { AppConfigService } from "../app-config/app-config.service";
import { AsyncServiceBase } from "../asyncService.base";
import { UserMetaService } from "../userMeta/userMeta.service";
import { DbService } from "./db.service";
import { DeploymentService } from "../deployment/deployment.service";

@Injectable({ providedIn: "root" })
/**
Expand All @@ -29,7 +30,8 @@ export class DBSyncService extends AsyncServiceBase {
private dbService: DbService,
private http: HttpClient,
private userMetaService: UserMetaService,
private appConfigService: AppConfigService
private appConfigService: AppConfigService,
private deploymentService: DeploymentService
) {
super("DB Sync");
this.registerInitFunction(this.inititialise);
Expand Down Expand Up @@ -81,13 +83,14 @@ export class DBSyncService extends AsyncServiceBase {

/** Populate common app_meta to local record */
private generateServerRecord(record: any, mapping: IDBServerMapping) {
const { name, _app_builder_version } = this.deploymentService.config;
const { is_user_record, user_record_id_field } = mapping;
if (is_user_record && user_record_id_field) {
const serverRecord: IDBServerUserRecord = {
app_user_id: this.userMetaService.getUserMeta("uuid"),
app_user_record_id: record[user_record_id_field],
app_deployment_name: environment.deploymentName,
app_version: environment.version,
app_deployment_name: name,
app_version: _app_builder_version,
data: record,
};
return serverRecord;
Expand Down
20 changes: 13 additions & 7 deletions src/app/shared/services/deployment/deployment.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, signal } from "@angular/core";
import { Injectable } from "@angular/core";
import { AsyncServiceBase } from "../asyncService.base";
import { HttpClient } from "@angular/common/http";
import { catchError, map, of, firstValueFrom } from "rxjs";
Expand All @@ -8,8 +8,12 @@ import { DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, IDeploymentRuntimeConfig } from "pa
/**
* Deployment runtime config settings
*
* NOTE - this is populated as a blocking service during init,
* so no need to await service initialisation before access
* NOTE - this is intialized using an `APP_INITIALIZER` token within
* the main app.module.ts and will block all other services from loading until
* it is fully initialised
*
* Services that access the deployment config therefore do not need to await
* DeploymentService init/ready methods.
*/
export class DeploymentService extends AsyncServiceBase {
constructor(private http: HttpClient) {
Expand All @@ -18,21 +22,23 @@ export class DeploymentService extends AsyncServiceBase {
}

/** Private writeable config to allow population from JSON */
private _config = signal(DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS);
private _config = DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS;

/** Read-only access to deployment runtime config */
public config = this._config.asReadonly();
public get config() {
return this._config;
}

/** Load active deployment configuration from JSON file */
private async initialise() {
const deployment = await firstValueFrom(this.loadDeployment());
if (deployment) {
this._config.set(deployment);
this._config = deployment;
}
}

private loadDeployment() {
return this.http.get("assets/deployment.json").pipe(
return this.http.get("assets/app_data/deployment.json").pipe(
catchError(() => {
console.warn("No deployment config available");
return of(null);
Expand Down
Loading

0 comments on commit a3a6185

Please sign in to comment.