diff --git a/.dockerignore b/.dockerignore index 1984fdcb..1e7d376b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,4 @@ .git .idea -build **/*.env* diff --git a/.env b/.env index 3c7959cd..616a4492 100644 --- a/.env +++ b/.env @@ -1,5 +1 @@ -# PCDS config -REACT_APP_BC_BASE_MAP_TILES_URL=https://services.pacificclimate.org/tiles/bc-albers-lite/{z}/{x}/{y}.png - -# YNWT config -REACT_APP_YNWT_BASE_MAP_TILES_URL=https://services.pacificclimate.org/tiles/yukon-albers-lite/{z}/{x}/{y}.png +PUBLIC_URL=%REPLACE_PUBLIC_URL% diff --git a/.env.development b/.env.development new file mode 100755 index 00000000..f0211f66 --- /dev/null +++ b/.env.development @@ -0,0 +1 @@ +PUBLIC_URL=http://localhost:3000 diff --git a/.env.production b/.env.production index acc51464..616a4492 100644 --- a/.env.production +++ b/.env.production @@ -1 +1 @@ -PUBLIC_URL=http://localhost:3333/ +PUBLIC_URL=%REPLACE_PUBLIC_URL% diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 39056b2c..8833b2cc 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -17,6 +17,10 @@ jobs: run: | git fetch --prune --unshallow echo "REACT_APP_APP_VERSION=$(git describe --tags --abbrev=0) ($(git rev-parse --abbrev-ref HEAD):$(git log -1 --format=%h))" >> $GITHUB_ENV + - name: Build npm package + run: | + npm ci + npm run build - name: Publish to Registry uses: docker/build-push-action@v1 with: diff --git a/Makefile b/Makefile index 3a0540fa..f773db70 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,16 @@ # These variables are set to make it convenient to run the docker image locally. tag = $(shell git rev-parse --abbrev-ref HEAD) -port = 30502 +port = 30503 public_url = http://localhost:${port} image: - @SDP_TAG=$(tag) SDP_PORT=$(port) SDP_PUBLIC_URL=$(public_url) docker compose -f docker/docker-compose.yaml build --build-arg REACT_APP_APP_VERSION='$(shell ./generate-commitish.sh)' + @npm run build + @SDP_TAG=$(tag) SDP_PORT=$(port) docker compose -f docker/docker-compose.yaml build --build-arg REACT_APP_APP_VERSION='$(shell ./generate-commitish.sh)' up: - @SDP_TAG=$(tag) SDP_PORT=$(port) SDP_PUBLIC_URL=$(public_url) docker compose -f docker/docker-compose.yaml up -d --force-recreate - @echo "Station Data Portal running at $(public_url)" + @SDP_TAG=$(tag) SDP_PORT=$(port) docker compose -f docker/docker-compose.yaml up --force-recreate + @echo "Station Data Portal running on $(port)" @docker logs -f station-data-portal-frontend down: - @SDP_TAG=$(tag) SDP_PORT=$(port) SDP_PUBLIC_URL=$(public_url) docker compose -f docker/docker-compose.yaml down + @SDP_TAG=$(tag) SDP_PORT=$(port) docker compose -f docker/docker-compose.yaml down diff --git a/craco.config.js b/craco.config.js index 2125f679..944da6cc 100644 --- a/craco.config.js +++ b/craco.config.js @@ -3,10 +3,29 @@ const { CracoAliasPlugin } = require("react-app-alias"); const options = {}; // default is empty for most cases module.exports = { + eslint: null, plugins: [ { plugin: CracoAliasPlugin, options: {}, }, ], + // Works around a warning that plotly.js doesn't have a source map + // As warnings are treated as errors in the build, this is necessary + // node_modules are generally external code so it is hard to fix the warning + // and we can safely ignore sourcemap errors. + webpack: { + configure: { + ignoreWarnings: [ + function ignoreSourcemapsloaderWarnings(warning) { + return ( + warning.module && + warning.module.resource.includes("node_modules") && + warning.details && + warning.details.includes("source-map-loader") + ); + }, + ], + }, + }, }; diff --git a/docker/Dockerfile b/docker/Dockerfile index 5d881291..1e13587c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,39 +1,20 @@ -# This Dockerfile adapted from https://mherman.org/blog/dockerizing-a-react-app/ -# and Client Explorer Dockerfile. -# This Dockerfile can (and should) be used to pass through automatically generated -# version information to the build which is triggered when the image is run. -# To do this, issue the following build command: -# -# docker build --build-arg REACT_APP_APP_VERSION="$(./generate-commitish.sh)" -t . -# At this moment, Node.js 10.16 LTS is recommended for most users. -# -# In future, as we scale up, we may want to use an Alpine base image, which would reduce -# the size of the image by about an order of magnitude and reduce the attack surface of -# the image as well. +FROM node:22-bookworm-slim -FROM node:16 +RUN apt-get -y update && \ + apt-get install --no-install-recommends \ + -y curl rpl && \ + rm -rf /var/lib/apt/lists/* -ADD . /app -WORKDIR /app -RUN chown node /app - -ENV PATH /app/node_modules/.bin:$PATH -COPY --chown=node:node package.json /app/package.json - -# Currently, we have to force install. For details, see ./docs/installation.md -RUN npm install --quiet --force RUN npm install -g serve -COPY --chown=node:node . /app - -EXPOSE 8080 -# Move the build arg REACT_APP_APP_VERSION into an -# environment variable of the same name, for consumption -# by the npm build process in ./entrypoint.sh -ARG REACT_APP_APP_VERSION -ENV REACT_APP_APP_VERSION $REACT_APP_APP_VERSION +COPY --chown=node build /app +COPY --chown=node docker/entrypoint.sh /app/docker/entrypoint.sh +WORKDIR /app USER node -ENTRYPOINT docker/entrypoint.sh +EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "curl", "-f", "http://localhost:8080/healthcheck.js" ] + +ENTRYPOINT ["docker/entrypoint.sh"] diff --git a/docker/bc-config.yaml b/docker/bc-config.yaml deleted file mode 100644 index c11caac9..00000000 --- a/docker/bc-config.yaml +++ /dev/null @@ -1,25 +0,0 @@ -appTitle: BC Station Data - PCDS -baseMap: BC - -# sdsUrl values will be replaced by dev or prod URLs when they become ready. -# For now, we have demo instances inside the firewall. -# Uses monsoon database -#sdsUrl: http://docker-dev02.pcic.uvic.ca:30512 -# Uses new database -#sdsUrl: http://docker-dev02.pcic.uvic.ca:30562 -sdsUrl: http://localhost:5000 - -# Currently deployed metadata backends do not respond to provinces QP. -# When they do, we can invert the commenting out below. -#stationsQpProvinces: BC -stationFilters: histories[0].province = "BC" - -# Always necessary for CRMP database -networkFilters: name != "PCIC Climate Variables" - -# pdpDataUrl values will be replaced by dev or prod URLs when they become ready. -# For now, we have a demo instance inside the firewall, below. -# Uses monsoon database -pdpDataUrl: http://docker-dev02.pcic.uvic.ca:30514 -# Uses new database -#pdpDataUrl: http://docker-dev02.pcic.uvic.ca:??? diff --git a/docker/bc.env b/docker/bc.env deleted file mode 100644 index bdd28b36..00000000 --- a/docker/bc.env +++ /dev/null @@ -1,3 +0,0 @@ -PUBLIC_PORT=30506 -PUBLIC_URL=https://localhost:8080/ -REACT_APP_BC_BASE_MAP_TILES_URL=https://services.pacificclimate.org/tiles/bc-albers-lite/{z}/{x}/{y}.png diff --git a/docker/config.bc.js b/docker/config.bc.js new file mode 100644 index 00000000..d271cc91 --- /dev/null +++ b/docker/config.bc.js @@ -0,0 +1,25 @@ +window.env = { + PUBLIC_URL: "http://localhost:30503", + REACT_APP_BC_BASE_MAP_TILES_URL: + "https://services.pacificclimate.org/tiles/bc-albers-lite/{z}/{x}/{y}.png", + appTitle: "BC Station Data - PCDS", + baseMap: "BC", + + // Uses swarm based dev url by default + sdsUrl: + "https://beehive.pacificclimate.org/station-data-portal/pcds/metadata/", + + // Currently deployed metadata backends do not respond to provinces QP. + // When they do, we can invert the commenting out below. + //stationsQpProvinces: BC + stationFilters: 'histories[0].province = "BC"', + + // Always necessary for CRMP database + networkFilters: 'name != "PCIC Climate Variables"', + + // Uses monsoon database + pdpDataUrl: + "https://beehive.pacificclimate.org/station-data-portal/pcds/data/", + // Uses new database + //pdpDataUrl: http://docker-dev02.pcic.uvic.ca:??? +}; diff --git a/docker/config.ynwt.js b/docker/config.ynwt.js new file mode 100644 index 00000000..0859b129 --- /dev/null +++ b/docker/config.ynwt.js @@ -0,0 +1,26 @@ +// Example environment config for a YNWT based deployment + +window.env = { + PUBLIC_URL: "http://localhost:30503", + REACT_APP_YNWT_BASE_MAP_TILES_URL: + "https://services.pacificclimate.org/tiles/yukon-albers-lite/{z}/{x}/{y}.png", + appTitle: "YNWT Station Data", + baseMap: "YNWT", + + // sdsUrl values will be replaced by prod URLs when they become ready. + // For now, we have a dev instance. + // Uses monsoon database + sdsUrl: + "https://beehive.pacificclimate.org/station-data-portal/ynwt/metadata/", + + // We do not at present need to filter based on province (verify!) + //stationsQpProvinces: YK,NT + // We do not at present need to filter networks (verify!) + //networkFilters: ??? + + // pdpDataUrl values will be replaced by prod URLs when they become ready. + // For now, we have a dev instance. + // Uses monsoon database + pdpDataUrl: + "https://beehive.pacificclimate.org/station-data-portal/ynwt/data/", +}; diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 97ff47e9..9da51cb5 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -2,7 +2,6 @@ # # `SDP_TAG`: Image tag # `SDP_PORT`: External port to map to -# `SDP_PUBLIC_URL`: Public URL of app # Note: These values are set as part of the makefile, initialize the container # Using it. # @@ -19,14 +18,7 @@ services: dockerfile: ./docker/Dockerfile #image: pcic/station-data-portal-frontend:${SDP_TAG} container_name: station-data-portal-frontend - environment: - - PUBLIC_URL=${SDP_PUBLIC_URL} - env_file: - - bc.env volumes: - - type: bind - source: ./bc-config.yaml - target: /app/public/config.yaml - read_only: true + - ./config.bc.js:/app/config.js ports: - "${SDP_PORT}:8080" diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 455cd7d9..63857b7d 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,8 +1,19 @@ -# We build the app as part of the container startup so that the build process -# can consume the runtime environment variables. (CRA apps can only access -# environment variables at build time, not at run time.) This makes starting a -# container a lot heavier, but we don't spin up many instances, or often, -# so it doesn't matter. - -npm run build -serve -s build -l 8080 \ No newline at end of file +#!/bin/bash + +# Note: this pulls the public url by a combination of grep and cut and relies on +# PUBLIC_URL to be on its own line with the value and in the format of PUBLIC_URL="http://localhost:8080" +# Fragile to additional quotes due to us looking for index 2 +PUBLIC_URL=$(grep PUBLIC_URL config.js | cut -d'"' -f 2) + +# update static files with the public url +rpl -iR \ + -x **/*.js \ + -x **/*.html \ + -x **/*.css \ + -x **/*.json \ + "%REPLACE_PUBLIC_URL%" $PUBLIC_URL . + +# It is possible that the above could be replaced by a node.js based +# script which may prove more resillient long term + +serve -s . -l 8080 diff --git a/docker/ynwt-config.yaml b/docker/ynwt-config.yaml deleted file mode 100644 index b54bf196..00000000 --- a/docker/ynwt-config.yaml +++ /dev/null @@ -1,17 +0,0 @@ -appTitle: YNWT Station Data -baseMap: YNWT - -# sdsUrl values will be replaced by prod URLs when they become ready. -# For now, we have a dev instance. -# Uses monsoon database -sdsUrl: https://services.pacificclimate.org/dev/ynwt/meta - -# We do not at present need to filter based on province (verify!) -#stationsQpProvinces: YK,NT -# We do not at present need to filter networks (verify!) -#networkFilters: ??? - -# pdpDataUrl values will be replaced by prod URLs when they become ready. -# For now, we have a dev instance. -# Uses monsoon database -pdpDataUrl: https://services.pacificclimate.org/dev/ynwt/data diff --git a/docker/ynwt.env b/docker/ynwt.env deleted file mode 100644 index c6cf0270..00000000 --- a/docker/ynwt.env +++ /dev/null @@ -1,2 +0,0 @@ -#PUBLIC_URL=... -REACT_APP_YNWT_BASE_MAP_TILES_URL=https://services.pacificclimate.org/tiles/yukon-albers-lite/{z}/{x}/{y}.png \ No newline at end of file diff --git a/docs/developer/build.md b/docs/developer/build.md new file mode 100644 index 00000000..66b8969f --- /dev/null +++ b/docs/developer/build.md @@ -0,0 +1,51 @@ +# Building the project + +There are 3 modes of operation for the application which take slightly different build steps: + +### Local + +Everything in the project should be set up for easy development with defaults provided that allow +execution without modification to configuration. This execution is done via `npm run start`. The +project is then built via create react app and a local development server is started. + +Local config is provided via [public/config.js](../public/config.js) and is loaded automatically +as a static javascript file via the local development server. + +Public URL is overridden by the `.env.development` file to our expected `http://localhost:3000` + +### Local Docker + +Testing for deployment involves building in production mode and setting up a container as we will +in production. This allows us to ensure that dependencies are met and gives us a portable artifact +that we can set up on any docker capable machine and expect to work. + +Creating the container can be done via the `make image` command. This command executes `npm run build` +creating a static version of the website. `process.env` variables are baked into the files at this +time, so it should be avoided for evironment specific configuration use. These static assests are in +the `build/` folder. Once built the [Dockerfile](../../docker/Dockerfile) pulls in these files along +with dependencies to generate a docker image. + +Running the created docker image can be done via `make up`. This brings up the image based on the +specification in the [docker-compose.yaml](../../docker/docker-compose.yaml). This specification also +overrides our local development configuration values by mounting an alternative configuration. Two examples +are provided `config.bc.js` and `config.ynwt.js` representing our two common production versions. `bc` +is used by default. + +`PUBLIC_URL` is handled in two steps. During the build process we define a replacement value in `.env.production` +which is injected into any locations where the public URL is required. When the container starts we replace +these instances with the public URL defined in whatever `/app/config.js` within the container has for the +`PUBLIC_URL` value. The specific implementation of this replacement can be found in the +[entrypoint.sh](../../docker/entrypoint.sh) file which is used as the default entrypoint for the container +when it starts. + +### Production Docker + +Production docker essentially follows the same steps as above (what good would a local test be otherwise!) +but is executed via a github workflow. The resulting image is uploaded to our +[docker hub](https://registry.hub.docker.com/r/pcic/station-data-portal-frontend) for use where desired. + +Specific steps are defined in the [github workflow](../../.github/workflows/docker-publish.yml) file. + +When running in production we need to provide environment specific config, this config will closely +resemble the templates defined in the `config.bc.js` and `config.ynwt.js` files noted above and should be +mounted to `/app/config.js` within the container. diff --git a/docs/developer/configuration.md b/docs/developer/configuration.md index f712e579..cc699777 100644 --- a/docs/developer/configuration.md +++ b/docs/developer/configuration.md @@ -1,16 +1,16 @@ # Configuration Most configuration of the Station Data Portal frontend is done via a YAML -file, `public/config.yaml`. For details, see below. +file, `public/config.js`. For details, see below. For technical reasons, a few configuration parameters must be supplied via environment variables. -## Configuration via `public/config.yaml` +## Configuration via `public/config.js` This file must be a key-value map. It overrides the default configuration values, which are given below. Certain keys do not have default values and -_must_ be specified in `public/config.yaml`. The others are optional. +_must_ be specified in `public/config.js`. The others are optional. ### Configuration options @@ -19,7 +19,7 @@ For default values see [Default configuration](#default-configuration). #### Absolutely required values These values do not have defaults and must be specified -in `public/config.yaml`. They are critical to the functioning of the app. +in `public/config.js`. They are critical to the functioning of the app. `appTitle` @@ -208,35 +208,40 @@ of the correct type. ### Default configuration ```yaml -adjustableColumnWidthsDefault: [7, 5] -defaultTab: Filters -defaultNetworkColor: "#000000" -zoomToMarkerRadiusSpec: [[7, 2], [99, 4]] -userDocs: - showLink: false - url: "https://data.pacificclimate.org/portal/docs/" - text: "User Docs" -lethargy: - enabled: true - stability: 7 - sensitivity: 50 - tolerance: 0.05 -disclaimer: - enabled: false - title: "Disclaimer Title" - body: "Disclaimer body ..." - buttonLabel: "Acknowledge" -mapSpinner: - spinner: "Bars" - x: "40%" - y: "40%" - width: "80" - stroke: "darkgray" - fill: "lightgray" -stationDebugFetchOptions: false -stationDebugFetchLimits: [100, 500, 1000, 2000, 4000, 8000] -showReloadStationsButton: false -timingEnabled: false +{ + "adjustableColumnWidthsDefault": [7, 5], + "defaultTab": "Filters", + "defaultNetworkColor": "#000000", + "zoomToMarkerRadiusSpec": [[7, 2], [99, 4]], + "userDocs": + { + "showLink": false, + "url": "https://data.pacificclimate.org/portal/docs/", + "text": "User Docs", + }, + "lethargy": + { "enabled": true, "stability": 7, "sensitivity": 50, "tolerance": 0.05 }, + "disclaimer": + { + "enabled": false, + "title": "Disclaimer Title", + "body": "Disclaimer body ...", + "buttonLabel": "Acknowledge", + }, + "mapSpinner": + { + "spinner": "Bars", + "x": "40%", + "y": "40%", + "width": "80", + "stroke": "darkgray", + "fill": "lightgray", + }, + "stationDebugFetchOptions": false, + "stationDebugFetchLimits": [100, 500, 1000, 2000, 4000, 8000], + "showReloadStationsButton": false, + "timingEnabled": false, +} ``` ### Example custom configuration files @@ -245,94 +250,72 @@ timingEnabled: false Currently targets CRMP database. -```yaml -appTitle: BC Station Data - PCDS -baseMap: BC - -# sdsUrl values will be replaced by dev or prod URLs when they become ready. -# For now, we have demo instances inside the firewall. -# Uses monsoon database -sdsUrl: http://docker-dev02.pcic.uvic.ca:30512 -# Uses new database -#sdsUrl: http://docker-dev02.pcic.uvic.ca:30562 -# Local instance -#sdsUrl: http://localhost:5000 - -# Currently deployed metadata backends do not respond to provinces QP. -# When they do, we can use stationsQpProvinces and lose stationFilters -#stationsQpProvinces: BC -stationFilters: histories[0].province = "BC" - -# Always necessary for CRMP database -networkFilters: name != "PCIC Climate Variables" - -# pdpDataUrl values will be replaced by dev or prod URLs when they become ready. -# For now, we have a demo instance inside the firewall, below. -# Uses monsoon database -pdpDataUrl: http://docker-dev02.pcic.uvic.ca:30514 -# Uses new database -#pdpDataUrl: http://docker-dev02.pcic.uvic.ca:??? - -# Debug params -#stationFilters: histories[0].province = "BC" -#stationDebugFetchOptions: false -#stationDebugFetchLimits: [100, 500, 1000, 2000, 4000, 8000] -#stationOffset: undefined -#stationLimit: undefined -#stationStride: undefined -#showReloadStationsButton: false -#timingEnabled: false +```js +window.env = { + appTitle: "BC Station Data - PCDS", + baseMap: "BC", + // Uses development database + sdsUrl: + "https://beehive.pacificclimate.org/station-data-portal/pcds/metadata", + //sdsUrl: http://localhost:5000, + // Currently deployed metadata backends do not respond to provinces QP. + // When they do, we can use stationsQpProvinces and lose stationFilters + //stationsQpProvinces: "BC", + stationFilters: 'histories[0].province = "BC"', + // Always necessary for CRMP database + networkFilters: 'name != "PCIC Climate Variables"', + // pdpDataUrl values will be replaced by dev or prod URLs when they become ready. + // For now, we have a demo instance inside the firewall, below. + // Uses development database + pdpDataUrl: + "https://beehive.pacificclimate.org/station-data-portal/pcds/data", + // optional Debug params + //stationFilters: histories[0].province = "BC" + //stationDebugFetchOptions: false + //stationDebugFetchLimits: [100, 500, 1000, 2000, 4000, 8000] + //stationOffset: undefined + //stationLimit: undefined + //stationStride: undefined + //showReloadStationsButton: false + //timingEnabled: false +}; ``` #### PCDS data portal -```yaml -appTitle: BC Station Data - PCDS -baseMap: BC - -# sdsUrl values will be replaced by dev or prod URLs when they become ready. -# For now, we have demo instances inside the firewall. -# Uses monsoon database -sdsUrl: http://docker-dev02.pcic.uvic.ca:30512 -# Uses new database -#sdsUrl: http://docker-dev02.pcic.uvic.ca:30562 - -# Currently deployed metadata backends do not respond to provinces QP. -# When they do, we can invert the commenting out below. -#stationsQpProvinces: BC -stationFilters: histories[0].province = "BC" - -# Always necessary for CRMP database -networkFilters: name != "PCIC Climate Variables" - -# pdpDataUrl values will be replaced by dev or prod URLs when they become ready. -# For now, we have a demo instance inside the firewall, below. -# Uses monsoon database -pdpDataUrl: http://docker-dev02.pcic.uvic.ca:30514 -# Uses new database -#pdpDataUrl: http://docker-dev02.pcic.uvic.ca:??? +```js +window.env = { + appTitle: "BC Station Data - PCDS", + baseMap: "BC", + // uses development database + sdsUrl: + "https://beehive.pacificclimate.org/station-data-portal/pcds/metadata", + // Currently deployed metadata backends do not respond to provinces QP. + // When they do, we can invert the commenting out below. + //stationsQpProvinces: "BC", + stationFilters: 'histories[0].province = "BC"', + // Always necessary for CRMP database + networkFilters: 'name != "PCIC Climate Variables"', + pdpDataUrl: + "https://beehive.pacificclimate.org/station-data-portal/pcds/data", +}; ``` #### YNWT data portal -```yaml -appTitle: YNWT Station Data -baseMap: YNWT - -# sdsUrl values will be replaced by prod URLs when they become ready. -# For now, we have a dev instance. -# Uses monsoon database -sdsUrl: https://services.pacificclimate.org/dev/ynwt/meta - -# We do not at present need to filter based on province (verify!) -#stationsQpProvinces: YK,NT -# We do not at present need to filter networks (verify!) -#networkFilters: ??? - -# pdpDataUrl values will be replaced by prod URLs when they become ready. -# For now, we have a dev instance. -# Uses monsoon database -pdpDataUrl: https://services.pacificclimate.org/dev/ynwt/data +```js +window.env = { + appTitle: "YNWT Station Data", + baseMap: "YNWT", + // We do not at present need to filter based on province (verify!) + //stationsQpProvinces: YK,NT + // We do not at present need to filter networks (verify!) + //networkFilters: ??? + sdsUrl: + "https://beehive.pacificclimate.org/station-data-portal/ynwt/metadata", + pdpDataUrl: + "https://beehive.pacificclimate.org/station-data-portal/ynwt/metadata", +}; ``` ### Custom configuration and Docker deployment @@ -344,17 +327,15 @@ For example, in your docker-compose.yaml, include the following mount: ```yaml volumes: - type: bind - source: /path/to/custom/config.yaml - target: /app/public/config.yaml + source: /path/to/custom/config.js + target: /app/config.js read_only: true ``` -Note: We mount to target `/app/public/config.yml` because the app is built -in the Docker _container_, and this file is picked up in the build and -copied to the appropriate build artifact. -If we move to a system in which the app is built in advance in the -Docker _image_, then the target will change, likely to -`/app/build/static/config.yaml`. +Note: We mount to target `/app/config.js` because the app is built and loaded into +this directory. When executed the docker container's entrypoint will substitute any +`%PUBLIC_URL%` references in code (`%REPLACE_PUBLIC_URL%` in the container) with +the values of PUBLIC_URL in the config.js. ## Environment variables @@ -370,17 +351,13 @@ For development runs of the app launched with `npm start`, the files For more details, see the [CRA documentation](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables). -For production runs, environment variables are provided by -`docker-compose.yaml`. - -### Deployment +### Build time variables `PUBLIC_URL` - Base URL for Station Data Portal frontend. -- Type: string. -- For production, set this to the URL configured in our proxy server. -- Required. +- For local development this should match the expected local url (generally http://localhost:3000/) +- For production this will be set to %REPLACE_PUBLIC_URL% and needs to be injected at start time. This happens in the docker container's entrypoint.sh and allows us to configure the sites expected path at run time. `REACT_APP_APP_VERSION` @@ -390,18 +367,6 @@ For production runs, environment variables are provided by - It is not recommended to manually override the automatically generated value when the image is run. - Note doubled `APP_` in name. -`REACT_APP_BC_BASE_MAP_TILES_URL` - -- URL template (includes x, y, z) for BC base map tiles. -- Type: string. -- Required if YAML config.baseMap === "BC" - -`REACT_APP_YNWT_BASE_MAP_TILES_URL` - -- URL template (includes x, y, z) for YNWT base map tiles. -- Type: string. -- Required if YAML config.baseMap === "YNWT" - ## Filtering metadata The app can filter metadata to include only a desired subset of items. diff --git a/docs/developer/dependencies.md b/docs/developer/dependencies.md index 73c64668..5a3385f2 100644 --- a/docs/developer/dependencies.md +++ b/docs/developer/dependencies.md @@ -2,7 +2,7 @@ ## Execution environment -Node.js >= 9.2.0 (**important**) +Node.js >= 16 (**important**) All other package requirements are specified in `package.json`. @@ -10,6 +10,9 @@ We **strongly** recommend using [`nvm`](https://github.com/creationix/nvm) to ma In particular, you will have trouble finding later versions of Node.js in standard Linux installs; `nvm` however is up to date with all recent releases. +Alternatively the .devcontainer setup will contain everything needed to set up this environment to the last verified version +of node used for the project. + Note: Avoid `snap` packages. Experience to date suggests it does not result in stable, reliable installations. ## Backend services diff --git a/docs/developer/development.md b/docs/developer/development.md index bcfd51dc..1f187885 100644 --- a/docs/developer/development.md +++ b/docs/developer/development.md @@ -6,8 +6,9 @@ npm start ``` -This starts the app with environment variables taken from local `.env` -files. Brief summary: +This starts the app with environment variables taken from local `.env` files. Generally env variables should be avoided +In favour of values being placed in the `public/config.js` file as these values are pulled when the code +is built. Brief summary: - `.env`: Global default settings - `.env.development`: Development-specific settings (`npm start`) @@ -15,9 +16,20 @@ files. Brief summary: For more details, see the [CRA documentation](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables). +Take special note of the `.env.*` file priorities when attempting to override for different build types. For building and running a production app, see below. +## the PUBLIC_URL variable + +In line with create react app this variable still needs to be set for correct working of application routers. +In development this will be set to the value in `.env.development` which should be set to the expected url while +executing `npm start`. Generally this will be `http://localhost:3000` by default. + +For production (running `npm run build`) this will be set to `%REPLACE_PUBLIC_URL%` and allow the built +docker container to be able to set this at true run time. See `docker/entrypoint.sh` for the specifics +of this replace implementation. + ## Upgrading `pcic-react-leaflet-components` To get a successful upgrade in your local environment, you must do the diff --git a/docs/developer/production.md b/docs/developer/production.md index c0116f99..fe606561 100644 --- a/docs/developer/production.md +++ b/docs/developer/production.md @@ -10,50 +10,25 @@ builds and tags a Docker image on each commit. The image name is ### Configuration, environment variables, and Docker -It is best practice to configure a web app externally, at run-time, -typically using environment variables for any simple (atomic, e.g., -string) configuration values. - -Here we run into a problem introduced by CRA: -CRA injects environment variables only at _build time_, not at run time. -["The environment variables are embedded during the build time. Since Create React App produces a static -HTML/CSS/JS bundle, it can’t possibly read them at runtime."](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables). - -We deploy our apps with Docker. A natural approach to build (`npm build`) -the app as part of building the image, and then just serve it from a -container. Because of CRA's build-time injection of environment -variables, such Docker images cannot be configured at run-time (i.e., -when a container is run). Only static, build-time environment variables -are available to CRA apps inside such images. - -It takes some extra effort to inject run-time environment variables (or -configuration generally) into Dockerized CRA applications. There are -two basic approaches: - -1. Build the app, and inject run-time environment variables, as part of - the image run (i.e., the commands run by the image include building - the app, which at that point has access to environment variables - provided via the `docker run` command). - -This is simple but it means that containers are slow to start up - and contain a lot of infrastructure (Node.js, etc.) needed to build - the image. This isn't an issue for us, because we don't start many - instances and we don't do it often. - -2. Fetch the environment variables (or a configuration file) from the server. - - This approach has several variants, which are outlined in this - [CRA issue](https://github.com/facebook/create-react-app/issues/2353). - -A key requirement is to be able to configure at run time the the URL at -which the app is deployed. CRA provides a (build-time) environment -variable for this, `PUBLIC_URL`. - -Option 1 makes setting `PUBLIC_URL` simple and requires no change to the codebase; -as noted, we don't care about the cost of using such containers. - -Option 2 makes setting `PUBLIC_URL` _much_ harder to accomplish, and -would require significant changes to the codebase. - -Therefore we use option 1. +It is best practice to configure a web app externally, at +run-time, typically using environment variables for any simple +(atomic, e.g., string) configuration values. + +CRA makes this a little challenging, but we use the following +build flow to allow us to inject this configuration. + +1. Configuration information is stored in `public/config.js`. + This file is mounted in our docker containers with environment + specific configuration options. +2. Avoid use of `process.env`. While convenient, these variables + are build time required so can't be used for environments unless + we build at run time. Building at runtime incurs a significant + time delay, so shouldbe avoided. +3. Use `window.env` (defined in config.js) as an alternative, any + environment specific information is appropriate here. +4. `PUBLIC_URL` is special and is handled using a replacement + during docker container startup. A full explanation can be found + in the [build](./build.md) documentation. ### Deployment @@ -63,11 +38,4 @@ image using `docker-compose`. You may wish to copy and modify `docker-compose.yaml` to construct a production deployment. Note: All **deployment environment variables**, except `REACT_APP_APP_VERSION`, -are provided by `docker/docker-compose.yaml` (and any `*.env` files it -names). `REACT_APP_APP_VERSION` is set during image build (see -`.github/workflows/docker-publish.yml`), and it should not be set -otherwise. - -This is different from **development environment variables**, which are -provided by the `.env.*` files found in the project root directory. -(Also, `REACT_APP_APP_VERSION` is not set for development.) +are provided by `docker/docker-compose.yaml` via the `/app/config.js` file mount. diff --git a/package-lock.json b/package-lock.json index 351192cd..6c93c687 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "station-data-portal", - "version": "1.5.0", + "version": "1.6.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "station-data-portal", - "version": "1.5.0", + "version": "1.6.1", "dependencies": { "@tanstack/react-query": "^5.20.2", "@tanstack/react-table": "^8.12.0", @@ -22,8 +22,8 @@ "lethargy": "^1.0.9", "lodash": "^4.17.21", "memoize-one": "^6.0.0", - "pcic-react-leaflet-components": "git+https://git@github.com/pacificclimate/pcic-react-leaflet-components.git#3.0.1", - "plotly.js-basic-dist": "^2.29.0", + "pcic-react-leaflet-components": "git+https://git@github.com/pacificclimate/pcic-react-leaflet-components.git#tile-url-as-prop", + "plotly.js-basic-dist": "^2.34.0", "prop-types": "^15.8.1", "react": "^18.2.0", "react-bootstrap": "^2.4.0", @@ -5456,38 +5456,6 @@ "node": ">= 4.5.0" } }, - "node_modules/autoprefixer": { - "version": "10.4.7", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.7.tgz", - "integrity": "sha512-ypHju4Y2Oav95SipEcCcI5J7CGPuvz8oat7sUtYj3ClK44bldfvtvcxK6IEK++7rqB7YchDGzweZIBG+SD0ZAA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - } - ], - "dependencies": { - "browserslist": "^4.20.3", - "caniuse-lite": "^1.0.30001335", - "fraction.js": "^4.2.0", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/axe-core": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.4.2.tgz", @@ -6412,9 +6380,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001591", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz", - "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==", + "version": "1.0.30001653", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001653.tgz", + "integrity": "sha512-XGWQVB8wFQ2+9NZwZ10GxTYC5hk0Fa+q8cSkr0tgvMhYhMHP/QC+WTgrePMDBWiWc/pV+1ik82Al20XOK25Gcw==", "funding": [ { "type": "opencollective", @@ -6428,7 +6396,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/canvas-fit": { "version": "1.5.0", @@ -17014,9 +16983,8 @@ } }, "node_modules/pcic-react-leaflet-components": { - "version": "3.1.0", - "resolved": "git+https://git@github.com/pacificclimate/pcic-react-leaflet-components.git#bf88217948ce6fdd8165b8bbbd0d4fc29939aecc", - "integrity": "sha512-uTRXFxWNWM5EUm1yaJnAuFafTUMKg43GZq09sq6hBC7yQEU3/CRdHgJOa/i8N7IRC1LX+RVrh7eW3NrOG/Kp4w==", + "version": "3.2.0", + "resolved": "git+https://git@github.com/pacificclimate/pcic-react-leaflet-components.git#e0a83b1a0da3f7b3d2209dd6679a8cab0819431d", "dependencies": { "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.3.0", @@ -17277,9 +17245,10 @@ } }, "node_modules/plotly.js-basic-dist": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/plotly.js-basic-dist/-/plotly.js-basic-dist-2.29.0.tgz", - "integrity": "sha512-UdPOVN3If7CPXScXcjG9TO2oSLlxvk/Qn1LN7pZOjWWf8zxoM9CWOJWz9NSq2M/0cxuaqovgi65P386CMSE4Yw==" + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/plotly.js-basic-dist/-/plotly.js-basic-dist-2.34.0.tgz", + "integrity": "sha512-0c+/5JcEuNI8+JKXN6xx3Enmmy9ju9wL90VdRIPNObMloxksHjXuXEaXAwN80IIbaUyUlOf4F9pco2H76BRn4A==", + "license": "MIT" }, "node_modules/point-in-polygon": { "version": "1.1.0", @@ -18265,6 +18234,39 @@ "postcss": "^8.2" } }, + "node_modules/postcss-preset-env/node_modules/autoprefixer": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.5.tgz", + "integrity": "sha512-Fvd8yCoA7lNX/OUllvS+aS1I7WRBclGXsepbvT8ZaPgrH24rgXpZzF0/6Hh3ZEkwg+0AES/Osd196VZmYoEFtw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.20.2", + "caniuse-lite": "^1.0.30001332", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/postcss-pseudo-class-any-link": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.5.tgz", diff --git a/package.json b/package.json index b7860554..5ccd2c23 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,8 @@ "lethargy": "^1.0.9", "lodash": "^4.17.21", "memoize-one": "^6.0.0", - "pcic-react-leaflet-components": "git+https://git@github.com/pacificclimate/pcic-react-leaflet-components.git#3.0.1", - "plotly.js-basic-dist": "^2.29.0", + "pcic-react-leaflet-components": "git+https://git@github.com/pacificclimate/pcic-react-leaflet-components.git#tile-url-as-prop", + "plotly.js-basic-dist": "^2.34.0", "prop-types": "^15.8.1", "react": "^18.2.0", "react-bootstrap": "^2.4.0", @@ -41,6 +41,9 @@ "use-immer": "^0.7.0", "zustand": "^4.4.7" }, + "overrides": { + "autoprefixer": "10.4.5" + }, "devDependencies": { "@tanstack/react-query-devtools": "^5.20.2", "craco": "^0.0.3", @@ -61,7 +64,7 @@ }, "scripts": { "start": "craco start", - "build": "craco build", + "build": "REACT_APP_APP_VERSION=$(bash ./generate-commitish.sh) && craco build", "test": "craco test --env=jsdom", "eject": "react-scripts eject", "reinstall": "rm -rf ./node_modules; npm install --dev", diff --git a/public/config.js b/public/config.js new file mode 100644 index 00000000..a3d991cd --- /dev/null +++ b/public/config.js @@ -0,0 +1,60 @@ +window.env = { + PUBLIC_URL: "http://localhost:30503", + REACT_APP_BC_BASE_MAP_TILES_URL: + "https://swarm.pacificclimate.org/tiles/bc-albers-lite/{z}/{x}/{y}.png", + REACT_APP_YNWT_BASE_MAP_TILES_URL: + "https://swarm.pacificclimate.org/tiles/yukon-albers-lite/{z}/{x}/{y}.png", + appTitle: "BC Station Data - PCDS", + baseMap: "BC", + + // sdsUrl values will be replaced by dev or prod URLs when they become ready. + // For now, we have demo instances inside the firewall. + // Uses monsoon database + //sdsUrl: http://docker-dev02.pcic.uvic.ca:30512 + // Uses new database + //sdsUrl: http://docker-dev02.pcic.uvic.ca:30562 + // Local instance + sdsUrl: + "https://beehive.pacificclimate.org/station-data-portal/pcds/metadata/", + + // Currently deployed metadata backends do not respond to provinces QP. + // When they do, we can use stationsQpProvinces and lose stationFilters + //stationsQpProvinces: BC + stationFilters: 'histories[0].province = "BC"', + + // Always necessary for CRMP database + networkFilters: 'name != "PCIC Climate Variables"', + + // pdpDataUrl values will be replaced by dev or prod URLs when they become ready. + // For now, we have a demo instance inside the firewall, below. + // Uses monsoon database + pdpDataUrl: + "https://beehive.pacificclimate.org/station-data-portal/pcds/data/", + // Uses new database + //pdpDataUrl: http://docker-dev02.pcic.uvic.ca:??? + + // Debug params + //stationFilters: histories[0].province = "BC" + //stationDebugFetchOptions: false + //stationDebugFetchLimits: [100, 500, 1000, 2000, 4000, 8000] + //stationOffset: undefined + //stationLimit: undefined + //stationStride: undefined + //showReloadStationsButton: false + //timingEnabled: false + + // Preview Options + plotColor: "#1f77b4", + dataRequestDurations: [1, 3, 6, 12], // months + dataRequestDurationsDefault: 6, // months + + // disclaimer: + // enabled: true + // title: "Disclaimer" + // body: | + // The Pacific Climate Impacts Consortium (PCIC), and all monitoring and or funding partners + // who provide data or support to PCIC or its partners for this portal, take no responsibility + // for the accuracy of this data. Portal users acknowledge that they are using data from the + // portal at their own risk. + // buttonLabel: "Agree" +}; diff --git a/public/config.yaml b/public/config.yaml deleted file mode 100755 index d7e43cba..00000000 --- a/public/config.yaml +++ /dev/null @@ -1,51 +0,0 @@ -appTitle: BC Station Data - PCDS -baseMap: BC - -# sdsUrl values will be replaced by dev or prod URLs when they become ready. -# For now, we have demo instances inside the firewall. -# Uses monsoon database -#sdsUrl: http://docker-dev02.pcic.uvic.ca:30512 -# Uses new database -#sdsUrl: http://docker-dev02.pcic.uvic.ca:30562 -# Local instance -sdsUrl: http://localhost:5000 - -# Currently deployed metadata backends do not respond to provinces QP. -# When they do, we can use stationsQpProvinces and lose stationFilters -#stationsQpProvinces: BC -stationFilters: histories[0].province = "BC" - -# Always necessary for CRMP database -networkFilters: name != "PCIC Climate Variables" - -# pdpDataUrl values will be replaced by dev or prod URLs when they become ready. -# For now, we have a demo instance inside the firewall, below. -# Uses monsoon database -pdpDataUrl: http://docker-dev02.pcic.uvic.ca:30514 -# Uses new database -#pdpDataUrl: http://docker-dev02.pcic.uvic.ca:??? - -# Debug params -#stationFilters: histories[0].province = "BC" -#stationDebugFetchOptions: false -#stationDebugFetchLimits: [100, 500, 1000, 2000, 4000, 8000] -#stationOffset: undefined -#stationLimit: undefined -#stationStride: undefined -#showReloadStationsButton: false -#timingEnabled: false - -# Preview Options -plotColor: "#1f77b4" -dataRequestDurations: [1, 3, 6, 12] # months -dataRequestDurationsDefault: 6 # months - -# disclaimer: -# enabled: true -# title: "Disclaimer" -# body: | -# The Pacific Climate Impacts Consortium (PCIC), and all monitoring and or funding partners -# who provide data or support to PCIC or its partners for this portal, take no responsibility -# for the accuracy of this data. Portal users acknowledge that they are using data from the -# portal at their own risk. -# buttonLabel: "Agree" diff --git a/public/healthcheck.js b/public/healthcheck.js new file mode 100644 index 00000000..640e7879 --- /dev/null +++ b/public/healthcheck.js @@ -0,0 +1 @@ +// app is serving diff --git a/public/index.html b/public/index.html index a32933d7..a7bda29b 100644 --- a/public/index.html +++ b/public/index.html @@ -20,9 +20,12 @@ Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will work correctly both with client-side routing and a non-root public URL. - Learn how to configure a non-root public URL by running `npm run build`. + + See [Development Docs](docs/developer/development.md) for more information about configuring + public URLs for development and build time. --> Met Data Portal + diff --git a/src/components/maps/StationMap.js b/src/components/maps/StationMap.js index dba6c205..5c5add6d 100644 --- a/src/components/maps/StationMap.js +++ b/src/components/maps/StationMap.js @@ -65,6 +65,7 @@ const StationMapRenderer = React.memo( ({ BaseMap, initialViewport, + baseMapTilesUrl, userShapeLayerRef, userShapeStyle, handleChangedGeometryLayers, @@ -122,10 +123,13 @@ const StationMapRenderer = React.memo( const isPending = externalIsPending || markerUpdateIsPending || markerRenderIsPending; + console.log(baseMapTilesUrl); + return ( @@ -195,7 +199,8 @@ const StationMap = ({ const userShapeLayerRef = useRef(); - const { BaseMap, initialViewport } = baseMaps[config.baseMap]; + const { BaseMap, initialViewport, baseMapTilesUrl } = + baseMaps[config.baseMap]; const handleChangedGeometryLayers = useMemo( () => () => { @@ -213,6 +218,7 @@ const StationMap = ({ {...{ BaseMap, initialViewport, + baseMapTilesUrl, userShapeLayerRef, userShapeStyle, handleChangedGeometryLayers, diff --git a/src/components/maps/baseMaps.js b/src/components/maps/baseMaps.js index c5a05b77..8e8e2f58 100644 --- a/src/components/maps/baseMaps.js +++ b/src/components/maps/baseMaps.js @@ -4,9 +4,11 @@ export default { BC: { BaseMap: BCBaseMap, initialViewport: BCBaseMap.initialViewport, + baseMapTilesUrl: window.env.REACT_APP_BC_BASE_MAP_TILES_URL, }, YNWT: { BaseMap: YNWTBaseMap, initialViewport: YNWTBaseMap.initialViewport, + baseMapTilesUrl: window.env.REACT_APP_YNWT_BASE_MAP_TILES_URL, }, }; diff --git a/src/index.js b/src/index.js index 52c8a692..2f03dee3 100644 --- a/src/index.js +++ b/src/index.js @@ -30,8 +30,8 @@ import "./index.css"; * @returns string The base URL of the app */ const getBaseName = () => { - if (process.env.PUBLIC_URL?.indexOf(".") >= 0) { - return new URL(process.env.PUBLIC_URL).pathname; + if (window.env.PUBLIC_URL?.indexOf(".") >= 0) { + return new URL(window.env.PUBLIC_URL).pathname; } return ""; diff --git a/src/state/query-hooks/use-config.js b/src/state/query-hooks/use-config.js index b43fbf01..bf82be25 100644 --- a/src/state/query-hooks/use-config.js +++ b/src/state/query-hooks/use-config.js @@ -89,10 +89,8 @@ const getZoomMarkerRadius = (zmrSpec) => { * @returns {Promise} */ const fetchConfig = async () => { - const response = await fetch(`${process.env.PUBLIC_URL}/config.yaml`); - const yamlConfig = await response.text(); - const fetchedConfig = yaml.load(yamlConfig); - const config = { ...defaultConfig, ...fetchedConfig }; + const loadedConfig = window.env; + const config = { ...defaultConfig, ...loadedConfig }; checkMissingKeys(config);