diff --git a/webapp/README.md b/webapp/README.md index f8be376..7294676 100644 --- a/webapp/README.md +++ b/webapp/README.md @@ -33,9 +33,38 @@ These files are used to simplify the configuration of the app and should not con Run the project locally by copying the `.env` to `.env.local` and setting the following environment variables: +| | | +| ----------------- | --------------------------------------- | +| `APP_TITLE` | Application title (For meta tags) | +| `APP_DESCRIPTION` | Application description (For meta tags) | +| `PUBLIC_URL` | Full url for the app | +| `MAPBOX_TOKEN` | Mapbox token | +| `STAC_API` | STAC API endpoint | +| `TILER_API` | TILER API endpoint | + +### Runtime configuration +It is possible to change some configuration after the app is built by providing the configuration via the `app_config.js` file. + +The file should be placed in the root of the `dist` directory and should contain a single object: + +```js +window.__APP_CONFIG__ = { + {{VARIABLE}}: {{value}} +}; +``` +A JSON object can also be used but needs to be converted to an object. + +```js +window.__APP_CONFIG__ = JSON.parse('{{JSON_STRING}}'); +``` + +The following variables can be changed at runtime, while the other ones are needed during the build process: +| | | +| -------------- | ------------------ | +| `MAPBOX_TOKEN` | Mapbox token | +| `STAC_API` | STAC API endpoint | +| `TILER_API` | TILER API endpoint | -| --- | --- | -| `{{VARIABLE}}` | {{description}} | ### Starting the app diff --git a/webapp/app/components/aois/index.tsx b/webapp/app/components/aois/index.tsx index 5c9204f..a749458 100644 --- a/webapp/app/components/aois/index.tsx +++ b/webapp/app/components/aois/index.tsx @@ -23,7 +23,7 @@ import Map, { Layer, Source } from 'react-map-gl'; import { CollecticonIsoStack } from '@devseed-ui/collecticons-chakra'; import debounce from 'lodash.debounce'; import booleanPointInPolygon from '@turf/boolean-point-in-polygon'; -import { add, eachDayOfInterval, isWithinInterval } from 'date-fns'; +import { add, eachDayOfInterval } from 'date-fns'; import { format } from 'date-fns/format.js'; import { useSettings } from './settings'; @@ -32,12 +32,12 @@ import { DataIndicator, IndicatorLegend } from './data-indicator'; import { PointStats } from './point-stats'; import { ShareOptions } from './share-options'; -import { AreaTitle } from '$components/common/area-title'; +import config from '$utils/config'; import { IndicatorProperties, - FeatureProperties, - IndicatorDataRaw + FeatureProperties } from '$utils/loaders'; +import { AreaTitle } from '$components/common/area-title'; import { FloatBox } from '$components/common/shared'; import { Timeline } from '$components/common/timeline'; import { DatePicker } from '$components/common/calendar'; @@ -73,7 +73,6 @@ const dataFetcher = new DataFetcher(); interface LakesLoaderData { lake: Feature; indicators: IndicatorProperties[]; - indicatorData: IndicatorDataRaw[]; } export function Component() { @@ -105,11 +104,10 @@ export function Component() { } > - {({ lake, indicators, indicatorData }) => ( + {({ lake, indicators }) => ( )} @@ -120,7 +118,7 @@ export function Component() { Component.displayName = 'LakesComponent'; function LakesSingle(props: LakesLoaderData) { - const { lake, indicators, indicatorData } = props; + const { lake, indicators } = props; const [params, setSearchParams] = useSearchParams(); const ind = params.get('ind'); @@ -176,7 +174,7 @@ function LakesSingle(props: LakesLoaderData) { daysToRequest.forEach((day) => { dataFetcher.fetchData({ key: ['lakes', lake.properties.idhidro, day.toISOString()], - url: `${process.env.STAC_API}/collections/whis-lakes-labelec-scenes-c2rcc/items/${lake.properties.idhidro}_${format(day, 'yyyyMMdd')}` + url: `${config.STAC_API}/collections/whis-lakes-labelec-scenes-c2rcc/items/${lake.properties.idhidro}_${format(day, 'yyyyMMdd')}` }); }); }, 500), @@ -261,7 +259,7 @@ function LakesSingle(props: LakesLoaderData) { [lake] ); - const lakeIndicatorTileUrl = `${process.env.TILER_API}/collections/whis-lakes-labelec-scenes-c2rcc/items/${itemId}/tiles/{z}/{x}/{y}?assets=${activeIndicator.id}&rescale=${[valueMin, valueMax].join(',')}&colormap_name=${colorName}`; + const lakeIndicatorTileUrl = `${config.TILER_API}/collections/whis-lakes-labelec-scenes-c2rcc/items/${itemId}/tiles/{z}/{x}/{y}?assets=${activeIndicator.id}&rescale=${[valueMin, valueMax].join(',')}&colormap_name=${colorName}`; return ( <> @@ -284,10 +282,7 @@ function LakesSingle(props: LakesLoaderData) { pointerEvents='all' /> - + )} /> @@ -301,7 +296,7 @@ function LakesSingle(props: LakesLoaderData) { onOptionChange={onMapOptionChange} /> { - const days = indicatorData - .filter((d) => - isWithinInterval(d.date, { - start: firstDay, - end: lastDay - }) - ) - .map((d) => d.date); - - return Promise.resolve(days); - }, - [indicatorData] - )} + // getAllowedDays={useCallback( + // ({ firstDay, lastDay }) => { + // const days = indicatorData + // .filter((d) => + // isWithinInterval(d.date, { + // start: firstDay, + // end: lastDay + // }) + // ) + // .map((d) => d.date); + // return Promise.resolve(days); + // }, + // [] + // )} /> { @@ -86,6 +87,7 @@ export function MapOptionsDrawer(props: MapOptionsDrawerProps) { Processed image { @@ -104,6 +106,7 @@ export function MapOptionsDrawer(props: MapOptionsDrawerProps) { Terrain elevation { diff --git a/webapp/app/components/aois/point-stats.tsx b/webapp/app/components/aois/point-stats.tsx index ccc134e..2988c60 100644 --- a/webapp/app/components/aois/point-stats.tsx +++ b/webapp/app/components/aois/point-stats.tsx @@ -7,6 +7,8 @@ import { format } from 'date-fns/format.js'; import { PointStatsPopover } from './point-stats-popover'; +import config from '$utils/config'; + const markerPulse = keyframes` 0% { opacity: 0; @@ -54,7 +56,7 @@ export function PointStats(props: PointStatsProps) { setData({ state: 'loading', data: null }); const cogUrl = getCogUrl(lakeId, indicatorId, date); const stats = await axios.get( - `${process.env.TILER_API}/cog/point/${lngLat?.join(',')}?url=${cogUrl}` + `${config.TILER_API}/cog/point/${lngLat?.join(',')}?url=${cogUrl}` ); const prevMeasurement = await searchPrevMeasurement( lakeId, @@ -163,7 +165,7 @@ async function searchPrevMeasurement( }; const candidatesData = await axios.post( - `${process.env.STAC_API}/search`, + `${config.STAC_API}/search`, candidateQuery ); @@ -176,7 +178,7 @@ async function searchPrevMeasurement( const cogUrl = getCogUrl(lakeId, indicatorId, date); try { const stats = await axios.get( - `${process.env.TILER_API}/cog/point/${lngLat?.join(',')}?url=${cogUrl}` + `${config.TILER_API}/cog/point/${lngLat?.join(',')}?url=${cogUrl}` ); return { date, diff --git a/webapp/app/components/aois/share-options.tsx b/webapp/app/components/aois/share-options.tsx index 9bee0f5..8f8e0de 100644 --- a/webapp/app/components/aois/share-options.tsx +++ b/webapp/app/components/aois/share-options.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { - Button, + // Button, Flex, Heading, IconButton, @@ -19,72 +19,71 @@ import { } from '@devseed-ui/collecticons-chakra'; import { CopyField } from '$components/common/copy-field'; -import { IndicatorDataRaw } from '$utils/loaders'; +// import { IndicatorDataRaw } from '$utils/loaders'; -const indicatorData2CSV = (data) => { - const columns = [ - { id: 'date', fn: (d) => d.date.toISOString() }, - 'percent_valid_in_water_body', - 'chlorophyll.mean', - 'chlorophyll.stddev', - 'chlorophyll.minimum', - 'chlorophyll.maximum', - 'tsm.mean', - 'tsm.stddev', - 'tsm.minimum', - 'tsm.maximum' - ]; +// const indicatorData2CSV = (data) => { +// const columns = [ +// { id: 'date', fn: (d) => d.date.toISOString() }, +// 'percent_valid_in_water_body', +// 'chlorophyll.mean', +// 'chlorophyll.stddev', +// 'chlorophyll.minimum', +// 'chlorophyll.maximum', +// 'tsm.mean', +// 'tsm.stddev', +// 'tsm.minimum', +// 'tsm.maximum' +// ]; - const header = columns - .map((c) => (typeof c === 'string' ? c : c.id)) - .join(','); +// const header = columns +// .map((c) => (typeof c === 'string' ? c : c.id)) +// .join(','); - const rows = data.map((row) => { - const values = columns.map((col) => { - const getter = - typeof col === 'string' - ? // quick get value from nested object - (r) => col.split('.').reduce((acc, segment) => acc[segment], r) - : col.fn; - return getter(row); - }); - return values.join(','); - }); +// const rows = data.map((row) => { +// const values = columns.map((col) => { +// const getter = +// typeof col === 'string' +// ? // quick get value from nested object +// (r) => col.split('.').reduce((acc, segment) => acc[segment], r) +// : col.fn; +// return getter(row); +// }); +// return values.join(','); +// }); - const csv = `${header}\n${rows.join('\n')}`; - return csv; -}; +// const csv = `${header}\n${rows.join('\n')}`; +// return csv; +// }; -// Function to handle the download action -const handleDownload = (data) => { - // Generate CSV content from data - const csvContent = indicatorData2CSV(data); +// // Function to handle the download action +// const handleDownload = (data) => { +// // Generate CSV content from data +// const csvContent = indicatorData2CSV(data); - // Convert the CSV content to a Blob - const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); +// // Convert the CSV content to a Blob +// const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); - // Create a URL for the Blob - const url = URL.createObjectURL(blob); +// // Create a URL for the Blob +// const url = URL.createObjectURL(blob); - // Create an anchor element and trigger the download - const link = document.createElement('a'); - link.href = url; - link.setAttribute('download', 'data.csv'); // Name the file here - document.body.appendChild(link); - link.click(); +// // Create an anchor element and trigger the download +// const link = document.createElement('a'); +// link.href = url; +// link.setAttribute('download', 'data.csv'); // Name the file here +// document.body.appendChild(link); +// link.click(); - // Clean up by revoking the Blob URL and removing the anchor element - URL.revokeObjectURL(url); - link.remove(); -}; +// // Clean up by revoking the Blob URL and removing the anchor element +// URL.revokeObjectURL(url); +// link.remove(); +// }; interface ShareOptionsProps { - indicatorData: IndicatorDataRaw[]; tileEndpoint: string; } export function ShareOptions(props: ShareOptionsProps) { - const { indicatorData, tileEndpoint } = props; + const { tileEndpoint } = props; return ( @@ -93,6 +92,7 @@ export function ShareOptions(props: ShareOptionsProps) { aria-label='Indicator settings' size='xs' variant='ghost' + colorScheme='base' icon={} ml='auto' /> @@ -109,14 +109,6 @@ export function ShareOptions(props: ShareOptionsProps) { > Share - - - Export - - - Tile endpoint diff --git a/webapp/app/components/common/area-title.tsx b/webapp/app/components/common/area-title.tsx index bf39213..4ef2bdf 100644 --- a/webapp/app/components/common/area-title.tsx +++ b/webapp/app/components/common/area-title.tsx @@ -26,7 +26,7 @@ export function AreaTitle(props: AreaTitleProps) { {!isRoot && } {name} {!!volume && ( - + {volume} hm3 )} diff --git a/webapp/app/components/common/calendar/index.tsx b/webapp/app/components/common/calendar/index.tsx index ab171d5..62c03e4 100644 --- a/webapp/app/components/common/calendar/index.tsx +++ b/webapp/app/components/common/calendar/index.tsx @@ -115,6 +115,7 @@ export function DatePicker(props: DatePickerProps) { size='md' variant='ghost' borderRadius='md' + colorScheme='base' textTransform='none' rightIcon={} isActive={isOpen} diff --git a/webapp/app/components/common/logo.tsx b/webapp/app/components/common/logo.tsx index 65da4e0..e7afa2c 100644 --- a/webapp/app/components/common/logo.tsx +++ b/webapp/app/components/common/logo.tsx @@ -1,9 +1,7 @@ import React from 'react'; -import { chakra, ChakraProps, Flex, Text, useToken } from '@chakra-ui/react'; +import { chakra, ChakraProps, Flex, Text } from '@chakra-ui/react'; export default function Logo(props: ChakraProps) { - const [colorPrimary] = useToken('colors', ['primary.500']); - return ( - Poldre + Polder ); diff --git a/webapp/app/components/common/slider.tsx b/webapp/app/components/common/slider.tsx index 72240a4..61315f4 100644 --- a/webapp/app/components/common/slider.tsx +++ b/webapp/app/components/common/slider.tsx @@ -30,6 +30,7 @@ export function Slider(props: { variant='ghost' size='sm' borderRadius='md' + colorScheme='base' aria-label='Show preview dates' onClick={() => actions.stepTo(value - DAY_WIDTH)} > @@ -59,6 +60,7 @@ export function Slider(props: { variant='ghost' size='sm' borderRadius='md' + colorScheme='base' aria-label='Show next dates' onClick={() => actions.stepTo(value + DAY_WIDTH)} > diff --git a/webapp/app/components/home/index.tsx b/webapp/app/components/home/index.tsx index b5f8cce..f0465b1 100644 --- a/webapp/app/components/home/index.tsx +++ b/webapp/app/components/home/index.tsx @@ -19,6 +19,7 @@ import { Layer, Map, Source, useMap } from 'react-map-gl'; import centroid from '@turf/centroid'; import bbox from '@turf/bbox'; +import config from '$utils/config'; import SmartLink from '$components/common/smart-link'; import { FeatureProperties } from '$utils/loaders'; import { AreaTitle } from '$components/common/area-title'; @@ -49,7 +50,7 @@ export function Component() { logotype + diff --git a/webapp/app/utils/config.ts b/webapp/app/utils/config.ts new file mode 100644 index 0000000..ac51a26 --- /dev/null +++ b/webapp/app/utils/config.ts @@ -0,0 +1,8 @@ +export default { + MAPBOX_TOKEN: process.env.MAPBOX_TOKEN, + STAC_API: process.env.STAC_API, + TILER_API: process.env.TILER_API, + + /* @ts-expect-error __APP_CONFIG__ is the global config injected data */ + ...window.__APP_CONFIG__ +}; diff --git a/webapp/app/utils/loaders.ts b/webapp/app/utils/loaders.ts index a16118d..7d6a069 100644 --- a/webapp/app/utils/loaders.ts +++ b/webapp/app/utils/loaders.ts @@ -1,8 +1,11 @@ import axios from 'axios'; import { Feature, FeatureCollection, MultiPolygon } from 'geojson'; import { defer } from 'react-router-dom'; + import { round } from './format'; +import config from '$utils/config'; + interface StacSearchResponse extends FeatureCollection { context: { @@ -69,7 +72,7 @@ export interface IndicatorProperties { export async function requestLakes() { const dataPromise = async () => { const data = await axios.get( - `${process.env.STAC_API}/collections/whis-lakes-labelec-features-c2rcc/items?limit=100` + `${config.STAC_API}/collections/whis-lakes-labelec-features-c2rcc/items?limit=100` ); return data.data; @@ -88,7 +91,7 @@ export async function requestSingleLake({ const dataPromise = async () => { try { const { data: lakeData } = await axios.get( - `${process.env.STAC_API}/collections/whis-lakes-labelec-features-c2rcc/items/${params.id}`, + `${config.STAC_API}/collections/whis-lakes-labelec-features-c2rcc/items/${params.id}`, { signal: request.signal } @@ -119,7 +122,6 @@ export async function requestSingleLake({ return { lake: lakeData, - indicatorData: values, indicators: [ { id: 'chlorophyll', diff --git a/webapp/package.json b/webapp/package.json index 4b622c4..5c34542 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -100,7 +100,8 @@ "use-pan-and-zoom": "^0.6.5" }, "parcelIgnore": [ - ".*/meta/" + ".*/meta/", + ".*/app_config.js" ], "alias": { "$components": "~/app/components", diff --git a/webapp/static/app_config.js b/webapp/static/app_config.js new file mode 100644 index 0000000..f0c7eb3 --- /dev/null +++ b/webapp/static/app_config.js @@ -0,0 +1 @@ +window.__APP_CONFIG__ = {}; \ No newline at end of file