Skip to content

Commit

Permalink
Merge branch 'master' into fix/android-workflow-variables
Browse files Browse the repository at this point in the history
  • Loading branch information
esmeetewinkel authored Apr 5, 2024
2 parents ba7f7f6 + fe4f120 commit 39826af
Show file tree
Hide file tree
Showing 20 changed files with 158 additions and 39 deletions.
2 changes: 1 addition & 1 deletion documentation/docs/developers/server-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ yarn workspace api test:e2e -t app_user
Requires [docker desktop](https://www.docker.com/products/docker-desktop/) installed locally

1. Configure .env variables as per `packages\server\README.md`
Ensure `API_BASE_PATH="/api/"` to allow running as part of full stack
Ensure `API_BASE_PATH="/api"` to allow running as part of full stack

2. Build api
```bash
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "frontend",
"version": "0.16.27",
"version": "0.16.28",
"author": "IDEMS International",
"license": "See LICENSE",
"homepage": "https://idems.international/",
Expand Down
3 changes: 1 addition & 2 deletions packages/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ WEBSOCKET_PORT=3000
API_PORT=3000
# If running within docker containers this should be set to reverse proxy, e.g. /api/
# If serving locally this should be left blank
# REMEMBER TO REVERT TO /api/ WHEN BUILDING DOCKER IMAGE
API_BASE_PATH="/api/"
API_BASE_PATH=""
DB_HOST=localhost
DB_PORT=5432

Expand Down
2 changes: 1 addition & 1 deletion packages/api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ RUN yarn build
# Add secondary set of production-only node_modules to dist (not sure why yarn berry needs re-init but seems to be the case)
# NOTE - if updating to yarn 4 will require additional plugin import line
RUN yarn set version 3.3.1 && \
# yarn plugin import workspace-tools && \
yarn plugin import workspace-tools && \
yarn workspaces focus api --production

### STAGE 2: Serve Dashboard ###
Expand Down
2 changes: 1 addition & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "api",
"version": "1.4.3",
"version": "1.4.4",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
Expand Down
3 changes: 2 additions & 1 deletion packages/api/spec-export.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"parameters": [
{
"name": "x-deployment-db-name",
"required": true,
"in": "header",
"description": "Name of db for deployment to populate",
"schema": {
Expand Down Expand Up @@ -520,7 +521,7 @@
"info": {
"title": "IDEMS Apps API",
"description": "App-Server Communication",
"version": "1.3.0",
"version": "1.4.4",
"contact": {}
},
"tags": [
Expand Down
10 changes: 0 additions & 10 deletions packages/api/src/endpoints/app_users/app_user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,6 @@ export class AppUsersController {
return this.appUsersService.model.findAll();
}

// @Get(":app_user_id")
// findOne(@Param("app_user_id") app_user_id: string): Promise<AppUser> {
// return this.appUsersService.findOne(app_user_id);
// }

// @Delete(":id")
// remove(@Param("id") id: string): Promise<void> {
// return this.appUsersService.remove(id);
// }

@Get(":app_user_id")
@ApiParam({ name: "app_user_id", type: String })
@ApiOperation({ summary: "Get user profile" })
Expand Down
5 changes: 5 additions & 0 deletions packages/api/src/environment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,16 @@ function loadEnv() {
const { NODE_ENV } = process.env;

let envFilePath = resolve(__dirname, "../../.env");
// Hack - update path if running from compiled dist folder
if (envFilePath.includes("dist")) {
envFilePath = resolve(envFilePath, "../../.env");
}
if (NODE_ENV === "test") {
envFilePath = resolve(__dirname, "../../test/.test.env");
}
// In production env vars are passed from docker container instead of local file
if (!existsSync(envFilePath)) {
console.log(envFilePath);
console.warn("Env file does not exist, using local env variables", Object.keys(process.env));
return;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ async function bootstrap() {
.setDescription("App-Server Communication")
.setVersion(version)
.addTag("api")
// .setBasePath("/api")
// Fix swagger redirection issue
// https://github.com/nestjs/swagger/issues/448
// https://stackoverflow.com/questions/63954037/nestjs-swagger-missing-base-path
.addServer(environment.API_BASE_PATH ? `/${environment.API_BASE_PATH}` : "")
// Ensure api base path populated as /api (not /api/ or similar)
.addServer(environment.API_BASE_PATH ? `/${environment.API_BASE_PATH.replace(/\//g, "")}` : "")
.build();
const document = SwaggerModule.createDocument(app, config, { ignoreGlobalPrefix: true });
// add export for docs (https://github.com/nestjs/swagger/issues/158)
Expand Down
1 change: 1 addition & 0 deletions packages/data-models/flowTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ export namespace FlowTypes {
"toggle_field",
"track_event",
"trigger_actions",
"user",
] as const;

export interface TemplateRowAction {
Expand Down
16 changes: 8 additions & 8 deletions packages/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ You should be able to access the dashboard at http://localhost (will redirect)
http://localhost/dbadmin/

Login details
```
```yml
System: PostgreSQL
Server: db
Username: (POSTGRES_USER in .env)
Expand All @@ -79,7 +79,7 @@ http://localhost/dashboard/
A new database will need to be created to allow access for metabase. This should be automatically configured in [docker/config/db/init.sh](./docker/config/db/init.sh), however if these steps fail they can be run manually (currently a bit temperamental - TODO ISSUE - will know if successful if can see a database created that matches the provided $MB_DB_DBNAME).

Manual SQL can be executed from the Adminer DB interface, e.g. using the example config:
```
```sql
CREATE USER metabase WITH PASSWORD 'metabase';
CREATE DATABASE metabase;
GRANT ALL PRIVILEGES ON DATABASE metabase to metabase;
Expand All @@ -88,13 +88,13 @@ GRANT ALL PRIVILEGES ON DATABASE metabase to metabase;
Once running complete configuration from within the dashboard app.

Create a user account using preferred credentials and retain securely elsewhere. As a default when running test servers in docker the following credentials are used
```
```yml
email: [email protected]
password: demo1234
```

Configure connection to the same database created by the api:
```
```yml
Database type: PostgreSQL
Name: (any)
Host: db
Expand All @@ -111,7 +111,7 @@ You should then see the main dashboard page
http://localhost/analytics/

An initial set of configuration screens should walk through the process of setting up users and a database connection. If connection fails or additional users need to be created the database can be accessed via the same Adminer `/dbadmin` path and `root` user credentials
```
```yml
System: MySQL
Server: analytics_db
Username: root
Expand All @@ -120,13 +120,13 @@ Database: (blank)
```
You will be asked to create a super user login and password. This information should be stored safely.
As a default when running test servers in docker the following credentials are used
```
```yml
email: [email protected]
password: demo1234
```

To enable data collection from the frontend application follow instructions in the dashboard. You may need to record the Matomo Url and site ID as seen on the initial page, e.g.
```
```yml
Matomo URL: http://localhost/
Your site ID: 1
```
Expand All @@ -150,7 +150,7 @@ http://localhost/triggers/
This uses the tool [N8N](https://n8n.io/) to provide access to automation and triggers based on events.
Currently the tool only supports single user authentication provided by username and password provided in the `.env` file

```
```yml
Username: ($N8N_BASIC_AUTH_USER in .env)
Password: ($N8N_BASIC_AUTH_PASSWORD in .env)
```
Expand Down
4 changes: 2 additions & 2 deletions packages/server/docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ services:
# context: ../../api
# dockerfile: Dockerfile
# target: prod-env
image: idems/apps-api:1.4.3
image: idems/apps-api:1.4.4
env_file:
- ../../api/.env
environment:
Expand Down Expand Up @@ -89,7 +89,7 @@ services:
# Postgres DB admin (pgadmin)
####################################################################
pgadmin:
image: dpage/pgadmin4:6
image: dpage/pgadmin4:8
container_name: plh_pg_admin
restart: unless-stopped
env_file:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class SelectTextComponent

getParams() {
this.placeholder = getStringParamFromTemplateRow(this._row, "placeholder", "");
this.maxLength = getNumberParamFromTemplateRow(this._row, "max_length", 30);
this.maxLength = getNumberParamFromTemplateRow(this._row, "max_length", -1);
this.textAlign = getStringParamFromTemplateRow(this._row, "text_align", "center");
this.style = getStringParamFromTemplateRow(this._row, "style", null);
this.isNumberInput = getBooleanParamFromTemplateRow(this._row, "number_input", false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class TmplTextBoxComponent extends TemplateBaseComponent implements OnIni

getParams() {
this.placeholder = getStringParamFromTemplateRow(this._row, "placeholder", "");
this.maxLength = getNumberParamFromTemplateRow(this._row, "max_length", 30);
this.maxLength = getNumberParamFromTemplateRow(this._row, "max_length", -1);
this.textAlign = getStringParamFromTemplateRow(this._row, "text_align", "center");
this.style = getStringParamFromTemplateRow(this._row, "style", null);
this.isNumberInput = getBooleanParamFromTemplateRow(this._row, "number_input", false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,31 @@ import { booleanStringToBoolean } from "src/app/shared/utils";
import { TemplateTranslateService } from "./template-translate.service";
import { AsyncServiceBase } from "src/app/shared/services/asyncService.base";
import { TemplateActionRegistry } from "./instance/template-action.registry";
import { AppConfigService } from "src/app/shared/services/app-config/app-config.service";

@Injectable({ providedIn: "root" })
export class TemplateFieldService extends AsyncServiceBase {
globals: { [name: string]: FlowTypes.GlobalRow } = {};

/** App config prefix used */
public prefix: string;

constructor(
private localStorageService: LocalStorageService,
private dbService: DbService,
private translateService: TemplateTranslateService,
private templateActionRegistry: TemplateActionRegistry
private templateActionRegistry: TemplateActionRegistry,
private appConfigService: AppConfigService
) {
super("TemplateField");
this.registerInitFunction(this.initialise);
this.registerTemplateActionHandlers();
this.prefix = appConfigService.APP_CONFIG.FIELD_PREFIX;
}

private async initialise() {
await this.ensureAsyncServicesReady([this.dbService, this.translateService]);
this.ensureSyncServicesReady([this.localStorageService]);
this.ensureSyncServicesReady([this.localStorageService, this.appConfigService]);
}

private registerTemplateActionHandlers() {
Expand All @@ -49,7 +55,7 @@ export class TemplateFieldService extends AsyncServiceBase {
* TODO - ideally showWarnings should be linked to some sort of debug mode
*/
public getField(key: string, showWarnings = true) {
let val: any = this.localStorageService.getString("rp-contact-field." + key);
let val: any = this.localStorageService.getString(`${this.prefix}.${key}`);
// provide a fallback if the target variable does not exist in local storage
if (val === null && showWarnings) {
// console.warn("field value not found for key:", key);
Expand Down Expand Up @@ -80,7 +86,7 @@ export class TemplateFieldService extends AsyncServiceBase {
}
}
// write to local storage - this will cast to string
this.localStorageService.setString("rp-contact-field." + key, value);
this.localStorageService.setString(`${this.prefix}.${key}`, value);

// write to db - note this can handle more data formats but only string/number will be available to queries
if (typeof value === "boolean") value = "value";
Expand Down
2 changes: 2 additions & 0 deletions src/app/shared/services/server/interceptors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ let { db_name, endpoint: API_ENDPOINT } = environment.deploymentConfig.api;

// Override development credentials when running locally
if (!environment.production) {
// Docker endpoint. Replace :3000 with /api if running standalone api
API_ENDPOINT = "http://localhost:3000";
db_name = "dev";
}

/** Handle updating urls intended for api server */
Expand Down
62 changes: 59 additions & 3 deletions src/app/shared/services/userMeta/userMeta.service.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Device } from "@capacitor/device";
import { firstValueFrom } from "rxjs";

import { AsyncServiceBase } from "../asyncService.base";
import { DbService } from "../db/db.service";
import { TemplateActionRegistry } from "../../components/template/services/instance/template-action.registry";
import { TemplateFieldService } from "../../components/template/services/template-field.service";

@Injectable({ providedIn: "root" })
export class UserMetaService extends AsyncServiceBase {
/** keep an in-memory copy of user to provide synchronously */
public userMeta: IUserMeta;
constructor(private dbService: DbService) {
super("UsesrMetaService");
constructor(
private dbService: DbService,
private templateActionRegistry: TemplateActionRegistry,
private http: HttpClient,
private fieldService: TemplateFieldService
) {
super("UserMetaService");
this.registerInitFunction(this.initialise);
}

/** When first initialising ensure a default profile created and any newer defaults are merged with older user profiles */
private async initialise() {
await this.ensureAsyncServicesReady([this.dbService]);
await this.ensureAsyncServicesReady([this.dbService, this.fieldService]);
this.registerUserActions();
const userMetaValues = await this.dbService.table<IUserMetaEntry>("user_meta").toArray();
const userMeta: IUserMeta = USER_DEFAULTS;
userMetaValues.forEach((v) => {
Expand All @@ -27,6 +38,8 @@ export class UserMetaService extends AsyncServiceBase {
}
userMeta.uuid = uuid;
this.userMeta = userMeta;
// populate user id contact field
this.fieldService.setField("_app_user_id", uuid);
}

getUserMeta(key: keyof IUserMeta) {
Expand All @@ -38,6 +51,49 @@ export class UserMetaService extends AsyncServiceBase {
await this.dbService.table<IUserMetaEntry>("user_meta").bulkPut(entries as any);
this.userMeta = { ...this.userMeta, ...meta };
}

/** Import existing user contact fields and replace current user */
private async importUserFields(id: string) {
try {
// TODO - get type-safe return types using openapi http client
const profile = await firstValueFrom(
this.http.get(`/app_users/${id}`, { responseType: "json" })
);
if (!profile) {
console.error("[User Import] not found:" + id);
return;
}
const { contact_fields } = profile as any;
for (const [key, value] of Object.entries(contact_fields)) {
const fieldName = key.replace(`${this.fieldService.prefix}.`, "");
// TODO - handle special contact fields as required (e.g. _app_skin, _app_theme)
if (!fieldName.startsWith("_")) {
await this.fieldService.setField(fieldName, value as string);
}
}
} catch (error) {
console.error("[User Import] failed", error);
}
}

private registerUserActions() {
const childActions = {
import: this.importUserFields.bind(this),
};
const childActionNames = Object.keys(childActions).join(",");
this.templateActionRegistry.register({
user: async ({ args }) => {
const [actionId, ...childArgs] = args;
if (!childActions[actionId]) {
console.error(
`[${actionId}] user action not defined. Available actions:\n${childActionNames}`
);
return;
}
return childActions[actionId](childArgs);
},
});
}
}

interface IUserMetaEntry {
Expand Down
3 changes: 2 additions & 1 deletion src/theme/themes/_index.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@forward "./default.scss";
@forward "./pfr.scss";
@forward "./professional.scss";
@forward "./pfr.scss";
@forward "./plh_facilitator_mx.scss";
Loading

0 comments on commit 39826af

Please sign in to comment.