From c7202eadd2ba771e836f1d25fe37cb418019d517 Mon Sep 17 00:00:00 2001 From: Paul Bienkowski Date: Tue, 14 Sep 2021 22:02:57 +0200 Subject: [PATCH] frontend: load config.json at runtime from public dir, to be overwritable --- .gitignore | 2 - docker-compose.yaml | 10 +++- frontend/{src => }/config.dev.json | 0 ...onfig.json.example => config.example.json} | 0 frontend/public/config.json | 0 frontend/src/App.js | 7 +-- frontend/src/api.js | 7 ++- frontend/src/components/Map/index.js | 31 +++++++----- frontend/src/config.ts | 26 ++++++++++ frontend/src/pages/SettingsPage.tsx | 5 +- frontend/src/pages/TrackPage/TrackMap.tsx | 1 - frontend/src/pages/UploadPage.tsx | 49 +++++++++++-------- 12 files changed, 96 insertions(+), 42 deletions(-) rename frontend/{src => }/config.dev.json (100%) rename frontend/{src/config.json.example => config.example.json} (100%) create mode 100755 frontend/public/config.json create mode 100644 frontend/src/config.ts diff --git a/.gitignore b/.gitignore index 7665e26..b29e1cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ local -config.json -src/config.json data diff --git a/docker-compose.yaml b/docker-compose.yaml index b039ccf..9f824b3 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,3 +1,9 @@ +# This docker-compose file is intended for development use. You can simply run +# `docker-compose up -d` in the repository and it should build and run all +# required parts of the application. See README.md for details. +# +# For a production docker setup, please check the corresponding documentation. + version: '3' services: @@ -75,7 +81,9 @@ services: - ./frontend/public:/opt/obs/frontend/public - ./frontend/tsconfig.json:/opt/obs/frontend/tsconfig.json - ./frontend/package.json:/opt/obs/frontend/package.json - - ./frontend/src/config.dev.json:/opt/obs/frontend/src/config.json + + # Overwrite the default config with the development mode configuration + - ./frontend/config.dev.json:/opt/obs/frontend/public/config.json environment: - PORT=3000 links: diff --git a/frontend/src/config.dev.json b/frontend/config.dev.json similarity index 100% rename from frontend/src/config.dev.json rename to frontend/config.dev.json diff --git a/frontend/src/config.json.example b/frontend/config.example.json similarity index 100% rename from frontend/src/config.json.example rename to frontend/config.example.json diff --git a/frontend/public/config.json b/frontend/public/config.json new file mode 100755 index 0000000..e69de29 diff --git a/frontend/src/App.js b/frontend/src/App.js index e74e597..4ef6842 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -6,7 +6,7 @@ import {useObservable} from 'rxjs-hooks' import {from} from 'rxjs' import {pluck} from 'rxjs/operators' -import config from 'config.json' +import {useConfig} from 'config' import styles from './App.module.scss' import { @@ -38,6 +38,7 @@ function DropdownItemForLink({navigate, ...props}) { } const App = connect((state) => ({login: state.login}))(function App({login}) { + const config = useConfig() const apiVersion = useObservable(() => from(api.get('/info')).pipe(pluck('version'))) return ( @@ -148,12 +149,12 @@ const App = connect((state) => ({login: state.login}))(function App({login}) {
This installation
- + Privacy policy - + Imprint diff --git a/frontend/src/api.js b/frontend/src/api.js index 0928c7a..d949bbd 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -2,7 +2,7 @@ import {stringifyParams} from 'query' import globalStore from 'store' import {setAuth, invalidateAccessToken, resetAuth} from 'reducers/auth' import {setLogin} from 'reducers/login' -import config from 'config.json' +import configPromise from 'config' import {create as createPkce} from 'pkce' import download from 'downloadjs' @@ -39,6 +39,7 @@ class API { * it supports PKCE. */ async getAuthorizationServerMetadata() { + const config = await configPromise const url = new URL(config.auth.server) const pathSuffix = url.pathname.replace(/^\/+|\/+$/, '') url.pathname = '/.well-known/oauth-authorization-server' + (pathSuffix ? '/' + pathSuffix : '') @@ -117,6 +118,7 @@ class API { // Try to use the refresh token const {tokenEndpoint} = await this.getAuthorizationServerMetadata() + const config = await configPromise const url = new URL(tokenEndpoint) url.searchParams.append('refresh_token', refreshToken) url.searchParams.append('grant_type', 'refresh_token') @@ -142,6 +144,7 @@ class API { } const {tokenEndpoint} = await this.getAuthorizationServerMetadata() + const config = await configPromise const url = new URL(tokenEndpoint) url.searchParams.append('code', code) url.searchParams.append('grant_type', 'authorization_code') @@ -178,6 +181,7 @@ class API { async makeLoginUrl() { const {authorizationEndpoint} = await this.getAuthorizationServerMetadata() + const config = await configPromise const {codeVerifier, codeChallenge} = createPkce() localStorage.setItem('codeVerifier', codeVerifier) @@ -197,6 +201,7 @@ class API { async fetch(url, options = {}) { const accessToken = await this.getValidAccessToken() + const config = await configPromise const {returnResponse = false, ...fetchOptions} = options diff --git a/frontend/src/components/Map/index.js b/frontend/src/components/Map/index.js index 9b98ddb..e770a55 100644 --- a/frontend/src/components/Map/index.js +++ b/frontend/src/components/Map/index.js @@ -13,7 +13,7 @@ import {fromLonLat} from 'ol/proj' // Import styles for open layers + addons import 'ol/ol.css' -import config from 'config.json' +import {useConfig} from 'config' // Prepare projection proj4.defs( @@ -86,6 +86,11 @@ export function TileLayer({osm, ...props}) { } export function BaseLayer(props) { + const config = useConfig() + if (!config) { + return null + } + return ( - new OlView({ + () => { + if (!config) return null + + const minZoom = config.mapTileset?.minZoom ?? 0 + const maxZoom = config.mapTileset?.maxZoom ?? 18 + const mapHomeZoom = config.mapHome?.zoom ?? 15 + const mapHomeLongitude = config.mapHome?.longitude ?? 9.1797 + const mapHomeLatitude = config.mapHome?.latitude ?? 48.7784 + + return new OlView({ minZoom, maxZoom, zoom: Math.max(Math.min(mapHomeZoom, maxZoom), minZoom), center: fromLonLat([mapHomeLongitude, mapHomeLatitude]), ...options, - }), + }) + }, // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [config] ) React.useEffect(() => { diff --git a/frontend/src/config.ts b/frontend/src/config.ts new file mode 100644 index 0000000..505c1cc --- /dev/null +++ b/frontend/src/config.ts @@ -0,0 +1,26 @@ +import React from 'react' + +interface Config { + apiUrl: string +} + +async function loadConfig(): Promise { + const response = await fetch('./config.json') + const config = await response.json() + return config +} + +let _configPromise: Promise = loadConfig() +let _configCache: null | Config = null + +export function useConfig() { + const [config, setConfig] = React.useState(_configCache) + React.useEffect(() => { + if (!_configCache) { + _configPromise.then(setConfig) + } + }, []) + return config +} + +export default _configPromise diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 5e07f14..daf3116 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -7,7 +7,7 @@ import {setLogin} from 'reducers/login' import {Page, Stats} from 'components' import api from 'api' import {findInput} from 'utils' -import config from 'config.json' +import {useConfig} from 'config' const SettingsPage = connect((state) => ({login: state.login}), {setLogin})(function SettingsPage({login, setLogin}) { const {register, handleSubmit} = useForm() @@ -102,6 +102,7 @@ function CopyInput({value, ...props}) { const selectField = findInput((ref) => ref?.select()) function ApiKeyDialog({login}) { + const config = useConfig() const [show, setShow] = React.useState(false) const onClick = React.useCallback( (e) => { @@ -131,7 +132,7 @@ function ApiKeyDialog({login}) { )}

The API URL should be set to:

- + ) } diff --git a/frontend/src/pages/TrackPage/TrackMap.tsx b/frontend/src/pages/TrackPage/TrackMap.tsx index 683407b..d7732d6 100644 --- a/frontend/src/pages/TrackPage/TrackMap.tsx +++ b/frontend/src/pages/TrackPage/TrackMap.tsx @@ -7,7 +7,6 @@ import {Fill, Stroke, Style, Text, Circle} from 'ol/style' import {Map} from 'components' import type {TrackData, TrackPoint} from 'types' -import config from 'config.json' const isValidTrackPoint = (point: TrackPoint): boolean => { const longitude = point.geometry?.coordinates?.[0] diff --git a/frontend/src/pages/UploadPage.tsx b/frontend/src/pages/UploadPage.tsx index 346fa1f..94ab4c5 100644 --- a/frontend/src/pages/UploadPage.tsx +++ b/frontend/src/pages/UploadPage.tsx @@ -6,7 +6,7 @@ import {Link} from 'react-router-dom' import {FileUploadField, Page} from 'components' import type {Track} from 'types' import api from 'api' -import config from '../config.json' +import configPromise from 'config' function isSameFile(a: File, b: File) { return a.name === b.name && a.size === b.size @@ -56,34 +56,41 @@ export function FileUploadStatus({ React.useEffect( () => { - const formData = new FormData() - formData.append('body', file) + let xhr - const xhr = new XMLHttpRequest() + async function _work() { + const formData = new FormData() + formData.append('body', file) - const onProgress = (e) => { - const progress = (e.loaded || 0) / (e.total || 1) - setProgress(progress) - } + xhr = new XMLHttpRequest() - const onLoad = (e) => { - onComplete(id, xhr.response) - } + const onProgress = (e) => { + const progress = (e.loaded || 0) / (e.total || 1) + setProgress(progress) + } - xhr.responseType = 'json' - xhr.onload = onLoad - xhr.upload.onprogress = onProgress - if (slug) { - xhr.open('PUT', `${config.apiUrl}/api/tracks/${slug}`) - } else { - xhr.open('POST', `${config.apiUrl}/api/tracks`) - } + const onLoad = (e) => { + onComplete(id, xhr.response) + } + + xhr.responseType = 'json' + xhr.onload = onLoad + xhr.upload.onprogress = onProgress + + const config = await configPromise + if (slug) { + xhr.open('PUT', `${config.apiUrl}/api/tracks/${slug}`) + } else { + xhr.open('POST', `${config.apiUrl}/api/tracks`) + } + + const accessToken = await api.getValidAccessToken() - api.getValidAccessToken().then((accessToken) => { xhr.setRequestHeader('Authorization', accessToken) xhr.send(formData) - }) + } + _work() return () => xhr.abort() }, // eslint-disable-next-line react-hooks/exhaustive-deps