diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 096ce70e42..a26c23d1e1 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -14,6 +14,7 @@ jobs: os: - ubuntu-latest - windows-latest + max-parallel: 2 name: frontend_tests runs-on: ${{ matrix.os }} @@ -21,21 +22,21 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Use Node.js 16.x + - name: Use Node.js 20.x uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 20 - - uses: actions/cache@v2 + - uses: actions/cache@v4 id: yarn-cache with: path: | **/node_modules **/.eslintcache ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-2024-2-${{ hashFiles('**/frontend/yarn.lock') }}-${{ hashFiles('**/common/yarn.lock') }} + key: ${{ runner.os }}-yarn-2024-7-${{ hashFiles('**/frontend/yarn.lock') }}-${{ hashFiles('**/common/yarn.lock') }} restore-keys: | - ${{ runner.os }}-yarn-2024-2 + ${{ runner.os }}-yarn-2024-7 - name: Install dependencies if needed. if: steps.yarn-cache.outputs.cache-hit != 'true' @@ -45,10 +46,12 @@ jobs: yarn setup:common - name: Lint - run: cd frontend && yarn lint + run: cd frontend && yarn lint:ci - name: Test - run: cd frontend && yarn test + run: | + cd frontend + yarn test - name: JSON check run: cd frontend && yarn prettier:json-check @@ -81,21 +84,21 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Use Node.js 16.x + - name: Use Node.js 20.x uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 20 - - uses: actions/cache@v2 + - uses: actions/cache@v4 id: yarn-cache with: path: | **/node_modules **/.eslintcache ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-2024-${{ hashFiles('**/frontend/yarn.lock') }}-${{ hashFiles('**/common/yarn.lock') }} + key: ${{ runner.os }}-yarn-2024-7-${{ hashFiles('**/frontend/yarn.lock') }}-${{ hashFiles('**/common/yarn.lock') }} restore-keys: | - ${{ runner.os }}-yarn-2024 + ${{ runner.os }}-yarn-2024-7 - name: Install dependencies if needed. if: steps.yarn-cache.outputs.cache-hit != 'true' @@ -151,16 +154,16 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Use Node.js 16.x + - name: Use Node.js 20.x uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 20 - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v2 + - uses: actions/cache@v4 id: yarn-cache with: path: | diff --git a/api/app/report.py b/api/app/report.py index 6db33784a8..aeb6fb850c 100644 --- a/api/app/report.py +++ b/api/app/report.py @@ -3,7 +3,6 @@ from typing import Final, Optional from urllib.parse import parse_qs, urlparse -import pytest from app.caching import CACHE_DIRECTORY from playwright.async_api import async_playwright, expect diff --git a/common/src/base/index.ts b/common/src/base/index.ts index 6b64f44795..aa25c5a850 100644 --- a/common/src/base/index.ts +++ b/common/src/base/index.ts @@ -25,7 +25,7 @@ export class Base { fetch?: any; service?: string; version?: string; - } = { fetch: undefined } + } = { fetch: undefined }, ) { this.debug = debug; this.fetch = customFetch || fetch; @@ -67,13 +67,13 @@ export class Base { async getLayerNames(): Promise { throw new Error( - `${this.constructor.name} does not implement getLayerNames` + `${this.constructor.name} does not implement getLayerNames`, ); } async hasLayerId( layerId: string, - options?: Parameters[2] + options?: Parameters[2], ): Promise { return hasLayerId(await this.getLayerIds(), layerId, options); } diff --git a/common/src/base/test.ts b/common/src/base/test.ts index a69cdd33ea..ae2982025b 100644 --- a/common/src/base/test.ts +++ b/common/src/base/test.ts @@ -14,7 +14,7 @@ test("Base.getCapabilities", async ({ eq }) => { { fetch, service: "WFS", - } + }, ); const xml3 = await ows.getCapabilities({ debug: true }); const xml4 = await ows.getCapabilities({ debug: true, version: "2.0.0" }); diff --git a/common/src/gml/index.ts b/common/src/gml/index.ts index 1ed490a2aa..240329751c 100644 --- a/common/src/gml/index.ts +++ b/common/src/gml/index.ts @@ -2,7 +2,7 @@ import { findTagByName } from "xml-utils"; import { findTagText } from "../utils"; export function parseEnvelope( - xml: string + xml: string, ): Readonly<[number, number, number, number]> { const lowerCorner = findTagText(xml, "gml:lowerCorner"); const upperCorner = findTagText(xml, "gml:upperCorner"); @@ -15,7 +15,7 @@ export function parseEnvelope( } export function findAndParseEnvelope( - xml: string + xml: string, ): Readonly<[number, number, number, number]> | undefined { const envelope = findTagByName(xml, "gml:Envelope")?.outer; return envelope ? parseEnvelope(envelope) : undefined; diff --git a/common/src/ows/capabilities.ts b/common/src/ows/capabilities.ts index 5581c08f4d..99ee584b5c 100644 --- a/common/src/ows/capabilities.ts +++ b/common/src/ows/capabilities.ts @@ -11,7 +11,7 @@ export function getCapabilitiesUrl( params?: { [k: string]: number | string }; service?: string; version?: string; - } = { service: undefined, version: undefined } + } = { service: undefined, version: undefined }, ) { try { const { origin, pathname, searchParams } = new URL(url); @@ -37,7 +37,7 @@ export function getCapabilitiesUrl( return formatUrl(base, paramsObj); } catch (error) { throw Error( - `getCapabilitiesUrl failed to parse "${url}" because of the following error:\n${error}` + `getCapabilitiesUrl failed to parse "${url}" because of the following error:\n${error}`, ); } } @@ -57,7 +57,7 @@ export async function getCapabilities( service?: string; version?: string; wait?: number; - } = {} + } = {}, ): Promise { const run = async () => { const capabilitiesUrl = getCapabilitiesUrl(url, { @@ -69,7 +69,7 @@ export async function getCapabilities( if (response.status !== 200) { throw new Error( - `fetch failed for "${capabilitiesUrl}" returning a status code of ${response.status}` + `fetch failed for "${capabilitiesUrl}" returning a status code of ${response.status}`, ); } @@ -78,7 +78,7 @@ export async function getCapabilities( const exception = findException(xml); if (exception) { throw new Error( - `fetch to "${capabilitiesUrl}" returned the following exception: "${exception}"` + `fetch to "${capabilitiesUrl}" returned the following exception: "${exception}"`, ); } diff --git a/common/src/ows/find-and-parse-bbox.ts b/common/src/ows/find-and-parse-bbox.ts index 9c0bd4cc05..8664e9e8de 100644 --- a/common/src/ows/find-and-parse-bbox.ts +++ b/common/src/ows/find-and-parse-bbox.ts @@ -2,7 +2,7 @@ import { findTagText } from "../utils"; import parseBoundingBox from "./parse-bbox"; export default function findAndParseBoundingBox( - xml: string + xml: string, ): Readonly<[number, number, number, number]> | undefined { const bbox = findTagText(xml, "ows:BoundingBox"); return bbox ? parseBoundingBox(bbox) : undefined; diff --git a/common/src/ows/find-and-parse-operation-url.ts b/common/src/ows/find-and-parse-operation-url.ts index 009caea4b5..f4fb7c31a0 100644 --- a/common/src/ows/find-and-parse-operation-url.ts +++ b/common/src/ows/find-and-parse-operation-url.ts @@ -6,7 +6,7 @@ import { titlecase } from "../utils"; export default function findAndParseOperationUrl( xml: string, op: string, - method: "GET" | "POST" | "Get" | "Post" = "Get" + method: "GET" | "POST" | "Get" | "Post" = "Get", ): string | undefined { const xmlOp = findOperation(xml, op); if (!xmlOp) { diff --git a/common/src/ows/find-and-parse-wgs84-bbox.ts b/common/src/ows/find-and-parse-wgs84-bbox.ts index a228832d9d..47a358d37a 100644 --- a/common/src/ows/find-and-parse-wgs84-bbox.ts +++ b/common/src/ows/find-and-parse-wgs84-bbox.ts @@ -2,7 +2,7 @@ import { findTagText } from "../utils"; import parseBoundingBox from "./parse-bbox"; export default function parseWGS84BoundingBox( - xml: string + xml: string, ): Readonly<[number, number, number, number]> | undefined { const bbox = findTagText(xml, "ows:WGS84BoundingBox"); if (bbox) { diff --git a/common/src/ows/find-operation.ts b/common/src/ows/find-operation.ts index 8a5ca91fd5..2fa8310338 100644 --- a/common/src/ows/find-operation.ts +++ b/common/src/ows/find-operation.ts @@ -4,7 +4,7 @@ import findOperations from "./find-operations"; export default function findOperation( xml: string, - name: string + name: string, ): string | undefined { return findOperations(xml)?.find((op) => getAttribute(op, "name") === name); } diff --git a/common/src/ows/find-operations.ts b/common/src/ows/find-operations.ts index b76f53db6a..2fe71ba3bf 100644 --- a/common/src/ows/find-operations.ts +++ b/common/src/ows/find-operations.ts @@ -2,6 +2,6 @@ import { findTagsByPath } from "xml-utils"; export default function findOperations(xml: string): string[] { return findTagsByPath(xml, ["ows:OperationsMetadata", "ows:Operation"]).map( - (tag) => tag.outer + (tag) => tag.outer, ); } diff --git a/common/src/ows/parse-bbox.ts b/common/src/ows/parse-bbox.ts index 842870b66c..734c72c99f 100644 --- a/common/src/ows/parse-bbox.ts +++ b/common/src/ows/parse-bbox.ts @@ -1,7 +1,7 @@ import { findTagText } from "../utils"; export default function parseBoundingBox( - xml: string + xml: string, ): Readonly<[number, number, number, number]> | undefined { const lowerCorner = findTagText(xml, "ows:LowerCorner"); const upperCorner = findTagText(xml, "ows:UpperCorner"); diff --git a/common/src/ows/test.ts b/common/src/ows/test.ts index 21df31d19b..82bdb3e436 100644 --- a/common/src/ows/test.ts +++ b/common/src/ows/test.ts @@ -32,12 +32,10 @@ test("ows: parseBoundingBox", ({ eq }) => { -180.0625 -81.0625 179.9375 81.0625 `; - eq(findAndParseWGS84BoundingBox(xml), [ - -180.0625, - -81.0625, - 179.9375, - 81.0625, - ]); + eq( + findAndParseWGS84BoundingBox(xml), + [-180.0625, -81.0625, 179.9375, 81.0625], + ); }); test("ows: findAndParseWGS84BoundingBox", ({ eq }) => { @@ -53,12 +51,13 @@ test("ows: findAndParseBoundingBox", ({ eq }) => { -60.35240259408949 -44.9887429023503 80.34944659977604 60.53764399304885 `; - eq(findAndParseBoundingBox(xml), [ - -60.35240259408949, - -44.9887429023503, - 80.34944659977604, - 60.53764399304885, - ]); + eq( + findAndParseBoundingBox(xml), + [ + -60.35240259408949, -44.9887429023503, 80.34944659977604, + 60.53764399304885, + ], + ); }); test("ows: find operation and its url", ({ eq }) => { @@ -98,7 +97,7 @@ test("ows: find operation and its url", ({ eq }) => { eq( findAndParseOperationUrl(xml, "DescribeCoverage"), - "https://geonode.wfp.org/geoserver/wcs?" + "https://geonode.wfp.org/geoserver/wcs?", ); }); @@ -109,14 +108,14 @@ test("ows: findException", ({ eq }) => { Unexpected error occurred during describe coverage xml encoding Unable to acquire a reader for this coverage with format: GeoTIFF `), - "Unable to acquire a reader for this coverage with format: GeoTIFFTranslator error\nUnexpected error occurred during describe coverage xml encoding\nUnable to acquire a reader for this coverage with format: GeoTIFF" + "Unable to acquire a reader for this coverage with format: GeoTIFFTranslator error\nUnexpected error occurred during describe coverage xml encoding\nUnable to acquire a reader for this coverage with format: GeoTIFF", ); eq( findException(` Could not understand version:1.0 `), - "Could not understand version:1.0" + "Could not understand version:1.0", ); eq( @@ -125,7 +124,7 @@ Unable to acquire a reader for this coverage with format: GeoTIFF Could not determine geoserver request from http request org.geoserver.monitor.MonitorServletRequest@4b7091d3 `), - "Could not determine geoserver request from http request org.geoserver.monitor.MonitorServletRequest@4b7091d3" + "Could not determine geoserver request from http request org.geoserver.monitor.MonitorServletRequest@4b7091d3", ); eq( @@ -135,51 +134,51 @@ Unable to acquire a reader for this coverage with format: GeoTIFF `), - "Missing version" + "Missing version", ); const exception = findException(wcsException); eq( exception?.startsWith("java.io.IOException: Failed to create reader from"), - true + true, ); eq( exception?.endsWith("STYLE_FACTORY = StyleFactoryImpl"), - true + true, ); }); test("getting capabilities url", async ({ eq }) => { eq( await getCapabilitiesUrl("https://geonode.wfp.org/geoserver/wfs"), - "https://geonode.wfp.org/geoserver/wfs?request=GetCapabilities&service=WFS" + "https://geonode.wfp.org/geoserver/wfs?request=GetCapabilities&service=WFS", ); eq( await getCapabilitiesUrl( - "https://geonode.wfp.org/geoserver/ows/?service=WFS" + "https://geonode.wfp.org/geoserver/ows/?service=WFS", ), - "https://geonode.wfp.org/geoserver/ows/?request=GetCapabilities&service=WFS" + "https://geonode.wfp.org/geoserver/ows/?request=GetCapabilities&service=WFS", ); eq( await getCapabilitiesUrl( - "https://geonode.wfp.org/geoserver/ows/?service=WFS&extra=true" + "https://geonode.wfp.org/geoserver/ows/?service=WFS&extra=true", ), - "https://geonode.wfp.org/geoserver/ows/?extra=true&request=GetCapabilities&service=WFS" + "https://geonode.wfp.org/geoserver/ows/?extra=true&request=GetCapabilities&service=WFS", ); eq( await getCapabilitiesUrl("https://geonode.wfp.org/geoserver/ows", { service: "WFS", }), - "https://geonode.wfp.org/geoserver/ows?request=GetCapabilities&service=WFS" + "https://geonode.wfp.org/geoserver/ows?request=GetCapabilities&service=WFS", ); eq( await getCapabilitiesUrl( "https://geonode.wfp.org/geoserver/ows?service=WFS", { version: "1.1.0", // add version - } + }, ), - "https://geonode.wfp.org/geoserver/ows?request=GetCapabilities&service=WFS&version=1.1.0" + "https://geonode.wfp.org/geoserver/ows?request=GetCapabilities&service=WFS&version=1.1.0", ); eq( await getCapabilitiesUrl( @@ -187,9 +186,9 @@ test("getting capabilities url", async ({ eq }) => { { service: "WFS", version: "1.1.1", // override - } + }, ), - "https://geonode.wfp.org/geoserver/ows?request=GetCapabilities&service=WFS&version=1.1.1" + "https://geonode.wfp.org/geoserver/ows?request=GetCapabilities&service=WFS&version=1.1.1", ); }); @@ -201,14 +200,14 @@ test("getCapabilities", async ({ eq }) => { wait: 3, }) ).length > 100, - true + true, ); const capabilities = await getCapabilities( "https://geonode.wfp.org/geoserver/wfs", { fetch, wait: 3, - } + }, ); eq(capabilities.includes("WFS"), true); diff --git a/common/src/utils/bbox.ts b/common/src/utils/bbox.ts index 1a73f8cc76..06d402769f 100644 --- a/common/src/utils/bbox.ts +++ b/common/src/utils/bbox.ts @@ -7,7 +7,7 @@ export function bboxToString( | Readonly | string[] | Array, - bboxDigits?: number + bboxDigits?: number, ): string { const [xmin, ymin, xmax, ymax] = bbox; return [ @@ -30,7 +30,7 @@ export function checkExtent(extent: BBOX): void { const [minX, minY, maxX, maxY] = extent; if (minX > maxX || minY > maxY) { throw new Error( - `the extent ${extent} seems malformed or else may contain "wrapping" which is not supported` + `the extent ${extent} seems malformed or else may contain "wrapping" which is not supported`, ); } } diff --git a/common/src/utils/image.ts b/common/src/utils/image.ts index 8b054a7b30..b3ba480e0e 100644 --- a/common/src/utils/image.ts +++ b/common/src/utils/image.ts @@ -15,7 +15,7 @@ export function scaleImage( checkExtent: true, resolution: 256, maxPixels: 5096, - } + }, ) { if (doCheckExtent) { checkExtent(extent); diff --git a/common/src/utils/other.ts b/common/src/utils/other.ts index 78fee49677..0c88d9e1a3 100644 --- a/common/src/utils/other.ts +++ b/common/src/utils/other.ts @@ -9,7 +9,7 @@ export function formatUrl( }: { debug?: boolean; sortParams?: boolean } = { debug: false, sortParams: true, - } + }, ): string { const url = new URL(baseUrl); const keys = Object.keys(params); @@ -37,7 +37,7 @@ export function hasLayerId( target: string, { strict = false }: { strict?: boolean } = { strict: false, - } + }, ): boolean { return !!ids.find((id) => { const { full, short, namespace } = parseName(id); diff --git a/common/src/utils/parse.ts b/common/src/utils/parse.ts index ea956e1fd4..dbf65bfebd 100644 --- a/common/src/utils/parse.ts +++ b/common/src/utils/parse.ts @@ -3,7 +3,7 @@ import { findTagText } from "./xml"; export function findAndParseAbstract( xml: string, - { trim = true }: { trim?: boolean } = { trim: true } + { trim = true }: { trim?: boolean } = { trim: true }, ) { let abstract = findTagText(xml, "Abstract"); if (!abstract) { @@ -18,7 +18,7 @@ export function findAndParseAbstract( export function findAndParseCapabilityUrl( xml: string, - capability: string + capability: string, ): string | undefined { const onlineResource = findTagByPath(xml, [ "Capability", @@ -84,9 +84,7 @@ export function findVersion(xml: string): string | undefined { return undefined; } -export function parseName( - name: string -): { +export function parseName(name: string): { full: string; namespace: string | undefined; short: string; @@ -101,7 +99,7 @@ export function parseName( export function parseService( url: string, - options?: { case?: "lower" | "raw" | "upper" } + options?: { case?: "lower" | "raw" | "upper" }, ) { const { pathname, searchParams } = new URL(url); diff --git a/common/src/utils/test.ts b/common/src/utils/test.ts index d37c78bcfe..4b12b2cb8c 100644 --- a/common/src/utils/test.ts +++ b/common/src/utils/test.ts @@ -29,11 +29,11 @@ test("bboxToString", ({ eq }) => { eq(bboxToString([-180, -90, 180, 90] as const), "-180,-90,180,90"); eq( bboxToString([-180.0001, -90.0001, 180.0001, 90.0001], 2), - "-180.00,-90.00,180.00,90.00" + "-180.00,-90.00,180.00,90.00", ); eq( bboxToString([100, 45, 103.09704125797317, 46.08568380901372]), - "100,45,103.09704125797317,46.08568380901372" + "100,45,103.09704125797317,46.08568380901372", ); }); @@ -48,7 +48,7 @@ test("check-extent", ({ eq }) => { } eq( msg, - 'Error: the extent 170,-90,150,90 seems malformed or else may contain "wrapping" which is not supported' + 'Error: the extent 170,-90,150,90 seems malformed or else may contain "wrapping" which is not supported', ); }); @@ -76,8 +76,7 @@ test("format-url: base url should add params", ({ eq }) => { service: "WMS", request: "GetMap", layers: "ModisIndices", - bbox: - "11897270.578531113,6261721.357121639,12523442.714243278,6887893.492833804", + bbox: "11897270.578531113,6261721.357121639,12523442.714243278,6887893.492833804", bboxsr: "3857", height: 256, srs: "EPSG:3857", @@ -90,7 +89,7 @@ test("format-url: base url should add params", ({ eq }) => { const url = formatUrl(baseUrl, params); eq( url, - "https://mongolia.sibelius-datacube.org:5000/wms?bbox=11897270.578531113%2C6261721.357121639%2C12523442.714243278%2C6887893.492833804&bboxsr=3857&crs=EPSG%3A3857&format=image%2Fpng&height=256&layers=ModisIndices&request=GetMap&service=WMS&srs=EPSG%3A3857&time=2022-07-11&transparent=true&version=1.3.0&width=256" + "https://mongolia.sibelius-datacube.org:5000/wms?bbox=11897270.578531113%2C6261721.357121639%2C12523442.714243278%2C6887893.492833804&bboxsr=3857&crs=EPSG%3A3857&format=image%2Fpng&height=256&layers=ModisIndices&request=GetMap&service=WMS&srs=EPSG%3A3857&time=2022-07-11&transparent=true&version=1.3.0&width=256", ); }); @@ -145,15 +144,15 @@ test("parse service from url", async ({ eq }) => { eq( parseService( "https://example.org/geoserver/ows?request=GetCapabilities&service=WFS", - { case: "lower" } + { case: "lower" }, ), - "wfs" + "wfs", ); eq( parseService( - "https://example.org/geoserver/ows?request=GetCapabilities&service=wfs" + "https://example.org/geoserver/ows?request=GetCapabilities&service=wfs", ), - "wfs" + "wfs", ); }); @@ -172,7 +171,7 @@ test("scaleImage", async ({ eq }) => { maxPixels: 5096, resolution: 64, }), - { height: 222, width: 677 } + { height: 222, width: 677 }, ); }); diff --git a/common/src/utils/utils.ts b/common/src/utils/utils.ts index 68860b13d2..d26a4d3c53 100644 --- a/common/src/utils/utils.ts +++ b/common/src/utils/utils.ts @@ -1,6 +1,6 @@ export function setTimeoutAsync( seconds: number, - callback: () => any + callback: () => any, ): Promise { // how do I error out if promise rejected return new Promise((resolve, reject) => { diff --git a/common/src/utils/xml.ts b/common/src/utils/xml.ts index 3bbcc25d26..b658524f8e 100644 --- a/common/src/utils/xml.ts +++ b/common/src/utils/xml.ts @@ -8,7 +8,7 @@ import { export function findTagArray( xml: string, - tagNameOrPath: string | string[] + tagNameOrPath: string | string[], ): string[] { return findTagsByPath(xml, castArray(tagNameOrPath)) .filter((tag) => tag.inner !== null) @@ -23,7 +23,7 @@ export function findTagText(xml: string, tagName: string): string | undefined { export function findTagAttribute( xml: string, tagNameOrPath: string | string[], - attribute: string + attribute: string, ): string | undefined { const tag = findTagByPath(xml, castArray(tagNameOrPath)); if (!tag) { diff --git a/common/src/wcs/WCS/index.ts b/common/src/wcs/WCS/index.ts index 0b4b312aa7..7becc85483 100644 --- a/common/src/wcs/WCS/index.ts +++ b/common/src/wcs/WCS/index.ts @@ -38,7 +38,7 @@ export default class WCS extends Base { > { const capabilities = await this.getCapabilities(); const coverages = findCoverages(capabilities); - return (Promise.all( + return Promise.all( coverages.slice(0, count).map((layer, layerIndex) => { try { return new WCSLayer({ @@ -55,10 +55,10 @@ export default class WCS extends Base { } throw error; } - }) + }), ).then((values) => - values.filter((value) => value !== undefined) - ) as any) as Promise; + values.filter((value) => value !== undefined), + ) as any as Promise; } async getLayerDays(options?: { diff --git a/common/src/wcs/WCS/test.ts b/common/src/wcs/WCS/test.ts index 43f591712e..8fa1688a40 100644 --- a/common/src/wcs/WCS/test.ts +++ b/common/src/wcs/WCS/test.ts @@ -8,12 +8,12 @@ const { port } = serve({ max: 20 }); test("WFP GeoNode", async ({ eq }) => { const wcs1 = new WCS( `https://geonode.wfp.org/geoserver/wcs?version=1.1.1&request=GetCapabilities&service=WCS`, - { fetch } + { fetch }, ); const wcs2 = new WCS( `https://geonode.wfp.org/geoserver/wcs?version=2.0.1&request=GetCapabilities&service=WCS`, - { fetch } + { fetch }, ); const layerIds1 = await wcs1.getLayerIds(); @@ -35,11 +35,11 @@ test("WFP GeoNode", async ({ eq }) => { const days2 = await wcs2.getLayerDays(); eq( Object.values(days1).every((days) => days.length === 0), - true + true, ); eq( Object.values(days2).every((days) => days.length === 0), - true + true, ); const layer1 = await wcs1.getLayer("geonode:wld_cli_tp_7d_ecmwf"); @@ -57,14 +57,14 @@ test("WFP GeoNode", async ({ eq }) => { }); eq( url, - "https://geonode.wfp.org/geoserver/wcs?bbox=-180.1%2C-81.1%2C179.9%2C81.1&coverage=geonode%3Awld_cli_tp_7d_ecmwf&crs=EPSG%3A4326&format=png&height=500&request=GetCoverage&service=WCS&version=1.0.0&width=500" + "https://geonode.wfp.org/geoserver/wcs?bbox=-180.1%2C-81.1%2C179.9%2C81.1&coverage=geonode%3Awld_cli_tp_7d_ecmwf&crs=EPSG%3A4326&format=png&height=500&request=GetCoverage&service=WCS&version=1.0.0&width=500", ); }); test("WCS version 1.0.0", async ({ eq }) => { const wcs = new WCS( `http://localhost:${port}/data/mongolia-sibelius-datacube-wcs-get-capabilities-1.0.0.xml`, - { fetch, service: "WCS", version: "1.0.0" } + { fetch, service: "WCS", version: "1.0.0" }, ); const layerIds = await wcs.getLayerIds(); eq(layerIds.includes("10DayAnomaly"), true); @@ -73,7 +73,7 @@ test("WCS version 1.0.0", async ({ eq }) => { eq(layerNames[0], "mdc 10 Day Indices"); eq( layerNames.every((layerName) => typeof layerName === "string"), - true + true, ); const days = await wcs.getLayerDays(); @@ -86,21 +86,21 @@ test("WCS version 1.0.0", async ({ eq }) => { eq(new Date(layerDays[0]).toUTCString(), "Tue, 21 May 2019 12:00:00 GMT"); eq( new Date(layerDays[layerDays.length - 1]).toUTCString(), - "Thu, 01 Oct 2020 12:00:00 GMT" + "Thu, 01 Oct 2020 12:00:00 GMT", ); }); test("WCS on version 1.1.1", async ({ eq }) => { const wcs = new WCS( `http://localhost:${port}/data/geonode-wfp-wcs-get-capabilities-1.1.1.xml`, - { fetch, service: "WCS", version: "1.1.1" } + { fetch, service: "WCS", version: "1.1.1" }, ); eq(wcs.version, "1.1.1"); eq(typeof wcs.fetch, "function"); const layerIds = await wcs.getLayerIds(); eq( layerIds.includes("geonode:_20apr08074540_s2as_r2c3_012701709010_01_p001"), - true + true, ); const layerNames = await wcs.getLayerNames(); @@ -125,17 +125,15 @@ test("WCS for data cube", async ({ eq }) => { eq(dates.length, 29); eq( dates.every((d) => typeof d === "string"), - true + true, ); eq(dates[0], "2019-05-21T00:00:00.000Z"); const extent = await layer.getExtent(); - eq(extent, [ - 86.7469655846003, - 41.4606540712216, - 117.717378164332, - 52.3174921613588, - ]); + eq( + extent, + [86.7469655846003, 41.4606540712216, 117.717378164332, 52.3174921613588], + ); // use WCS extent as bbox const url1 = await layer.getImageUrl({ @@ -146,7 +144,7 @@ test("WCS for data cube", async ({ eq }) => { }); eq( url1, - "https://mongolia.sibelius-datacube.org:5000/wcs?bbox=86.7%2C41.5%2C117.7%2C52.3&coverage=10DayTrend&crs=EPSG%3A4326&format=GeoTIFF&height=500&request=GetCoverage&service=WCS&version=1.0.0&width=500" + "https://mongolia.sibelius-datacube.org:5000/wcs?bbox=86.7%2C41.5%2C117.7%2C52.3&coverage=10DayTrend&crs=EPSG%3A4326&format=GeoTIFF&height=500&request=GetCoverage&service=WCS&version=1.0.0&width=500", ); // with bbox @@ -158,7 +156,7 @@ test("WCS for data cube", async ({ eq }) => { }); eq( url2, - "https://mongolia.sibelius-datacube.org:5000/wcs?bbox=100%2C45%2C103.09704125797317%2C46.08568380901372&coverage=10DayTrend&crs=EPSG%3A4326&format=GeoTIFF&height=222&request=GetCoverage&service=WCS&version=1.0.0&width=677" + "https://mongolia.sibelius-datacube.org:5000/wcs?bbox=100%2C45%2C103.09704125797317%2C46.08568380901372&coverage=10DayTrend&crs=EPSG%3A4326&format=GeoTIFF&height=222&request=GetCoverage&service=WCS&version=1.0.0&width=677", ); const image = await layer.getImage({ @@ -192,22 +190,22 @@ test("wcs: getLayerDays", async ({ eq }) => { eq( await new WCS( `http://localhost:${port}/data/geonode-wfp-wcs-get-capabilities-1.1.1.xml`, - { fetch } + { fetch }, ).getLayerDays(), - expectedGeoNodeDays + expectedGeoNodeDays, ); eq( await new WCS( `http://localhost:${port}/data/geonode-wfp-wcs-get-capabilities-2.0.1.xml`, - { fetch } + { fetch }, ).getLayerDays(), - expectedGeoNodeDays + expectedGeoNodeDays, ); eq( await new WCS( `http://localhost:${port}/data/mongolia-sibelius-datacube-wcs-get-capabilities-1.0.0.xml`, - { fetch } + { fetch }, ).getLayerDays(), { "10DayIndices": [1376222400000, 1642766400000], @@ -233,7 +231,7 @@ test("wcs: getLayerDays", async ({ eq }) => { ModisTCI: [1230811200000, 1657540800000], ModisVHI: [1230811200000, 1657540800000], DzudRisk: [1448366400000, 1610280000000], - } + }, ); const urls3 = [ @@ -241,14 +239,14 @@ test("wcs: getLayerDays", async ({ eq }) => { "https://api.earthobservation.vam.wfp.org/ows/wcs?service=WCS&request=GetCapabilities&version=2.0.1", ]; const layerDays3 = await Promise.all( - urls3.map((url) => new WCS(url, { fetch }).getLayerDays()) + urls3.map((url) => new WCS(url, { fetch }).getLayerDays()), ); eq(JSON.stringify(layerDays3[0]) === JSON.stringify(layerDays3[1]), true); eq(Object.keys(layerDays3[0]).length > 10, true); Object.values(layerDays3[0]).forEach((range) => { eq( range.every((day) => typeof day === "number"), - true + true, ); if (range.length === 2) { eq(range[0] < range[1], true); diff --git a/common/src/wcs/WCSLayer/index.ts b/common/src/wcs/WCSLayer/index.ts index 98a232bff9..69f8ab292f 100644 --- a/common/src/wcs/WCSLayer/index.ts +++ b/common/src/wcs/WCSLayer/index.ts @@ -33,7 +33,7 @@ export default class WCSLayer extends Layer { debug: options.debug, fetch: this.fetch, wait: options.wait, - }) + }), ); } diff --git a/common/src/wcs/utils/index.ts b/common/src/wcs/utils/index.ts index e1f546fcc8..126f289171 100644 --- a/common/src/wcs/utils/index.ts +++ b/common/src/wcs/utils/index.ts @@ -70,7 +70,7 @@ export function findCoverageName(xml: string): string | undefined { export function findLayerId( xml: string, - { normalize = true }: { normalize?: boolean } = { normalize: true } + { normalize = true }: { normalize?: boolean } = { normalize: true }, ): string { // version 2.x const coverageId = findCoverageId(xml); @@ -119,27 +119,27 @@ export function findCoverageDisplayName(xml: string): string | undefined { export function findCoverage( xml: string, - layerIdOrName: string + layerIdOrName: string, ): string | undefined { const normalized = normalizeCoverageId(layerIdOrName); return findCoverages(xml).find( (layer) => findLayerId(layer, { normalize: true }) === normalized || - findCoverageDisplayName(layer) === layerIdOrName + findCoverageDisplayName(layer) === layerIdOrName, ); } export function findCoverageDisplayNames(xml: string): string[] { const coverages = findCoverages(xml); const displayNames = coverages.map((coverage) => - findCoverageDisplayName(coverage) + findCoverageDisplayName(coverage), ); return displayNames.filter((name) => name !== undefined).map((name) => name!); } export function findLayerIds( xml: string, - { normalize }: { normalize?: boolean } = {} + { normalize }: { normalize?: boolean } = {}, ): string[] { return findCoverages(xml).map((layer) => findLayerId(layer, { normalize })); } @@ -148,7 +148,7 @@ export function findCoverageSubType(xml: string): string | undefined { return findTagText(xml, "wcs:CoverageSubtype"); } export function findAndParseLonLatEnvelope( - xml: string + xml: string, ): Readonly<[number, number, number, number] | undefined> { const envelope = findTagText(xml, "lonLatEnvelope"); if (!envelope) { @@ -171,7 +171,7 @@ export function findAndParseLonLatEnvelope( // for CoverageDescription export function findAndParseExtent( - xml: string + xml: string, ): Readonly<[number, number, number, number]> | undefined { return findAndParseLonLatEnvelope(xml) || findAndParseEnvelope(xml); } @@ -302,13 +302,13 @@ export function createGetCoverageUrl({ return `${formattedUrl}&${formattedSubsets}`; } throw new Error( - "[prism-common] createGetCoverageUrl was called with an unexpected version" + "[prism-common] createGetCoverageUrl was called with an unexpected version", ); } export function createDescribeCoverageUrl( xml: string, - layerId: string + layerId: string, ): string { const base = findDescribeCoverageUrl(xml); if (!base) { @@ -338,21 +338,21 @@ export function createDescribeCoverageUrl( export async function fetchCoverageDescriptionFromCapabilities( capabilities: string, layerId: string, - options: { debug?: boolean; fetch?: any; wait?: number } = {} + options: { debug?: boolean; fetch?: any; wait?: number } = {}, ) { const run = async () => { const url = createDescribeCoverageUrl(capabilities, layerId); const response = await (options.fetch || fetch)(url); if (response.status !== 200) { throw new Error( - `failed to fetch CoverageDescription from "${url}". status was ${response.status}` + `failed to fetch CoverageDescription from "${url}". status was ${response.status}`, ); } const text = await response.text(); const exception = findException(text); if (exception) { throw new Error( - `couldn't fetch coverage description because of the following error:\n${exception}` + `couldn't fetch coverage description because of the following error:\n${exception}`, ); } return text; @@ -427,7 +427,7 @@ export async function fetchCoverageLayerDays( { errorStrategy = "throw", fetch, - }: { errorStrategy?: string; fetch?: any } = {} + }: { errorStrategy?: string; fetch?: any } = {}, ): Promise<{ [layerId: string]: number[] }> { try { const capabilities = await getCapabilities(url, { diff --git a/common/src/wcs/utils/test.ts b/common/src/wcs/utils/test.ts index 4ae075ca8e..6140aa7907 100644 --- a/common/src/wcs/utils/test.ts +++ b/common/src/wcs/utils/test.ts @@ -31,33 +31,33 @@ const xml = findAndRead("geonode-wfp-wcs-get-capabilities-2.0.1.xml", { }); const xmlGeoNode111 = findAndRead( "geonode-wfp-wcs-get-capabilities-1.1.1.xml", - { encoding: "utf-8" } + { encoding: "utf-8" }, ); const xml100 = findAndRead( "mongolia-sibelius-datacube-wcs-get-capabilities-1.0.0.xml", - { encoding: "utf-8" } + { encoding: "utf-8" }, ); const xmlDescription201 = findAndRead( "geonode-wfp-wcs-describe-coverage-geonode__wld_cli_tp_7d_ecmwf-2.0.1.xml", - { encoding: "utf-8" } + { encoding: "utf-8" }, ); const xmlTemporalDescription100 = findAndRead( "mongolia-sibelius-datacube-wcs-coverage-description-10DayTrend-1.0.0.xml", - { encoding: "utf-8" } + { encoding: "utf-8" }, ); test("wcs: find coverage by id and name", ({ eq }) => { const coverageByFullId = findCoverage( xml, - "geonode:eyxao70woaa14nh_modificado" + "geonode:eyxao70woaa14nh_modificado", )!; const coverageByLegacyId = findCoverage( xml, - "geonode__eyxao70woaa14nh_modificado" + "geonode__eyxao70woaa14nh_modificado", )!; const coverageByName = findCoverage( xml, - "Climate Outlook across Eastern Africa" + "Climate Outlook across Eastern Africa", )!; eq(coverageByFullId.length, 2507); eq(coverageByLegacyId.length, 2507); @@ -110,9 +110,9 @@ test("wcs: find coverages", ({ eq }) => { test("wcs: normalize coverage identifier", ({ eq }) => { eq( normalizeCoverageId( - "geonode___20apr08074540_s2as_r2c3_012701709010_01_p001" + "geonode___20apr08074540_s2as_r2c3_012701709010_01_p001", ), - "geonode:_20apr08074540_s2as_r2c3_012701709010_01_p001" + "geonode:_20apr08074540_s2as_r2c3_012701709010_01_p001", ); }); @@ -126,7 +126,7 @@ test("wcs: find layer id", ({ eq }) => { eq(findCoverageId(coverageId), "geonode__sdn_fl_0909_noaa_40ff"); eq( findLayerId(coverageId, { normalize: true }), - "geonode:sdn_fl_0909_noaa_40ff" + "geonode:sdn_fl_0909_noaa_40ff", ); }); @@ -135,9 +135,9 @@ test("wcs: find layer ids", ({ eq }) => { eq(layerNames.length, 15); eq( layerNames.includes( - "geonode:_20apr08074540_s2as_r2c3_012701709010_01_p001" + "geonode:_20apr08074540_s2as_r2c3_012701709010_01_p001", ), - true + true, ); }); @@ -152,15 +152,11 @@ test("parse coverage", ({ eq }) => { keywords: ["WCS", "GeoTIFF", "eyxao70woaa14nh_modificado"], subType: "RectifiedGridCoverage", bbox: [ - -60.35240259408949, - -44.9887429023503, - 80.34944659977604, + -60.35240259408949, -44.9887429023503, 80.34944659977604, 60.53764399304885, ], wgs84bbox: [ - -52.12430934201362, - -42.939883795000185, - 72.12135334770016, + -52.12430934201362, -42.939883795000185, 72.12135334770016, 58.488784885698735, ], }); @@ -170,11 +166,11 @@ test("parse describe coverage url", ({ eq }) => { eq(findDescribeCoverageUrl(xml), "https://geonode.wfp.org/geoserver/wcs?"); eq( findAndParseCapabilityUrl(xml100, "GetCapabilities"), - "https://mongolia.sibelius-datacube.org:5000/wcs?" + "https://mongolia.sibelius-datacube.org:5000/wcs?", ); eq( findDescribeCoverageUrl(xml100), - "https://mongolia.sibelius-datacube.org:5000/wcs?" + "https://mongolia.sibelius-datacube.org:5000/wcs?", ); }); @@ -182,11 +178,11 @@ test("parse GetCoverage url", ({ eq }) => { eq(findGetCoverageUrl(xml), "https://geonode.wfp.org/geoserver/wcs?"); eq( findAndParseCapabilityUrl(xml100, "GetCoverage"), - "https://mongolia.sibelius-datacube.org:5000/wcs?" + "https://mongolia.sibelius-datacube.org:5000/wcs?", ); eq( findGetCoverageUrl(xml100), - "https://mongolia.sibelius-datacube.org:5000/wcs?" + "https://mongolia.sibelius-datacube.org:5000/wcs?", ); }); @@ -194,14 +190,14 @@ test("createDescribeCoverageUrl", ({ eq }) => { eq( createDescribeCoverageUrl( xmlGeoNode111, - "geonode:eyxao70woaa14nh_modificado" + "geonode:eyxao70woaa14nh_modificado", ), - "https://geonode.wfp.org/geoserver/wcs?identifiers=geonode%3Aeyxao70woaa14nh_modificado&request=DescribeCoverage&service=WCS&version=1.1.1" + "https://geonode.wfp.org/geoserver/wcs?identifiers=geonode%3Aeyxao70woaa14nh_modificado&request=DescribeCoverage&service=WCS&version=1.1.1", ); eq( createDescribeCoverageUrl(xml100, "10DayTrend"), - "https://mongolia.sibelius-datacube.org:5000/wcs?coverage=10DayTrend&request=DescribeCoverage&service=WCS&version=1.0.0" + "https://mongolia.sibelius-datacube.org:5000/wcs?coverage=10DayTrend&request=DescribeCoverage&service=WCS&version=1.0.0", ); }); @@ -216,7 +212,7 @@ test("wcs: createGetCoverageUrl", ({ eq }) => { }); eq( url, - "https://mongolia.sibelius-datacube.org:5000/wcs?bbox=87.7%2C41.6%2C119.9%2C52.1&coverage=ModisLST&crs=EPSG%3A4326&format=GeoTIFF&height=222&request=GetCoverage&service=WCS&version=1.0.0&width=677" + "https://mongolia.sibelius-datacube.org:5000/wcs?bbox=87.7%2C41.6%2C119.9%2C52.1&coverage=ModisLST&crs=EPSG%3A4326&format=GeoTIFF&height=222&request=GetCoverage&service=WCS&version=1.0.0&width=677", ); }); @@ -231,7 +227,7 @@ test("parseDates", ({ eq }) => { eq(dates.length, 29); eq( dates.every((d) => typeof d === "string"), - true + true, ); eq(dates[0], "2019-05-21T00:00:00.000Z"); }); @@ -243,12 +239,10 @@ test("parse envelope", ({ eq }) => { 2009-01-01 2022-07-11 `; - eq(findAndParseLonLatEnvelope(lonLatEnvelope), [ - 74.1288466392506, - 36.3432058634784, - 132.918744933946, - 54.4717721170595, - ]); + eq( + findAndParseLonLatEnvelope(lonLatEnvelope), + [74.1288466392506, 36.3432058634784, 132.918744933946, 54.4717721170595], + ); }); test("parse coverage (1.0.0)", ({ eq }) => { @@ -262,10 +256,7 @@ test("parse coverage (1.0.0)", ({ eq }) => { name: "mdc 10 Day Indices", subType: undefined, wgs84bbox: [ - 86.7469655846003, - 41.4606540712216, - 121.185885426709, - 52.3476515518552, + 86.7469655846003, 41.4606540712216, 121.185885426709, 52.3476515518552, ], }); }); @@ -274,7 +265,7 @@ test("fetchCoverageDescriptionFromCapabilities", async ({ eq }) => { const result = await fetchCoverageDescriptionFromCapabilities( xml100, "10DayTrend", - { fetch } + { fetch }, ); eq(result.length > 5000, true); eq(result.includes("CoverageDescription"), true); diff --git a/common/src/wfs/index.ts b/common/src/wfs/index.ts index d5e27f49f8..b299a7c4f9 100644 --- a/common/src/wfs/index.ts +++ b/common/src/wfs/index.ts @@ -16,7 +16,7 @@ export class WFS extends Base { async getLayerNames() { return getFeatureTypesFromCapabilities(await this.getCapabilities()).map( - (featureType) => featureType.name.short + (featureType) => featureType.name.short, ); } diff --git a/common/src/wfs/layer.ts b/common/src/wfs/layer.ts index 0a3c551c79..23c4ab02df 100644 --- a/common/src/wfs/layer.ts +++ b/common/src/wfs/layer.ts @@ -15,7 +15,7 @@ export default class FeatureLayer extends Layer { fetch: undefined, method: "POST", wait: 0, - } + }, ): Promise { // to-do: check if post available return getFeatures(await this.capabilities, this.id, { diff --git a/common/src/wfs/test.ts b/common/src/wfs/test.ts index c20cb0517b..7e678478cb 100644 --- a/common/src/wfs/test.ts +++ b/common/src/wfs/test.ts @@ -32,12 +32,10 @@ const xmls = { test("findAndParseLatLongBoundingBox", ({ eq }) => { const xml = ``; - eq(findAndParseLatLongBoundingBox(xml), [ - -81.7356205563041, - -4.22940646575291, - -66.8472154271626, - 13.394727761822, - ]); + eq( + findAndParseLatLongBoundingBox(xml), + [-81.7356205563041, -4.22940646575291, -66.8472154271626, 13.394727761822], + ); }); Object.entries(xmls).forEach(([version, xml]) => { @@ -45,11 +43,11 @@ Object.entries(xmls).forEach(([version, xml]) => { eq(parseGetFeatureUrl(xml), "https://geonode.wfp.org/geoserver/wfs"); eq( parseGetFeatureUrl(xml, { method: "GET" }), - "https://geonode.wfp.org/geoserver/wfs" + "https://geonode.wfp.org/geoserver/wfs", ); eq( parseGetFeatureUrl(xml, { method: "POST" }), - "https://geonode.wfp.org/geoserver/wfs" + "https://geonode.wfp.org/geoserver/wfs", ); }); }); @@ -85,9 +83,7 @@ Object.entries(xmls).forEach(([version, xml]) => { ], srs: "EPSG:4326", bbox: [ - -81.7356205563041, - -4.22940646575291, - -66.8472154271626, + -81.7356205563041, -4.22940646575291, -66.8472154271626, 13.394727761822, ], }; @@ -102,23 +98,23 @@ Object.entries(xmls).forEach(([version, xml]) => { const featureTypes = getFeatureTypesFromCapabilities(xml); eq( hasFeatureType(featureTypes, "geonode:col_second_level_admin_boundaries"), - true + true, ); eq( hasFeatureType(featureTypes, "wrong:col_second_level_admin_boundaries"), - false + false, ); eq(hasFeatureType(featureTypes, "col_second_level_admin_boundaries"), true); eq( hasFeatureType(featureTypes, "col_second_level_admin_boundaries", { strict: false, }), - true + true, ); eq(hasFeatureType(featureTypes, "geonode:does_not_exist"), false); eq( hasFeatureType(featureTypes, "geonode:does_not_exist", { strict: true }), - false + false, ); }); }); @@ -129,7 +125,7 @@ Object.entries(xmls).forEach(([version, xml]) => { getFeaturesUrl(xml, ["col_second_level_admin_boundaries"], { count: 2, }), - "https://geonode.wfp.org/geoserver/wfs?count=2&outputFormat=json&request=GetFeature&service=WFS&typeNames=col_second_level_admin_boundaries&version=2.0.0" + "https://geonode.wfp.org/geoserver/wfs?count=2&outputFormat=json&request=GetFeature&service=WFS&typeNames=col_second_level_admin_boundaries&version=2.0.0", ); eq( getFeaturesUrl(xml, "acled_incidents_syria", { @@ -137,13 +133,13 @@ Object.entries(xmls).forEach(([version, xml]) => { dateField: "event_date", dateRange: ["2020-09-18", "2022-09-20"], }), - "https://geonode.wfp.org/geoserver/wfs?count=1&cql_filter=event_date+BETWEEN+2020-09-18T00%3A00%3A00+AND+2022-09-20T23%3A59%3A59&outputFormat=json&request=GetFeature&service=WFS&typeNames=acled_incidents_syria&version=2.0.0" + "https://geonode.wfp.org/geoserver/wfs?count=1&cql_filter=event_date+BETWEEN+2020-09-18T00%3A00%3A00+AND+2022-09-20T23%3A59%3A59&outputFormat=json&request=GetFeature&service=WFS&typeNames=acled_incidents_syria&version=2.0.0", ); eq( getFeaturesUrl(xml, "geonode:afg_trs_roads_wfp", { count: Infinity, }), - "https://geonode.wfp.org/geoserver/wfs?outputFormat=json&request=GetFeature&service=WFS&typeNames=geonode%3Aafg_trs_roads_wfp&version=2.0.0" + "https://geonode.wfp.org/geoserver/wfs?outputFormat=json&request=GetFeature&service=WFS&typeNames=geonode%3Aafg_trs_roads_wfp&version=2.0.0", ); }); }); @@ -197,7 +193,7 @@ test("WFS", async ({ eq }) => { eq(layerNames.includes("_2020_global_adm3"), true); eq( layerNames.every((layerName) => layerName.indexOf(":") === -1), - true + true, ); eq(await instance.hasLayerId("_2020_global_adm3"), true); diff --git a/common/src/wfs/utils.ts b/common/src/wfs/utils.ts index c7f90b047a..44cdd1e61f 100644 --- a/common/src/wfs/utils.ts +++ b/common/src/wfs/utils.ts @@ -38,7 +38,7 @@ export function getBaseUrl(url: string): string { } export function findAndParseLatLongBoundingBox( - xml: string + xml: string, ): Readonly<[number, number, number, number]> | undefined { const tag = findTagByName(xml, "LatLongBoundingBox"); if (!tag) { @@ -56,7 +56,7 @@ export function findAndParseLatLongBoundingBox( // to-do: MetadataURL // to-do: parse prefix from name? export function getFeatureTypesFromCapabilities( - capabilities: string + capabilities: string, ): FeatureType[] { const featureTypes: FeatureType[] = []; findTagsByPath(capabilities, ["FeatureTypeList", "FeatureType"]).forEach( @@ -72,24 +72,24 @@ export function getFeatureTypesFromCapabilities( keywords: findAndParseKeywords(inner), srs: (findTagText(inner, "DefaultSRS")?.replace( "urn:x-ogc:def:crs:", - "" + "", ) || findTagText(inner, "SRS"))!, bbox: (findAndParseWGS84BoundingBox(inner) || findAndParseLatLongBoundingBox(inner))!, }); } } - } + }, ); return featureTypes; } export function parseFullFeatureTypeNames( capabilities: string, - { sort = true }: { sort?: boolean } = { sort: true } + { sort = true }: { sort?: boolean } = { sort: true }, ): string[] { const names = getFeatureTypesFromCapabilities(capabilities).map( - (featureType) => featureType.name.full + (featureType) => featureType.name.full, ); if (sort) { // eslint-disable-next-line fp/no-mutating-methods @@ -103,7 +103,7 @@ export function parseGetFeatureUrl( capabilities: string, { method = "GET" }: { method: "GET" | "POST"; throw?: boolean } = { method: "GET", - } + }, ): string | undefined { const url = findAndParseOperationUrl(capabilities, "GetFeature", method) || @@ -117,7 +117,7 @@ export function parseGetFeatureUrl( "HTTP", titlecase(method), ], - "onlineResource" + "onlineResource", ); if (!url) { return undefined; @@ -132,7 +132,7 @@ export function hasFeatureType( name: string, { strict = false }: { strict?: boolean } = { strict: false, - } + }, ): boolean { return !!featureTypes.find((featureType) => { if (strict) { @@ -185,7 +185,7 @@ export function getFeaturesUrl( method: "POST", sortBy: undefined, version: "2.0.0", - } + }, ) { const base = parseGetFeatureUrl(capabilities, { method }); @@ -201,9 +201,8 @@ export function getFeaturesUrl( service: "WFS", version, request: "GetFeature", - [/^(0|1)/.test(version) - ? "typeName" - : "typeNames"]: typeNameOrNames?.toString(), + [/^(0|1)/.test(version) ? "typeName" : "typeNames"]: + typeNameOrNames?.toString(), bbox: bbox?.toString(), featureID: featureId, srsName: srs, @@ -251,7 +250,7 @@ export async function getFeatures( format: "geojson", method: "POST", wait: 0, - } + }, ) { const run = async () => { const url = getFeaturesUrl(capabilities, typeNameOrNames, { diff --git a/common/src/wms/WMS/index.ts b/common/src/wms/WMS/index.ts index 87b022d11a..ae3ab58cef 100644 --- a/common/src/wms/WMS/index.ts +++ b/common/src/wms/WMS/index.ts @@ -41,8 +41,8 @@ export class WMS extends Base { id: findName(layer)!, fetch: this.fetch, layer, - }) - ) + }), + ), ); } diff --git a/common/src/wms/WMS/test.ts b/common/src/wms/WMS/test.ts index 916366abdd..ebfbe1cbd0 100644 --- a/common/src/wms/WMS/test.ts +++ b/common/src/wms/WMS/test.ts @@ -9,7 +9,7 @@ const { port } = serve({ max: 2 }); test("WMS", async ({ eq }) => { const client = new WMS( "https://geonode.wfp.org/geoserver/wms?version=1.3.0", - { fetch } + { fetch }, ); const layerIds = await client.getLayerIds(); eq(layerIds.length >= 669, true); @@ -24,7 +24,7 @@ test("WMS", async ({ eq }) => { eq(days["prism:col_gdacs_buffers"].length >= 22, true); eq( new Date(days["prism:col_gdacs_buffers"][0]).toUTCString(), - "Sun, 07 Aug 2011 12:00:00 GMT" + "Sun, 07 Aug 2011 12:00:00 GMT", ); const layer = await client.getLayer("prism:col_gdacs_buffers"); @@ -40,7 +40,7 @@ test("WMS", async ({ eq }) => { test("WMS (1.1.1)", async ({ eq }) => { const client = new WMS( "https://geonode.wfp.org/geoserver/wms?version=1.1.1", - { fetch } + { fetch }, ); const layerIds = await client.getLayerIds(); @@ -56,7 +56,7 @@ test("WMS (1.1.1)", async ({ eq }) => { eq(days["prism:col_gdacs_buffers"].length >= 22, true); eq( new Date(days["prism:col_gdacs_buffers"][0]).toUTCString(), - "Sun, 07 Aug 2011 12:00:00 GMT" + "Sun, 07 Aug 2011 12:00:00 GMT", ); const layer = await client.getLayer("prism:col_gdacs_buffers"); @@ -80,9 +80,7 @@ test("WMS (1.1.1)", async ({ eq }) => { version: "1.1.1", width: 256, bbox: [ - 5009377.085697312, - -3130860.6785608195, - 5635549.221409474, + 5009377.085697312, -3130860.6785608195, 5635549.221409474, -2504688.542848654, ], }; @@ -96,7 +94,7 @@ test("WMS (1.1.1)", async ({ eq }) => { eq( imageUrl, - "https://geonode.wfp.org/geoserver/wms?SERVICE=WMS&%3B=&bbox=5009377.085697312%2C-3130860.6785608195%2C5635549.221409474%2C-2504688.542848654&bboxsr=3857&exceptions=application%2Fvnd.ogc.se_inimage&format=image%2Fpng&height=256&imagesr=3857&layers=wld_gdacs_tc_events_nodes&request=GetMap&service=WMS&srs=EPSG%3A3857&time=2022-04-27&transparent=true&version=1.1.1&width=256" + "https://geonode.wfp.org/geoserver/wms?SERVICE=WMS&%3B=&bbox=5009377.085697312%2C-3130860.6785608195%2C5635549.221409474%2C-2504688.542848654&bboxsr=3857&exceptions=application%2Fvnd.ogc.se_inimage&format=image%2Fpng&height=256&imagesr=3857&layers=wld_gdacs_tc_events_nodes&request=GetMap&service=WMS&srs=EPSG%3A3857&time=2022-04-27&transparent=true&version=1.1.1&width=256", ); const { image } = await imageLayer.getImage(getImageOptions); @@ -106,7 +104,7 @@ test("WMS (1.1.1)", async ({ eq }) => { test("WMS Data Cube", async ({ eq }) => { const client = new WMS( `http://localhost:${port}/data/mongolia-sibelius-datacube-wms-get-capabilities-1.3.0.xml`, - { fetch } + { fetch }, ); const layerIds = await client.getLayerIds(); @@ -127,9 +125,7 @@ test("WMS Data Cube", async ({ eq }) => { const params = { bbox: [ - 11897270.578531113, - 6261721.357121639, - 12523442.714243278, + 11897270.578531113, 6261721.357121639, 12523442.714243278, 6887893.492833804, ], bboxSrs: 3857, @@ -141,7 +137,7 @@ test("WMS Data Cube", async ({ eq }) => { const imageUrl = await layer.getImageUrl(params); eq( imageUrl, - "https://mongolia.sibelius-datacube.org:5000/wms?bbox=11897270.578531113%2C6261721.357121639%2C12523442.714243278%2C6887893.492833804&bboxsr=3857&crs=EPSG%3A3857&format=image%2Fpng&height=256&layers=ModisIndices&request=GetMap&service=WMS&srs=EPSG%3A3857&time=2022-07-11&transparent=true&version=1.3.0&width=256" + "https://mongolia.sibelius-datacube.org:5000/wms?bbox=11897270.578531113%2C6261721.357121639%2C12523442.714243278%2C6887893.492833804&bboxsr=3857&crs=EPSG%3A3857&format=image%2Fpng&height=256&layers=ModisIndices&request=GetMap&service=WMS&srs=EPSG%3A3857&time=2022-07-11&transparent=true&version=1.3.0&width=256", ); const { image } = await layer.getImage(params); diff --git a/common/src/wms/layer.ts b/common/src/wms/layer.ts index 6d324a1bd4..a13b55bf9b 100644 --- a/common/src/wms/layer.ts +++ b/common/src/wms/layer.ts @@ -15,7 +15,7 @@ export default class WMSLayer extends Layer { if (!this.layer) { this.layer = this.capabilities.then( - (xml) => findLayer(xml, this.id, { errorStrategy: "throw" })! + (xml) => findLayer(xml, this.id, { errorStrategy: "throw" })!, ); } @@ -36,7 +36,7 @@ export default class WMSLayer extends Layer { } async getImage( - options: GetImageOptions + options: GetImageOptions, ): Promise { const url = await this.getImageUrl(options); const response = await this.fetch(url); diff --git a/common/src/wms/utils/index.ts b/common/src/wms/utils/index.ts index 3fa7f309e9..2cc706526c 100644 --- a/common/src/wms/utils/index.ts +++ b/common/src/wms/utils/index.ts @@ -60,7 +60,7 @@ export function findTitle(xml: string): string | undefined { export function findLayer( xml: string, layerName: string, - options?: { errorStrategy?: string } + options?: { errorStrategy?: string }, ): string | undefined { const result = findLayers(xml).find((layer) => { const name = findName(layer); @@ -81,7 +81,7 @@ export function getLayerIds(xml: string): string[] { export function getLayerNames( xml: string, // sometimes layer titles will have an extra space in the front or end unsuitable for display - { clean = false }: { clean: boolean } = { clean: false } + { clean = false }: { clean: boolean } = { clean: false }, ): string[] { const layers = findLayers(xml); const titles = layers.map((layer) => findTitle(layer) || ""); @@ -95,9 +95,9 @@ export function parseLayerDates(xml: string): string[] { } const dimensions = findTagsByName(xml, "Dimension"); - const timeDimension = dimensions.find((dimension) => { - return getAttribute(dimension.outer, "name") === "time"; - }); + const timeDimension = dimensions.find( + (dimension) => getAttribute(dimension.outer, "name") === "time", + ); // we have to trim because sometimes WMS adds spaces or new line if (timeDimension?.inner?.trim()) { @@ -110,9 +110,9 @@ export function parseLayerDates(xml: string): string[] { // we weren't able to find any times using the // so let's try const extents = findTagsByName(xml, "Extent"); - const timeExtent = extents.find((extent) => { - return getAttribute(extent.outer, "name") === "time"; - }); + const timeExtent = extents.find( + (extent) => getAttribute(extent.outer, "name") === "time", + ); // we have to trim because sometimes WMS adds spaces or new line if (timeExtent?.inner?.trim()) { @@ -179,14 +179,13 @@ export function parseLayer(xml: string): WMSLayer | undefined { namespace, abstract: findAndParseAbstract(xml), keywords: findTagArray(xml, "Keyword"), - srs: ((): string[] => { + srs: ((): string[] => // sometimes called CRS or SRS depending on the WMS version - return [...findTagArray(xml, "CRS"), ...findTagArray(xml, "SRS")]; - })(), + [...findTagArray(xml, "CRS"), ...findTagArray(xml, "SRS")])(), bbox: (() => { const ExGeographicBoundingBox = findTagByName( xml, - "EX_GeographicBoundingBox" + "EX_GeographicBoundingBox", ); if (ExGeographicBoundingBox) { const { inner } = ExGeographicBoundingBox; diff --git a/common/src/wms/utils/test.ts b/common/src/wms/utils/test.ts index 8ec68ea287..a0be77b073 100644 --- a/common/src/wms/utils/test.ts +++ b/common/src/wms/utils/test.ts @@ -25,7 +25,7 @@ const odcXml = findAndRead( "mongolia-sibelius-datacube-wms-get-capabilities-1.3.0.xml", { encoding: "utf-8", - } + }, ); test("get layer ids", async ({ eq }) => { @@ -102,16 +102,13 @@ test("get layer dates", async ({ eq }) => { test("get all layer days", ({ eq }) => { const days = getAllLayerDays(xml); const layerId = "prism:lka_gdacs_buffers"; - eq(days[layerId], [ - 1351684800000, - 1388923200000, - 1480593600000, - 1512475200000, - 1542542400000, - 1545048000000, - 1606392000000, - 1607083200000, - ]); + eq( + days[layerId], + [ + 1351684800000, 1388923200000, 1480593600000, 1512475200000, 1542542400000, + 1545048000000, 1606392000000, 1607083200000, + ], + ); eq(days["geonode:landslide"], []); }); @@ -144,9 +141,7 @@ test("parse layer", ({ eq }) => { test("createGetMapUrl", async ({ eq }) => { const url = createGetMapUrl({ bbox: [ - 11897270.578531113, - 6261721.357121639, - 12523442.714243278, + 11897270.578531113, 6261721.357121639, 12523442.714243278, 6887893.492833804, ], bboxSrs: 3857, @@ -159,7 +154,7 @@ test("createGetMapUrl", async ({ eq }) => { }); eq( url, - "https://mongolia.sibelius-datacube.org:5000/wms?bbox=11897270.578531113%2C6261721.357121639%2C12523442.714243278%2C6887893.492833804&bboxsr=3857&crs=EPSG%3A3857&format=image%2Fpng&height=256&layers=ModisIndices&request=GetMap&service=WMS&srs=EPSG%3A3857&time=2022-07-11&transparent=true&version=1.3.0&width=256" + "https://mongolia.sibelius-datacube.org:5000/wms?bbox=11897270.578531113%2C6261721.357121639%2C12523442.714243278%2C6887893.492833804&bboxsr=3857&crs=EPSG%3A3857&format=image%2Fpng&height=256&layers=ModisIndices&request=GetMap&service=WMS&srs=EPSG%3A3857&time=2022-07-11&transparent=true&version=1.3.0&width=256", ); }); @@ -170,6 +165,6 @@ test("createGetLegendGraphicUrl", ({ eq }) => { }); eq( url, - "https://mongolia.sibelius-datacube.org:5000/wms?format=image%2Fpng&layer=ModisIndices&legend_options=fontAntiAliasing%3Atrue%3BfontColor%3A0x2D3436%3BfontName%3ARoboto+Light%3BfontSize%3A13%3BforceLabels%3Aon%3BforceTitles%3Aon%3BgroupLayout%3Avertical%3BhideEmptyRules%3Afalse%3Blayout%3Avertical%3Bwrap%3Afalse&request=GetLegendGraphic&service=WMS" + "https://mongolia.sibelius-datacube.org:5000/wms?format=image%2Fpng&layer=ModisIndices&legend_options=fontAntiAliasing%3Atrue%3BfontColor%3A0x2D3436%3BfontName%3ARoboto+Light%3BfontSize%3A13%3BforceLabels%3Aon%3BforceTitles%3Aon%3BgroupLayout%3Avertical%3BhideEmptyRules%3Afalse%3Blayout%3Avertical%3Bwrap%3Afalse&request=GetLegendGraphic&service=WMS", ); }); diff --git a/frontend/.eslintrc b/frontend/.eslintrc deleted file mode 100644 index ee8aed35e3..0000000000 --- a/frontend/.eslintrc +++ /dev/null @@ -1,102 +0,0 @@ -{ - "extends": [ - "react-app", - "airbnb", - "plugin:jsx-a11y/recommended", - "prettier", - "prettier/react" - ], - "parser": "@typescript-eslint/parser", - "plugins": ["jsx-a11y", "fp", "@typescript-eslint", "prettier", "import"], - "rules": { - // Allow JSX within .js files - "react/jsx-filename-extension": [ - 1, - { "extensions": [".js", ".jsx", ".ts", ".tsx"] } - ], - // Allow props spreading in React - "react/jsx-props-no-spreading": 0, - // Allow named exports only files - "import/prefer-default-export": 0, - "object-curly-spacing": [ - "error", - "always", - { "arraysInObjects": true, "objectsInObjects": true } - ], - // More verbose prettier suggestions - "prettier/prettier": ["warn", { "endOfLine": "auto" }], - "react/require-default-props": ["off"], - "react/jsx-no-bind": ["warn"], - "import/extensions": [ - "error", - "ignorePackages", - { - "js": "never", - "jsx": "never", - "ts": "never", - "tsx": "never" - } - ], - // Warnings to enforce functional programming styles - e.g. no unintended mutations - "fp/no-delete": "warn", - "fp/no-mutating-assign": "warn", - "fp/no-mutating-methods": [ - "warn", - { - "allowedObjects": ["_"] - } - ], - "fp/no-mutation": [ - "warn", - { - "commonjs": true, - "allowThis": true, - "exceptions": [ - { "property": "propTypes" }, - { "property": "defaultProps" }, - { "property": "current" } - ] - } - ], - "no-console": ["warn", { "allow": ["warn", "error"] }], - - "lines-between-class-members": "off", - "max-classes-per-file": "off", - "spaced-comment": "warn", - "curly": ["warn", "all"], - "import/no-cycle": "error", - "import/no-extraneous-dependencies": [ - "error", - { - "devDependencies": true - } - ], - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": ["error"], - "no-use-before-define": "off", - "@typescript-eslint/no-use-before-define": ["error"], - "no-shadow": "off", - "@typescript-eslint/no-shadow": "warn" - }, - "settings": { - "import/resolver": { - "node": { - "moduleDirectory": ["node_modules", "./src"], - "extensions": [".js", ".jsx", ".ts", ".tsx"] - } - }, - "import/parsers": { - "@typescript-eslint/parser": [".ts", ".tsx"] - }, - "typescript": { - // always try to resolve types under `@types` directory even it doesn't contain any source code, like `@types/unist` - "alwaysTryTypes": true - } - }, - "globals": { - "NodeJS": true, - "RequestInit": true, - "RequestInfo": true, - "GeoJSON": true - } -} diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs new file mode 100644 index 0000000000..5c00512053 --- /dev/null +++ b/frontend/.eslintrc.cjs @@ -0,0 +1,112 @@ +module.exports = { + extends: [ + 'react-app', + 'airbnb', + 'plugin:jsx-a11y/recommended', + 'prettier', + 'plugin:react/jsx-runtime', + ], + parser: '@typescript-eslint/parser', + plugins: ['jsx-a11y', 'fp', 'prettier', 'import', 'react-refresh'], + rules: { + 'react-refresh/only-export-components': 'error', + // Allow JSX within .js files + 'react/jsx-filename-extension': [ + 1, + { extensions: ['.js', '.jsx', '.ts', '.tsx'] }, + ], + // Allow props spreading in React + 'react/jsx-props-no-spreading': 0, + // Allow named exports only files + 'import/prefer-default-export': 0, + 'object-curly-spacing': [ + 'error', + 'always', + { arraysInObjects: true, objectsInObjects: true }, + ], + // More verbose prettier suggestions + 'prettier/prettier': ['warn', { endOfLine: 'auto' }], + 'react/require-default-props': ['off'], + 'react/jsx-no-bind': ['warn'], + 'import/extensions': [ + 'error', + 'ignorePackages', + { + js: 'never', + jsx: 'never', + ts: 'never', + tsx: 'never', + }, + ], + // Warnings to enforce functional programming styles - e.g. no unintended mutations + 'fp/no-delete': 'warn', + 'fp/no-mutating-assign': 'warn', + 'fp/no-mutating-methods': [ + 'warn', + { + allowedObjects: ['_'], + }, + ], + 'fp/no-mutation': [ + 'warn', + { + commonjs: true, + allowThis: true, + exceptions: [ + { property: 'propTypes' }, + { property: 'defaultProps' }, + { property: 'current' }, + ], + }, + ], + 'no-console': ['warn', { allow: ['warn', 'error'] }], + 'lines-between-class-members': 'off', + 'max-classes-per-file': 'off', + 'spaced-comment': 'warn', + curly: ['warn', 'all'], + 'import/no-cycle': 'error', + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: true, + }, + ], + 'no-underscore-dangle': 'off', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + 'no-use-before-define': 'off', + '@typescript-eslint/no-use-before-define': ['error'], + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': 'warn', + 'react/prop-types': 'off', + 'default-param-last': 'warn', + }, + settings: { + 'import/resolver': { + node: { + moduleDirectory: ['node_modules', './src'], + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + }, + 'import/parsers': { + '@typescript-eslint/parser': ['.ts', '.tsx'], + }, + typescript: { + // always try to resolve types under `@types` directory even it doesn't contain any source code, like `@types/unist` + alwaysTryTypes: true, + }, + }, + globals: { + NodeJS: true, + RequestInit: true, + RequestInfo: true, + GeoJSON: true, + }, +}; diff --git a/frontend/.nvmrc b/frontend/.nvmrc new file mode 100644 index 0000000000..209e3ef4b6 --- /dev/null +++ b/frontend/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 37ff8db9e5..c656c89ddf 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,7 +1,7 @@ # A small image around the frontend code # that makes it easier to run python tests # against the frontend using playwright -FROM node:16-bookworm +FROM node:20 # copy dependencies definitions for better caching WORKDIR /common/ diff --git a/frontend/babel.config.cjs b/frontend/babel.config.cjs new file mode 100644 index 0000000000..954db76e51 --- /dev/null +++ b/frontend/babel.config.cjs @@ -0,0 +1,20 @@ +module.exports = { + presets: [ + [ + '@babel/preset-env', + { useBuiltIns: 'entry', corejs: '2', targets: { node: 'current' } }, + '@babel/preset-typescript', + ], + ], + plugins: [ + function () { + return { + visitor: { + MetaProperty(path) { + path.replaceWithSourceString('process'); + }, + }, + }; + }, + ], +}; diff --git a/frontend/config-overrides.js b/frontend/config-overrides.js deleted file mode 100644 index 6f305cc6fe..0000000000 --- a/frontend/config-overrides.js +++ /dev/null @@ -1,42 +0,0 @@ -/* eslint-disable fp/no-mutation */ -const { - override, - disableChunk, - useEslintRc, - addWebpackPlugin, -} = require('customize-cra'); -const path = require('path'); -const RemovePlugin = require('remove-files-webpack-plugin'); - -const country = process.env.REACT_APP_COUNTRY || 'mozambique'; -if (!!country) { - console.log( - `Building for country ${country}. Removing data for other countries.`, - ); -} - -// In case GIT_HASH is not set we are in github actions environment -process.env.REACT_APP_GIT_HASH = ( - process.env.GITHUB_SHA || process.env.GIT_HASH -)?.slice(0, 8); - -module.exports = override( - useEslintRc(path.resolve(__dirname, '.eslintrc')), - disableChunk(), - !!country && - addWebpackPlugin( - new RemovePlugin({ - after: { - root: './build', - test: [ - { - folder: './data', - method: absPath => - !new RegExp(country.toLowerCase(), 'm').test(absPath), - recursive: true, - }, - ], - }, - }), - ), -); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000000..127777bbfa --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + PRISM + + +
+ + + diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts new file mode 100644 index 0000000000..8bee65383d --- /dev/null +++ b/frontend/jest.config.ts @@ -0,0 +1,35 @@ +import type { Config } from 'jest'; + +const config: Config = { + preset: 'ts-jest', + testEnvironment: 'jest-environment-jsdom', + globalSetup: '/test/global-setup.cjs', + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + transformIgnorePatterns: [ + '/node_modules/(?!(.*\\.mjs$|quick-lru))', + ], + babelConfig: true, + useESM: true, + }, + ], + '^.+\\.js$': 'babel-jest', // Handle .js files with Babel + '^.+\\.mjs$': 'babel-jest', // Add this line to handle .mjs files + // process `*.tsx` files with `ts-jest` + }, + transformIgnorePatterns: ['/node_modules/(?!(quick-lru)/)'], + moduleNameMapper: { + '\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': + '/test/fileMock.ts', + '\\.(css|less)$': '/test/fileMock.ts', + }, + setupFilesAfterEnv: ['/test/setupTests.ts'], + moduleDirectories: ['node_modules', 'src'], + roots: [''], + modulePaths: [''], + snapshotSerializers: ['enzyme-to-json/serializer'], +}; + +export default config; diff --git a/frontend/package.json b/frontend/package.json index 3004572ec3..f4e79e8b2c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,13 +15,17 @@ ] }, "scripts": { - "check-boundary-keys": "node ./scripts/check-boundary-keys-validity.js", - "clean": "rimraf ./node_modules && pushd ../common && rimraf ./node_modules && popd", - "start": "cross-env EXTEND_ESLINT=true react-scripts start", + "start": "cross-env vite", + "test": "cross-env REACT_APP_TESTING=true NODE_OPTIONS=\"--experimental-vm-modules\" && jest --maxWorkers=8", + "test:watch": "yarn test --watch", + "build": "export GIT_HASH=$(git rev-parse --short HEAD) || true && tsc && cross-env vite build", + "lint": "eslint ./src ../common/src --ext js,jsx,ts,tsx -c .eslintrc.cjs", + "lint:ci": "yarn lint --max-warnings 0", + "preview": "vite preview", + "setup:common": "rimraf ./node_modules/prism-common && cd ../common && yarn install --network-timeout 600000 && yarn build && cd ../frontend && yarn install --check-files", + "clean": "pushd ../common && rimraf ./node_modules && popd && rimraf ./node_modules", "analyze": "yarn build && npx source-map-explorer 'build/static/js/*.js'", - "build": "export GIT_HASH=$(git rev-parse --short HEAD) || true && cross-env EXTEND_ESLINT=true react-app-rewired build", "profile:serve": "yarn build --profile && npx serve -s build", - "test": "cross-env REACT_APP_TESTING=true && react-scripts test", "prettier:json-fix": "prettier --write */**.json", "prettier:json-check": "prettier --check */**.json", "predeploy:prod": "yarn build", @@ -30,11 +34,9 @@ "deploy:staging": "firebase deploy --only hosting:staging-target", "deploy:surge": "surge --project ./build", "deploy:tests": "./scripts/deploy_to_surge.sh", - "eject": "react-scripts eject", - "lint": "eslint --ext .js,.jsx,.ts,.tsx -c .eslintrc ./src ../common/src", "precommit": "lint-staged", - "preprocess-layers": "ts-node --compilerOptions \"{\\\"module\\\":\\\"commonjs\\\"}\" ./scripts/preprocess-layers.ts", - "setup:common": "rimraf ./node_modules/prism-common && cd ../common && yarn install --network-timeout 600000 && yarn build && cd ../frontend && yarn install --check-files" + "check-boundary-keys": "node ./scripts/check-boundary-keys-validity.js", + "preprocess-layers": "ts-node --compilerOptions \"{\\\"module\\\":\\\"commonjs\\\"}\" ./scripts/preprocess-layers.ts" }, "dependencies": { "@azure/msal-browser": "^2.19.0", @@ -48,21 +50,23 @@ "@material-ui/icons": "^4.11.3", "@material-ui/lab": "4.0.0-alpha.61", "@material-ui/styles": "4.11.5", - "@react-pdf/font": "2.2.0", - "@react-pdf/renderer": "2.1.0", + "@react-pdf/font": "2.5.1", + "@react-pdf/renderer": "3.4.4", "@reduxjs/toolkit": "^1.4.0", "@sentry/browser": "^5.15.5", - "@turf/bbox": "^6.5.0", - "@turf/bbox-polygon": "^6.5.0", - "@turf/boolean-point-in-polygon": "^6.5.0", - "@turf/centroid": "^6.5.0", - "@turf/clone": "^6.5.0", - "@turf/mask": "^6.5.0", - "@turf/meta": "^6.5.0", - "@turf/simplify": "^6.5.0", - "@turf/union": "^6.5.0", + "@turf/bbox": "^7", + "@turf/bbox-polygon": "^7", + "@turf/boolean-point-in-polygon": "^7", + "@turf/centroid": "^7", + "@turf/clone": "^7", + "@turf/mask": "^7", + "@turf/meta": "^7", + "@turf/simplify": "^7", + "@turf/turf": "^7.0.0", + "@turf/union": "^7", "@types/react-window": "^1.8.8", "babel-eslint": "10.0.3", + "canvas": "link:./node_modules/.cache/null", "chart.js": "^2.9.3", "chartjs-plugin-datalabels": "^1.0.0", "colormap": "^2.3.1", @@ -75,15 +79,15 @@ "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.2", "enzyme-to-json": "^3.4.4", + "fontkit": "^2.0.2", "geojson": "^0.5.0", - "geotiff": "^1.0.0-beta.11", + "geotiff": "^2.1.3", "h3-js": "^3.7.0", - "html-react-parser": "^1.3.0", + "html-react-parser": "^5.1.10", "html2canvas": "^1.4.1", "husky": "^4.2.3", - "i18next": "^21.6.11", - "i18next-browser-languagedetector": "^6.1.3", - "i18next-http-backend": "^1.3.2", + "i18next": "^23.10.1", + "i18next-browser-languagedetector": "^7.2.0", "jspdf": "^2.3.0", "lint-staged": "^10.0.8", "lodash": "^4.17.21", @@ -91,42 +95,30 @@ "marked": "^4.0.10", "max-inscribed-circle": "^2.0.1", "papaparse": "^5.2.0", - "prettier": "2.0.5", "prism-common": "file:../common", "prop-types": "^15.7.2", - "react": "^16.13.0", - "react-app-rewired": "^2.2.1", + "react": "^18", "react-chartjs-2": "^2.9.0", "react-datepicker": "^2.14.1", - "react-dom": "^16.13.0", - "react-draggable": "^4.4.3", - "react-i18next": "^11.15.4", + "react-dom": "^18", + "react-draggable": "^4.4.6 ", + "react-i18next": "^14.1.2", "react-map-gl": "^7.1.0", "react-markdown": "^8.0.7", - "react-pdf": "^5.7.2", + "react-pdf": "^7.7.3", "react-range-slider-input": "^3.0.7", "react-redux": "^7.2.0", "react-router-dom": "^5.1.2", - "react-scripts": "3.4.0", "react-window": "^1.8.10", "redux": "^4.0.5", "reflect-metadata": "^0.1.13", - "remove-files-webpack-plugin": "^1.5.0", "rimraf": "^3.0.2", "url": "^0.11.0", "xml-js": "^1.6.11" }, "resolutions": { - "@react-pdf/font": "2.2.0", - "**/@typescript-eslint/eslint-plugin": "^4.1.1", - "**/@typescript-eslint/parser": "^4.1.1", - "node-notifier": "^10.0.1" - }, - "jest": { - "snapshotSerializers": [ - "enzyme-to-json/serializer" - ], - "globalSetup": "./test/global-setup.js" + "node-notifier": "^10.0.1", + "canvas": "link:./node_modules/.cache/null" }, "_comment": "The 'defaults' and 'not ie 11' are needed to avoid build issues with maplibre-gl.", "browserslist": { @@ -144,51 +136,65 @@ ] }, "devDependencies": { + "@babel/preset-env": "^7.24.7", + "@babel/preset-typescript": "^7.24.7", "@fortawesome/fontawesome-common-types": "^6.5.1", "@react-pdf/types": "^2.1.0", - "@testing-library/dom": "^6.15.0", - "@testing-library/jest-dom": "^4.2.4", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^7.1.2", - "@turf/helpers": "^6.5.0", + "@testing-library/dom": "^10.3.1", + "@testing-library/jest-dom": "^6.1.3", + "@testing-library/react": "^16", + "@testing-library/user-event": "^14.5.2", + "@turf/helpers": "^7", "@types/chart.js": "^2.9.21", "@types/colormap": "^2.3.1", "@types/d3": "^5.7.2", "@types/dompurify": "^2.2.3", "@types/geojson": "^7946.0.7", - "@types/jest": "^24.0.0", + "@types/jest": "^29.5.4", "@types/lodash": "^4.14.149", "@types/marked": "^4.0.2", - "@types/node": "^16.0.0", + "@types/node": "20.8.8", "@types/papaparse": "^5.0.3", - "@types/react": "^16.13.0", + "@types/react": "^18", "@types/react-datepicker": "^2.11.0", - "@types/react-dom": "^16.9.0", + "@types/react-dom": "^18", "@types/react-pdf": "^5.7.2", "@types/react-redux": "^7.1.7", "@types/react-router-dom": "^5.1.3", "@types/redux-mock-store": "^1.0.6", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^7.16.0", + "@vitejs/plugin-react-swc": "^3.5.0", + "babel-jest": "^29.7.0", "cross-env": "^7.0.2", - "eslint": "^6.8.0", - "eslint-config-airbnb": "^18.0.1", - "eslint-config-prettier": "^6.11.0", - "eslint-config-react-app": "^5.2.0", - "eslint-import-resolver-typescript": "^2.0.0", - "eslint-plugin-flowtype": "^3.0", + "cross-fetch": "^4.0.0", + "dotenv": "^16.4.5", + "eslint": "^8.45.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-prettier": "^9.1.0", + "eslint-config-react-app": "^7.0.1", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-flowtype": "^8.0.3", "eslint-plugin-fp": "^2.3.0", - "eslint-plugin-import": "^2.20.1", - "eslint-plugin-jsx-a11y": "^6.2.3", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.19.0", - "eslint-plugin-react-hooks": "^1.7", - "fs": "^0.0.1-security", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jsx-a11y": "^6.8.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-react": "^7.34.1", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "jest": "^29.6.4", + "jest-environment-jsdom": "^29.6.4", + "prettier": "^3.2.5", "redux-mock-store": "^1.5.4", "surge": "^0.23.1", "timezone-mock": "^1.3.6", + "ts-jest": "^29.1.1", "ts-node": "^10.9.2", - "typescript": "4.4.4" + "typescript": "^5.2.2", + "vite": "^5.2.0", + "vite-plugin-node-polyfills": "^0.21.0" }, "engines": { - "node": "16.x" + "node": "20.x" } } diff --git a/frontend/public/index.html b/frontend/public/index.html deleted file mode 100644 index e9bf92f725..0000000000 --- a/frontend/public/index.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - PRISM - - - - -
- - - - \ No newline at end of file diff --git a/frontend/scripts/preprocess-layers.ts b/frontend/scripts/preprocess-layers.ts index 93ce46563f..4135c40402 100644 --- a/frontend/scripts/preprocess-layers.ts +++ b/frontend/scripts/preprocess-layers.ts @@ -43,9 +43,13 @@ async function preprocessBoundaryLayer(country, boundaryLayer) { 'utf-8', ); const boundaryData = JSON.parse(fileContent); - const preprocessedData = mergeBoundaryData(boundaryData); - - fs.writeFileSync(outputFilePath, JSON.stringify(preprocessedData)); + try { + const preprocessedData = mergeBoundaryData(boundaryData); + fs.writeFileSync(outputFilePath, JSON.stringify(preprocessedData)); + } catch (error) { + console.warn(`Warning: Failed to merge boundary data for ${country}.`, error); + return; + } } } diff --git a/frontend/src/components/404Page/__snapshots__/index.test.tsx.snap b/frontend/src/components/404Page/__snapshots__/index.test.tsx.snap index 7da9beea08..0ef25e8a5f 100644 --- a/frontend/src/components/404Page/__snapshots__/index.test.tsx.snap +++ b/frontend/src/components/404Page/__snapshots__/index.test.tsx.snap @@ -3,10 +3,10 @@ exports[`renders as expected 1`] = `
World Food Programme logo
diff --git a/frontend/src/components/404Page/index.test.tsx b/frontend/src/components/404Page/index.test.tsx index 340baf9cee..0408466f9c 100644 --- a/frontend/src/components/404Page/index.test.tsx +++ b/frontend/src/components/404Page/index.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render } from '@testing-library/react'; import { BrowserRouter } from 'react-router-dom'; import NotFound from '.'; diff --git a/frontend/src/components/404Page/index.tsx b/frontend/src/components/404Page/index.tsx index 164e85c0fc..8a3f9a5842 100644 --- a/frontend/src/components/404Page/index.tsx +++ b/frontend/src/components/404Page/index.tsx @@ -1,17 +1,16 @@ -import React from 'react'; import { createStyles, - WithStyles, - withStyles, Typography, Button, Grid, + makeStyles, } from '@material-ui/core'; import { Link } from 'react-router-dom'; import { colors } from 'muiTheme'; -function NotFound({ classes }: NotFoundProps) { +function NotFound() { + const classes = useStyles(); return (
@@ -49,7 +48,7 @@ function NotFound({ classes }: NotFoundProps) { ); } -const styles = () => +const useStyles = makeStyles(() => createStyles({ container: { width: '100vw', @@ -68,8 +67,7 @@ const styles = () => width: '90%', opacity: '0.5', }, - }); + }), +); -export interface NotFoundProps extends WithStyles {} - -export default withStyles(styles)(NotFound); +export default NotFound; diff --git a/frontend/src/components/App/index.test.tsx b/frontend/src/components/App/index.test.tsx index ef24073948..b63543d5d2 100644 --- a/frontend/src/components/App/index.test.tsx +++ b/frontend/src/components/App/index.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render } from '@testing-library/react'; import App from '.'; diff --git a/frontend/src/components/App/index.tsx b/frontend/src/components/App/index.tsx index 0d0180f23f..f8775bcb52 100644 --- a/frontend/src/components/App/index.tsx +++ b/frontend/src/components/App/index.tsx @@ -13,6 +13,8 @@ import Notifier from 'components/Notifier'; import AuthModal from 'components/AuthModal'; // Basic CSS Layout for the whole page import './app.css'; +import RobotoFont from 'fonts/Roboto-Regular.ttf'; +import KhmerFont from 'fonts/Khmer-Regular.ttf'; if (process.env.NODE_ENV && process.env.NODE_ENV !== 'development') { if (process.env.REACT_APP_SENTRY_URL) { @@ -24,23 +26,31 @@ if (process.env.NODE_ENV && process.env.NODE_ENV !== 'development') { } } +// Register all the fonts necessary +Font.register({ + family: 'Roboto', + src: RobotoFont, +}); + +Font.register({ + family: 'Khmer', + src: KhmerFont, +}); + // https://github.com/diegomura/react-pdf/issues/1991 Font.register({ family: 'Roboto', fonts: [ { - src: - 'https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf', + src: 'https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf', fontWeight: 400, }, { - src: - 'https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmEU9vAx05IsDqlA.ttf', + src: 'https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmEU9vAx05IsDqlA.ttf', fontWeight: 500, }, { - src: - 'https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmWUlvAx05IsDqlA.ttf', + src: 'https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmWUlvAx05IsDqlA.ttf', fontWeight: 700, }, ], @@ -55,7 +65,7 @@ const Wrapper = memo(() => { setIsAlertFormOpen={setIsAlertFormOpen} /> - + diff --git a/frontend/src/components/AuthModal/index.tsx b/frontend/src/components/AuthModal/index.tsx index fa7e0de23b..df78452133 100644 --- a/frontend/src/components/AuthModal/index.tsx +++ b/frontend/src/components/AuthModal/index.tsx @@ -1,4 +1,4 @@ -import React, { +import { ChangeEvent, FormEvent, useCallback, @@ -16,10 +16,8 @@ import { TextField, Theme, Typography, - WithStyles, - withStyles, + makeStyles, } from '@material-ui/core'; -import { TFunctionKeys } from 'i18next'; import { useSafeTranslation } from 'i18n'; import { layersSelector } from 'context/mapStateSlice/selectors'; import { setUserAuthGlobal, userAuthSelector } from 'context/serverStateSlice'; @@ -27,11 +25,13 @@ import { UserAuth } from 'config/types'; import { getUrlKey, useUrlHistory } from 'utils/url-utils'; import { removeLayer } from 'context/mapStateSlice'; -const AuthModal = ({ classes }: AuthModalProps) => { - const initialAuthState: UserAuth = { - username: '', - password: '', - }; +const initialAuthState: UserAuth = { + username: '', + password: '', +}; + +const AuthModal = () => { + const classes = useStyles(); const [open, setOpen] = useState(false); const [auth, setAuth] = useState(initialAuthState); @@ -41,17 +41,17 @@ const AuthModal = ({ classes }: AuthModalProps) => { const { removeLayerFromUrl } = useUrlHistory(); const dispatch = useDispatch(); - const isUserAuthenticated = useMemo(() => { - return userAuth !== undefined; - }, [userAuth]); + const isUserAuthenticated = useMemo(() => userAuth !== undefined, [userAuth]); const { t } = useSafeTranslation(); - const layersWithAuthRequired = useMemo(() => { - return selectedLayers.filter( - layer => layer.type === 'point_data' && layer.authRequired, - ); - }, [selectedLayers]); + const layersWithAuthRequired = useMemo( + () => + selectedLayers.filter( + layer => layer.type === 'point_data' && layer.authRequired, + ), + [selectedLayers], + ); useEffect(() => { if (!layersWithAuthRequired.length || isUserAuthenticated) { @@ -70,24 +70,26 @@ const AuthModal = ({ classes }: AuthModalProps) => { ); // The layer with auth title - const layerWithAuthTitle = useMemo(() => { - return layersWithAuthRequired.reduce( - (acc: string, currentLayer, currentLayerIndex) => { - return currentLayerIndex === 0 - ? t(currentLayer?.title as TFunctionKeys) ?? '' - : `${acc}, ${t(currentLayer.title as TFunctionKeys)}`; - }, - '', - ); - }, [layersWithAuthRequired, t]); + const layerWithAuthTitle = useMemo( + () => + layersWithAuthRequired.reduce( + (acc: string, currentLayer, currentLayerIndex) => + currentLayerIndex === 0 + ? t(currentLayer?.title as any) ?? '' + : `${acc}, ${t(currentLayer.title as any)}`, + '', + ), + [layersWithAuthRequired, t], + ); // function that handles the text-field on change - const handleInputTextChanged = useCallback((identifier: keyof UserAuth) => { - return (event: ChangeEvent) => { + const handleInputTextChanged = useCallback( + (identifier: keyof UserAuth) => (event: ChangeEvent) => { const { value } = event.target; setAuth(params => ({ ...params, [identifier]: value })); - }; - }, []); + }, + [], + ); // function that is invoked when cancel is clicked const onCancelClick = useCallback(() => { @@ -99,11 +101,11 @@ const AuthModal = ({ classes }: AuthModalProps) => { }); setAuth(initialAuthState); setOpen(false); - }, [dispatch, initialAuthState, layersWithAuthRequired, removeLayerFromUrl]); + }, [dispatch, layersWithAuthRequired, removeLayerFromUrl]); // function that handles the close modal const closeModal = useCallback( - (event, reason) => { + (_event: any, reason: any) => { if (reason === 'backdropClick') { onCancelClick(); return; @@ -137,12 +139,18 @@ const AuthModal = ({ classes }: AuthModalProps) => {
- + {t('Username')} @@ -153,7 +161,11 @@ const AuthModal = ({ classes }: AuthModalProps) => { onChange={handleInputTextChanged('username')} /> - + {t('Password')} @@ -166,7 +178,12 @@ const AuthModal = ({ classes }: AuthModalProps) => { /> - +
- - + )} + {!mdUp && ( - - + )} { ); -}; +} export default GoToBoundaryDropdown; diff --git a/frontend/src/components/Common/BoundaryDropdown/index.tsx b/frontend/src/components/Common/BoundaryDropdown/index.tsx index 6ce4a7b63f..db999d82e4 100644 --- a/frontend/src/components/Common/BoundaryDropdown/index.tsx +++ b/frontend/src/components/Common/BoundaryDropdown/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo, useCallback, memo } from 'react'; +import { useState, useMemo, useCallback, memo } from 'react'; import { FormControl, InputLabel, @@ -16,46 +16,45 @@ import { import { useSafeTranslation } from 'i18n'; import SearchBar from './searchBar'; -import { setMenuItemStyle, containsText, createMatchesTree } from './utils'; - -const useStyles = makeStyles((theme: Theme) => { - return { - header: { +import { + setMenuItemStyle, + containsText, + createMatchesTree, + MapInteraction, +} from './utils'; + +const useStyles = makeStyles((theme: Theme) => ({ + header: { + textTransform: 'uppercase', + letterSpacing: '3px', + fontSize: '0.7em', + }, + subHeader: { + paddingLeft: '2em', + }, + menuItem: { + paddingLeft: '2.8em', + fontSize: '0.9em', + }, + select: { + '& .MuiSelect-icon': { + color: theme.palette.text.primary, + fontSize: '1.25rem', + }, + }, + formControl: { + width: '100%', + '& > .MuiInputLabel-shrink': { display: 'none' }, + '& > .MuiInput-root': { margin: 0 }, + '& label': { textTransform: 'uppercase', letterSpacing: '3px', - fontSize: '0.7em', - }, - subHeader: { - paddingLeft: '2em', + fontSize: '11px', + position: 'absolute', + top: '-13px', }, - menuItem: { - paddingLeft: '2.8em', - fontSize: '0.9em', - }, - select: { - '& .MuiSelect-icon': { - color: theme.palette.text.primary, - fontSize: '1.25rem', - }, - }, - formControl: { - width: '100%', - '& > .MuiInputLabel-shrink': { display: 'none' }, - '& > .MuiInput-root': { margin: 0 }, - '& label': { - textTransform: 'uppercase', - letterSpacing: '3px', - fontSize: '11px', - position: 'absolute', - top: '-13px', - }, - }, - }; -}); - -export enum MapInteraction { - GoTo = 'goto', -} + }, +})); type BoundaryDropdownProps = { labelText: string; @@ -73,9 +72,10 @@ const BoundaryDropdown = memo( const styles = useStyles(); - const levelsRelations = useMemo(() => { - return boundaryRelationDataDict[i18nLocale.language]; - }, [boundaryRelationDataDict, i18nLocale.language]); + const levelsRelations = useMemo( + () => boundaryRelationDataDict[i18nLocale.language], + [boundaryRelationDataDict, i18nLocale.language], + ); const relationsToRender = useMemo(() => { if ( diff --git a/frontend/src/components/Common/BoundaryDropdown/searchBar.tsx b/frontend/src/components/Common/BoundaryDropdown/searchBar.tsx index 47bfe634dc..21d649e6e0 100644 --- a/frontend/src/components/Common/BoundaryDropdown/searchBar.tsx +++ b/frontend/src/components/Common/BoundaryDropdown/searchBar.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef, Ref } from 'react'; +import { forwardRef, Ref } from 'react'; import { Search } from '@material-ui/icons'; import { TextField, diff --git a/frontend/src/components/Common/BoundaryDropdown/utils.ts b/frontend/src/components/Common/BoundaryDropdown/utils.ts index 33d1f2d7fc..0cc7157df9 100644 --- a/frontend/src/components/Common/BoundaryDropdown/utils.ts +++ b/frontend/src/components/Common/BoundaryDropdown/utils.ts @@ -1,4 +1,3 @@ -import type { Feature, MultiPolygon, BBox } from '@turf/helpers'; import { sortBy } from 'lodash'; import bbox from '@turf/bbox'; import { BoundaryLayerData } from 'context/layers/boundary'; @@ -8,6 +7,7 @@ import { BoundaryLayerProps, AdminLevelType, } from 'config/types'; +import { BBox, Feature, MultiPolygon } from 'geojson'; export type BoundaryRelationsDict = { [key: string]: BoundaryRelationData }; @@ -205,9 +205,8 @@ export const setMenuItemStyle = ( } }; -export const containsText = (text: string, searchText: string) => { - return (text?.toLowerCase().indexOf(searchText?.toLowerCase()) || 0) > -1; -}; +export const containsText = (text: string, searchText: string) => + (text?.toLowerCase().indexOf(searchText?.toLowerCase()) || 0) > -1; /* * This function returns the higher level relations from a given relation match. @@ -256,3 +255,7 @@ export const createMatchesTree = ( return [...acc, item]; }, [] as BoundaryRelation[]); + +export enum MapInteraction { + GoTo = 'goto', +} diff --git a/frontend/src/components/Common/Chart/index.test.tsx b/frontend/src/components/Common/Chart/index.test.tsx index 400c3d574d..fb43cf9fdf 100644 --- a/frontend/src/components/Common/Chart/index.test.tsx +++ b/frontend/src/components/Common/Chart/index.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render } from '@testing-library/react'; import Chart from '.'; diff --git a/frontend/src/components/Common/Chart/index.tsx b/frontend/src/components/Common/Chart/index.tsx index 6847d69342..bc01ccd32f 100644 --- a/frontend/src/components/Common/Chart/index.tsx +++ b/frontend/src/components/Common/Chart/index.tsx @@ -2,7 +2,6 @@ import React, { memo, useCallback, useMemo } from 'react'; import colormap from 'colormap'; import { ChartOptions } from 'chart.js'; import { Bar, Line } from 'react-chartjs-2'; -import { TFunctionKeys } from 'i18next'; import { ChartConfig, DatasetField } from 'config/types'; import { TableData } from 'context/tableStateSlice'; import { useSafeTranslation } from 'i18n'; @@ -84,22 +83,23 @@ const Chart = memo( ...title.split(' '), ]); - const transpose = useMemo(() => { - return config.transpose || false; - }, [config.transpose]); + const transpose = useMemo( + () => config.transpose || false, + [config.transpose], + ); - const header = useMemo(() => { - return data.rows[0]; - }, [data.rows]); + const header = useMemo(() => data.rows[0], [data.rows]); - const tableRows = useMemo(() => { - return data.rows.slice(1, data.rows.length); - }, [data.rows]); + const tableRows = useMemo( + () => data.rows.slice(1, data.rows.length), + [data.rows], + ); // Get the keys for the data of interest - const indices = useMemo(() => { - return Object.keys(header).filter(key => key.includes(config.data || '')); - }, [config.data, header]); + const indices = useMemo( + () => Object.keys(header).filter(key => key.includes(config.data || '')), + [config.data, header], + ); // rainbow-soft map requires nshades to be at least size 11 const nshades = useMemo(() => { @@ -109,14 +109,16 @@ const Chart = memo( return Math.max(11, indices.length); }, [indices.length, tableRows.length, transpose]); - const colorShuffle = useCallback((colors: string[]) => { - return colors.map((_, i) => - i % 2 ? colors[i] : colors[colors.length - i - 1], - ); - }, []); + const colorShuffle = useCallback( + (colors: string[]) => + colors.map((_, i) => + i % 2 ? colors[i] : colors[colors.length - i - 1], + ), + [], + ); - const colors = useMemo(() => { - return ( + const colors = useMemo( + () => config.colors || colorShuffle( colormap({ @@ -125,9 +127,9 @@ const Chart = memo( format: 'hex', alpha: 0.5, }), - ) - ); - }, [colorShuffle, config.colors, nshades]); + ), + [colorShuffle, config.colors, nshades], + ); const labels = React.useMemo(() => { if (!transpose) { @@ -139,10 +141,10 @@ const Chart = memo( }, [chartRange, config.category, header, indices, tableRows, transpose]); // The table rows data sets - const tableRowsDataSet = useMemo(() => { - return tableRows.map((row, i) => { - return { - label: t(row[config.category] as TFunctionKeys) || '', + const tableRowsDataSet = useMemo( + () => + tableRows.map((row, i) => ({ + label: t(row[config.category] as any) || '', fill: config.fill || false, backgroundColor: colors[i], borderColor: colors[i], @@ -150,24 +152,14 @@ const Chart = memo( pointRadius: isEWSChart ? 0 : 1, // Disable point rendering for EWS only. data: indices.map(index => (row[index] as number) || null), pointHitRadius: 10, - }; - }); - }, [ - colors, - config.category, - config.fill, - indices, - isEWSChart, - t, - tableRows, - ]); + })), + [colors, config.category, config.fill, indices, isEWSChart, t, tableRows], + ); const configureIndicePointRadius = useCallback( (indiceKey: string) => { const foundDataSetFieldPointRadius = datasetFields?.find( - datasetField => { - return header[indiceKey] === datasetField.label; - }, + datasetField => header[indiceKey] === datasetField.label, )?.pointRadius; if (foundDataSetFieldPointRadius !== undefined) { @@ -179,10 +171,10 @@ const Chart = memo( ); // The indicesDataSet - const indicesDataSet = useMemo(() => { - return indices.map((indiceKey, i) => { - return { - label: t(header[indiceKey] as TFunctionKeys), + const indicesDataSet = useMemo( + () => + indices.map((indiceKey, i) => ({ + label: t(header[indiceKey] as any), fill: config.fill || false, backgroundColor: colors[i], borderColor: colors[i], @@ -190,17 +182,17 @@ const Chart = memo( data: tableRows.map(row => (row[indiceKey] as number) || null), pointRadius: configureIndicePointRadius(indiceKey), pointHitRadius: 10, - }; - }); - }, [ - colors, - config.fill, - configureIndicePointRadius, - header, - indices, - t, - tableRows, - ]); + })), + [ + colors, + config.fill, + configureIndicePointRadius, + header, + indices, + t, + tableRows, + ], + ); const EWSthresholds = useMemo(() => { if (data.EWSConfig) { @@ -253,77 +245,81 @@ const Chart = memo( data: set.data.slice(chartRange[0], chartRange[1]), })); - const chartData = { - labels, - datasets: datasetsTrimmed, - }; + const chartData = React.useMemo( + () => ({ + labels, + datasets: datasetsTrimmed, + }), + [datasetsTrimmed, labels], + ); - const chartConfig = useMemo(() => { - return { - maintainAspectRatio: !(notMaintainAspectRatio ?? false), - title: { - fontColor: '#CCC', - display: true, - text: subtitle ? [title, subtitle] : title, - fontSize: 14, - }, - scales: { - xAxes: [ - { - stacked: config?.stacked ?? false, - gridLines: { - display: false, - }, - ticks: { - callback: value => { - // for EWS charts, we only want to display the time - return isEWSChart ? String(value).split(' ')[1] : value; + const chartConfig = useMemo( + () => + ({ + maintainAspectRatio: !(notMaintainAspectRatio ?? false), + title: { + fontColor: '#CCC', + display: true, + text: subtitle ? [title, subtitle] : title, + fontSize: 14, + }, + scales: { + xAxes: [ + { + stacked: config?.stacked ?? false, + gridLines: { + display: false, }, - fontColor: '#CCC', - }, - ...(xAxisLabel - ? { - scaleLabel: { - labelString: xAxisLabel, - display: true, - }, - } - : {}), - }, - ], - yAxes: [ - { - ticks: { - fontColor: '#CCC', - ...(config?.minValue && { suggestedMin: config?.minValue }), - ...(config?.maxValue && { suggestedMax: config?.maxValue }), + ticks: { + callback: value => + // for EWS charts, we only want to display the time + isEWSChart ? String(value).split(' ')[1] : value, + fontColor: '#CCC', + }, + ...(xAxisLabel + ? { + scaleLabel: { + labelString: xAxisLabel, + display: true, + }, + } + : {}), }, - stacked: config?.stacked ?? false, - gridLines: { - display: false, + ], + yAxes: [ + { + ticks: { + fontColor: '#CCC', + ...(config?.minValue && { suggestedMin: config?.minValue }), + ...(config?.maxValue && { suggestedMax: config?.maxValue }), + }, + stacked: config?.stacked ?? false, + gridLines: { + display: false, + }, }, - }, - ], - }, - // display values for all datasets in the tooltip - tooltips: { - mode: 'index', - }, - legend: { - display: config.displayLegend, - position: legendAtBottom ? 'bottom' : 'right', - labels: { boxWidth: 12, boxHeight: 12 }, - }, - } as ChartOptions; - }, [ - config, - isEWSChart, - legendAtBottom, - notMaintainAspectRatio, - title, - subtitle, - xAxisLabel, - ]); + ], + }, + // display values for all datasets in the tooltip + tooltips: { + mode: 'index', + }, + legend: { + display: config.displayLegend, + position: legendAtBottom ? 'bottom' : 'right', + labels: { boxWidth: 12, boxHeight: 12 }, + }, + }) as ChartOptions, + [ + config, + isEWSChart, + legendAtBottom, + notMaintainAspectRatio, + title, + subtitle, + xAxisLabel, + ], + ); return useMemo( () => ( diff --git a/frontend/src/components/Common/HashText/index.tsx b/frontend/src/components/Common/HashText/index.tsx index 4573a490ad..feb27b787e 100644 --- a/frontend/src/components/Common/HashText/index.tsx +++ b/frontend/src/components/Common/HashText/index.tsx @@ -1,7 +1,6 @@ // This component creates a hidden link, to the version in which the app was built. -import React from 'react'; -const HashText = () => { +function HashText() { const hash = process.env.REACT_APP_GIT_HASH; if (hash) { // eslint-disable-next-line no-console @@ -30,6 +29,6 @@ const HashText = () => { version hash: {hash} ); -}; +} export default HashText; diff --git a/frontend/src/components/Common/Loader/index.tsx b/frontend/src/components/Common/Loader/index.tsx index 30d7c0fd29..5b6ebca8ea 100644 --- a/frontend/src/components/Common/Loader/index.tsx +++ b/frontend/src/components/Common/Loader/index.tsx @@ -1,14 +1,13 @@ import { LinearProgress } from '@material-ui/core'; -import React from 'react'; interface LoaderProps { showLoader: boolean; } -const Loader = ({ showLoader }: LoaderProps) => { +function Loader({ showLoader }: LoaderProps) { if (!showLoader) { return null; } return ; -}; +} export default Loader; diff --git a/frontend/src/components/Common/LoadingBlinkingDots/index.tsx b/frontend/src/components/Common/LoadingBlinkingDots/index.tsx index c08967acd7..bfa6569257 100644 --- a/frontend/src/components/Common/LoadingBlinkingDots/index.tsx +++ b/frontend/src/components/Common/LoadingBlinkingDots/index.tsx @@ -1,27 +1,26 @@ -import React, { memo } from 'react'; -import { createStyles, withStyles, WithStyles } from '@material-ui/core'; +import { memo } from 'react'; +import { createStyles, makeStyles } from '@material-ui/core'; -const LoadingBlinkingDots = memo( - ({ classes, dotColor }: LoadingBlinkingDotsProps) => { - const colorStyle = { color: dotColor || 'black' }; - return ( - <> -   - - . - - - . - - - . - - - ); - }, -); +const LoadingBlinkingDots = memo(({ dotColor }: LoadingBlinkingDotsProps) => { + const classes = useStyles(); + const colorStyle = { color: dotColor || 'black' }; + return ( + <> +   + + . + + + . + + + . + + + ); +}); -const styles = () => +const useStyles = makeStyles(() => createStyles({ '@keyframes blink': { '50%': { @@ -37,10 +36,11 @@ const styles = () => animationDelay: '500ms', }, }, - }); + }), +); -export interface LoadingBlinkingDotsProps extends WithStyles { +export interface LoadingBlinkingDotsProps { dotColor?: string; } -export default withStyles(styles)(LoadingBlinkingDots); +export default LoadingBlinkingDots; diff --git a/frontend/src/components/Common/MultiOptionsButton/index.tsx b/frontend/src/components/Common/MultiOptionsButton/index.tsx index efcee7fc46..db8edd5e07 100644 --- a/frontend/src/components/Common/MultiOptionsButton/index.tsx +++ b/frontend/src/components/Common/MultiOptionsButton/index.tsx @@ -1,10 +1,11 @@ import { Button, Grid, - Hidden, ListItemText, MenuItem, Theme, + useMediaQuery, + useTheme, withStyles, } from '@material-ui/core'; import Menu, { MenuProps } from '@material-ui/core/Menu'; @@ -55,6 +56,8 @@ interface IProps { function MultiOptionsButton({ mainLabel, options }: IProps) { const [anchorEl, setAnchorEl] = useState(null); const { t } = useSafeTranslation(); + const theme = useTheme(); + const smDown = useMediaQuery(theme.breakpoints.down('sm')); const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); @@ -77,7 +80,7 @@ function MultiOptionsButton({ mainLabel, options }: IProps) { fullWidth onClick={handleClick} > - {t(mainLabel)} + {!smDown && <>{t(mainLabel)}} { const styles = makeStyles(theme); // The rendered definitions - const renderedDefinitions = useMemo(() => { - return definition.map(item => { - return ( + const renderedDefinitions = useMemo( + () => + definition.map(item => ( {item.value} - ); - }); - }, [definition, styles.legendContent, styles.legendText]); + )), + [definition, styles.legendContent, styles.legendText], + ); return ( diff --git a/frontend/src/components/Common/ReportDialog/ReportDocTable.tsx b/frontend/src/components/Common/ReportDialog/ReportDocTable.tsx index bdb0b89d56..dd49950022 100644 --- a/frontend/src/components/Common/ReportDialog/ReportDocTable.tsx +++ b/frontend/src/components/Common/ReportDialog/ReportDocTable.tsx @@ -1,8 +1,8 @@ -import React, { memo, useCallback, useMemo } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { Theme } from '@material-ui/core'; import { StyleSheet, Text, View } from '@react-pdf/renderer'; import { chunk } from 'lodash'; -import { TFunction, getRoundedData } from 'utils/data-utils'; +import { getRoundedData } from 'utils/data-utils'; import { TableRow as AnalysisTableRow } from 'context/analysisResultStateSlice'; import { Column } from 'utils/analysis-utils'; import { FIRST_PAGE_TABLE_ROWS, MAX_TABLE_ROWS_PER_PAGE } from './types'; @@ -34,7 +34,6 @@ interface TableProps { cellWidth: string; showTotal: boolean; showRowTotal: boolean; - t: TFunction; } const ReportDocTable = memo( @@ -49,61 +48,64 @@ const ReportDocTable = memo( }: TableProps) => { const styles = makeStyles(theme); - const totals = useMemo(() => { - return showTotal - ? columns.reduce((colPrev, colCurr) => { - const rowTotal = rows.reduce((rowPrev, rowCurr) => { - const val = Number(rowCurr[colCurr.id]); - if (!Number.isNaN(val)) { - return rowPrev + val; - } - return rowPrev; - }, 0); - return [...colPrev, rowTotal]; - }, []) - : []; - }, [columns, rows, showTotal]); + const totals = useMemo( + () => + showTotal + ? columns.reduce((colPrev, colCurr) => { + const rowTotal = rows.reduce((rowPrev, rowCurr) => { + const val = Number(rowCurr[colCurr.id]); + if (!Number.isNaN(val)) { + return rowPrev + val; + } + return rowPrev; + }, 0); + return [...colPrev, rowTotal]; + }, []) + : [], + [columns, rows, showTotal], + ); - const firstPageChunk = useMemo(() => { - return rows.slice(0, FIRST_PAGE_TABLE_ROWS); - }, [rows]); + const firstPageChunk = useMemo( + () => rows.slice(0, FIRST_PAGE_TABLE_ROWS), + [rows], + ); - const restPagesChunks = useMemo(() => { - return chunk(rows.slice(FIRST_PAGE_TABLE_ROWS), MAX_TABLE_ROWS_PER_PAGE); - }, [rows]); + const restPagesChunks = useMemo( + () => chunk(rows.slice(FIRST_PAGE_TABLE_ROWS), MAX_TABLE_ROWS_PER_PAGE), + [rows], + ); - const chunks = useMemo(() => { - return [firstPageChunk, ...restPagesChunks]; - }, [firstPageChunk, restPagesChunks]); + const chunks = useMemo( + () => [firstPageChunk, ...restPagesChunks], + [firstPageChunk, restPagesChunks], + ); // the table row color const getTableRowColor = useCallback( - (rowIndex: number) => { - return rowIndex % 2 + (rowIndex: number) => + rowIndex % 2 ? theme.pdf?.table?.darkRowColor - : theme.pdf?.table?.lightRowColor; - }, + : theme.pdf?.table?.lightRowColor, [theme.pdf], ); // Gets the total sum of row const getRowTotal = useCallback( - (rowData: AnalysisTableRow) => { - return columns.reduce((prev, curr) => { + (rowData: AnalysisTableRow) => + columns.reduce((prev, curr) => { const val = rowData[curr.id]; if (!Number.isNaN(Number(val))) { return prev + Number(val); } return prev; - }, 0); - }, + }, 0), [columns], ); // The rendered tableCell values const renderedTableCellValues = useCallback( - (rowData: AnalysisTableRow) => { - return columns.map((column: Column) => { + (rowData: AnalysisTableRow) => + columns.map((column: Column) => { const value = rowData[column.id]; return ( ); - }); - }, + }), [cellWidth, columns, styles.tableCell], ); // The rendered total row const renderedTotalRow = useCallback( - rowData => { + (rowData: any) => { if (!showRowTotal) { return null; } @@ -137,23 +138,20 @@ const ReportDocTable = memo( // The rendered table row const renderedTableRow = useCallback( - (tableRow: AnalysisTableRow[]) => { - return tableRow.map((rowData, index) => { - return ( - - {renderedTableCellValues(rowData)} - {renderedTotalRow(rowData)} - - ); - }); - }, + (tableRow: AnalysisTableRow[]) => + tableRow.map((rowData, index) => ( + + {renderedTableCellValues(rowData)} + {renderedTotalRow(rowData)} + + )), [ getTableRowColor, renderedTableCellValues, @@ -163,9 +161,9 @@ const ReportDocTable = memo( ); // The rendered table view - const renderedTableView = useMemo(() => { - return chunks.map(chunkRow => { - return ( + const renderedTableView = useMemo( + () => + chunks.map(chunkRow => ( {renderedTableRow(chunkRow)} - ); - }); - }, [ - cellWidth, - chunks, - columns, - name, - renderedTableRow, - showRowTotal, - theme, - ]); + )), + [cellWidth, chunks, columns, name, renderedTableRow, showRowTotal, theme], + ); // gets the total row color - const totalRowBackgroundColor = useMemo(() => { - return rows.length % 2 - ? theme.pdf?.table?.darkRowColor - : theme.pdf?.table?.lightRowColor; - }, [rows.length, theme.pdf]); + const totalRowBackgroundColor = useMemo( + () => + rows.length % 2 + ? theme.pdf?.table?.darkRowColor + : theme.pdf?.table?.lightRowColor, + [rows.length, theme.pdf], + ); // The total row - const totalRow = useMemo(() => { - return totals.map((val, index) => { - if (index === 0) { + const totalRow = useMemo( + () => + totals.map((val, index) => { + if (index === 0) { + return ( + + Total + + ); + } return ( - Total + {getRoundedData(val)} ); - } - return ( - - {getRoundedData(val)} - - ); - }); - }, [cellWidth, styles.tableCell, totals]); + }), + [cellWidth, styles.tableCell, totals], + ); - const totalsNumberForTotalRow = useMemo(() => { - return totals.reduce((prev, cur) => { - return prev + cur; - }, 0); - }, [totals]); + const totalsNumberForTotalRow = useMemo( + () => totals.reduce((prev, cur) => prev + cur, 0), + [totals], + ); const renderedLastRowTotal = useMemo(() => { if (!showRowTotal) { diff --git a/frontend/src/components/Common/ReportDialog/ReportDocTableHeader.tsx b/frontend/src/components/Common/ReportDialog/ReportDocTableHeader.tsx index 263bcdbe3c..afe39d3b4c 100644 --- a/frontend/src/components/Common/ReportDialog/ReportDocTableHeader.tsx +++ b/frontend/src/components/Common/ReportDialog/ReportDocTableHeader.tsx @@ -1,4 +1,4 @@ -import React, { memo, useMemo } from 'react'; +import { memo, useMemo } from 'react'; import { StyleSheet, Text, View } from '@react-pdf/renderer'; import { Theme } from '@material-ui/core'; import { Column } from 'utils/analysis-utils'; @@ -39,18 +39,18 @@ const ReportDocTableHeader = memo( const styles = makeStyles(theme); // The rendered table header columns - const renderedTableHeaderColumns = useMemo(() => { - return columns.map((column: Column) => { - return ( + const renderedTableHeaderColumns = useMemo( + () => + columns.map((column: Column) => ( {column.label} - ); - }); - }, [cellWidth, columns, styles.tableCell]); + )), + [cellWidth, columns, styles.tableCell], + ); // Whether to show the total columns number const renderedTotalColumnsNumber = useMemo(() => { diff --git a/frontend/src/components/Common/ReportDialog/index.tsx b/frontend/src/components/Common/ReportDialog/index.tsx index 549c6692e7..c5fc8b7eca 100644 --- a/frontend/src/components/Common/ReportDialog/index.tsx +++ b/frontend/src/components/Common/ReportDialog/index.tsx @@ -1,5 +1,5 @@ -import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { + makeStyles, Box, Button, createStyles, @@ -11,9 +11,9 @@ import { Theme, Typography, useTheme, - WithStyles, - withStyles, } from '@material-ui/core'; + +import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import { ArrowBack } from '@material-ui/icons'; import { PDFDownloadLink, PDFViewer } from '@react-pdf/renderer'; @@ -34,14 +34,8 @@ import ReportDoc from './reportDoc'; type Format = 'png' | 'jpeg'; const ReportDialog = memo( - ({ - classes, - open, - reportConfig, - handleClose, - tableData, - columns, - }: ReportProps) => { + ({ open, reportConfig, handleClose, tableData, columns }: ReportProps) => { + const classes = useStyles(); const theme = useTheme(); const { t } = useSafeTranslation(); const [mapImage, setMapImage] = useState(null); @@ -51,14 +45,16 @@ const ReportDialog = memo( analysisResultSelector, ) as ExposedPopulationResult; - const reportDate = useMemo(() => { - return analysisResult?.analysisDate - ? getFormattedDate( - new Date(analysisResult?.analysisDate).toISOString(), - 'default', - ) - : ''; - }, [analysisResult]); + const reportDate = useMemo( + () => + analysisResult?.analysisDate + ? getFormattedDate( + new Date(analysisResult?.analysisDate).toISOString(), + 'default', + ) + : '', + [analysisResult], + ); const getPDFName = useMemo(() => { const type = snakeCase(analysisResult?.legendText); @@ -81,17 +77,6 @@ const ReportDialog = memo( [selectedMap], ); - // Manual loader wait to show that the document is loading - useEffect(() => { - const loadingTimer = setTimeout(() => { - setDocumentIsLoading(false); - }, 15000); - if (!open) { - return clearTimeout(loadingTimer); - } - return () => clearTimeout(loadingTimer); - }, [open]); - useEffect(() => { if (!open) { return; @@ -133,37 +118,16 @@ const ReportDialog = memo( theme, ]); - const renderedPdfDocumentLoading = useMemo(() => { - if (!documentIsLoading) { - return null; - } - return ( - - - {t('Loading document')} - - - - ); - }, [ - classes.documentLoaderText, - classes.documentLoadingContainer, - documentIsLoading, - t, - ]); - const renderedLoadingButtonText = useCallback( - ({ loading }) => { - if (loading || documentIsLoading) { + ({ loading }: any) => { + if (loading) { + setDocumentIsLoading(true); return `${t('Loading document')}...`; } + setDocumentIsLoading(false); return t('Download'); }, - [documentIsLoading, t], + [t], ); const renderedDownloadPdfButton = useMemo(() => { @@ -171,27 +135,25 @@ const ReportDialog = memo( return null; } return ( - <> - - + ); }, [ analysisResult, @@ -206,11 +168,13 @@ const ReportDialog = memo( theme, ]); - const renderedSignatureText = useMemo(() => { - return reportConfig?.signatureText - ? t(reportConfig.signatureText) - : t('PRISM automated report'); - }, [reportConfig, t]); + const renderedSignatureText = useMemo( + () => + reportConfig?.signatureText + ? t(reportConfig.signatureText) + : t('PRISM automated report'), + [reportConfig, t], + ); return ( - {renderedPdfDocumentLoading} + {documentIsLoading && ( + + + {t('Loading document')} + + + + )} {renderedPdfViewer} @@ -251,9 +226,10 @@ const ReportDialog = memo( }, ); -const styles = (theme: Theme) => +const useStyles = makeStyles((theme: Theme) => createStyles({ documentLoadingContainer: { + zIndex: 1000, backgroundColor: 'white', display: 'flex', justifyContent: 'center', @@ -301,9 +277,10 @@ const styles = (theme: Theme) => fontWeight: 500, paddingLeft: '1em', }, - }); + }), +); -export interface ReportProps extends WithStyles { +export interface ReportProps { open: boolean; reportConfig: ReportType; handleClose: () => void; @@ -311,4 +288,4 @@ export interface ReportProps extends WithStyles { columns: Column[]; } -export default withStyles(styles)(ReportDialog); +export default ReportDialog; diff --git a/frontend/src/components/Common/ReportDialog/reportDoc.tsx b/frontend/src/components/Common/ReportDialog/reportDoc.tsx index 1e961e47c0..431e9d3995 100644 --- a/frontend/src/components/Common/ReportDialog/reportDoc.tsx +++ b/frontend/src/components/Common/ReportDialog/reportDoc.tsx @@ -1,7 +1,6 @@ -import React, { memo, useMemo } from 'react'; +import { memo, useMemo } from 'react'; import { Document, - Font, Image, Page, StyleSheet, @@ -13,25 +12,12 @@ import { TableRow as AnalysisTableRow } from 'context/analysisResultStateSlice'; import { getLegendItemLabel } from 'components/MapView/utils'; import { LegendDefinition, ReportType } from 'config/types'; import { Column } from 'utils/analysis-utils'; -import RobotoFont from 'fonts/Roboto-Regular.ttf'; -import KhmerFont from 'fonts/Khmer-Regular.ttf'; import { useSafeTranslation } from 'i18n'; import { PDFLegendDefinition } from './types'; import ReportDocLegend from './ReportDocLegend'; import ReportDocTable from './ReportDocTable'; import { getReportFontFamily } from './utils'; -// Register all the fonts necessary -Font.register({ - family: 'Roboto', - src: RobotoFont, -}); - -Font.register({ - family: 'Khmer', - src: KhmerFont, -}); - const makeStyles = (theme: Theme, selectedLanguage: string) => StyleSheet.create({ page: { @@ -105,43 +91,44 @@ const ReportDoc = memo( const styles = makeStyles(theme, i18n.language); - const date = useMemo(() => { - return new Date().toUTCString(); - }, []); + const date = useMemo(() => new Date().toUTCString(), []); - const tableName = useMemo(() => { - return reportConfig?.tableName - ? reportConfig?.tableName - : 'Population Exposure'; - }, [reportConfig]); + const tableName = useMemo( + () => + reportConfig?.tableName + ? reportConfig?.tableName + : 'Population Exposure', + [reportConfig], + ); - const showRowTotal = useMemo(() => { - return columns.length > 2; - }, [columns.length]); + const showRowTotal = useMemo(() => columns.length > 2, [columns.length]); - const tableCellWidth = useMemo(() => { - return `${100 / (columns.length + (showRowTotal ? 1 : 0))}%`; - }, [columns.length, showRowTotal]); + const tableCellWidth = useMemo( + () => `${100 / (columns.length + (showRowTotal ? 1 : 0))}%`, + [columns.length, showRowTotal], + ); - const trimmedTableRows = useMemo(() => { - return tableRowsNum !== undefined - ? tableData.slice(0, tableRowsNum - (tableShowTotal ? 1 : 0)) - : tableData; - }, [tableData, tableRowsNum, tableShowTotal]); + const trimmedTableRows = useMemo( + () => + tableRowsNum !== undefined + ? tableData.slice(0, tableRowsNum - (tableShowTotal ? 1 : 0)) + : tableData, + [tableData, tableRowsNum, tableShowTotal], + ); - const areasLegendDefinition: PDFLegendDefinition[] = useMemo(() => { - return reportConfig.areasLegendDefinition.items.map(areaDefinition => { - return { + const areasLegendDefinition: PDFLegendDefinition[] = useMemo( + () => + reportConfig.areasLegendDefinition.items.map(areaDefinition => ({ value: t(areaDefinition.title), style: [styles.dash, { backgroundColor: areaDefinition.color }], - }; - }); - }, [reportConfig.areasLegendDefinition.items, styles.dash, t]); + })), + [reportConfig.areasLegendDefinition.items, styles.dash, t], + ); - const typeLegendDefinition: PDFLegendDefinition[] = useMemo(() => { - return reportConfig.typeLegendDefinition.items.map( - typeLegendDefinitionItem => { - return { + const typeLegendDefinition: PDFLegendDefinition[] = useMemo( + () => + reportConfig.typeLegendDefinition.items.map( + typeLegendDefinitionItem => ({ value: t(typeLegendDefinitionItem.title), style: [ typeLegendDefinitionItem?.border @@ -154,22 +141,24 @@ const ReportDoc = memo( }), }, ], - }; - }, - ); - }, [ - reportConfig.typeLegendDefinition.items, - styles.borderedBox, - styles.box, - t, - ]); + }), + ), + [ + reportConfig.typeLegendDefinition.items, + styles.borderedBox, + styles.box, + t, + ], + ); - const populationExposureLegendDefinition: PDFLegendDefinition[] = useMemo(() => { - return exposureLegendDefinition.map(item => ({ - value: getLegendItemLabel(t, item), - style: [styles.box, { backgroundColor: item.color, opacity: 0.5 }], - })); - }, [exposureLegendDefinition, styles.box, t]); + const populationExposureLegendDefinition: PDFLegendDefinition[] = useMemo( + () => + exposureLegendDefinition.map(item => ({ + value: getLegendItemLabel(t, item), + style: [styles.box, { backgroundColor: item.color, opacity: 0.5 }], + })), + [exposureLegendDefinition, styles.box, t], + ); const renderedMapFooterText = useMemo(() => { if (!reportConfig?.mapFooterText) { @@ -222,11 +211,13 @@ const ReportDoc = memo( return {t(reportConfig.subText)}; }, [reportConfig, styles.subText, t]); - const renderedSignatureText = useMemo(() => { - return reportConfig?.signatureText - ? t(reportConfig.signatureText) - : t('PRISM automated report'); - }, [reportConfig, t]); + const renderedSignatureText = useMemo( + () => + reportConfig?.signatureText + ? t(reportConfig.signatureText) + : t('PRISM automated report'), + [reportConfig, t], + ); return ( @@ -268,7 +259,6 @@ const ReportDoc = memo( cellWidth={tableCellWidth} showTotal={tableShowTotal} showRowTotal={showRowTotal} - t={t} /> diff --git a/frontend/src/components/Common/ReportDialog/types.ts b/frontend/src/components/Common/ReportDialog/types.ts index 8ce499c0de..034008e0a9 100644 --- a/frontend/src/components/Common/ReportDialog/types.ts +++ b/frontend/src/components/Common/ReportDialog/types.ts @@ -1,8 +1,9 @@ import { Style } from '@react-pdf/types'; -import { TFunctionResult } from 'i18next'; +// import { TFunctionResult } from 'i18next'; +// TODO: export interface PDFLegendDefinition { - value: string | number | TFunctionResult; + value: string | number; // | TFunctionResult; style: Style | Style[]; } diff --git a/frontend/src/components/Common/SimpleDropdown/index.tsx b/frontend/src/components/Common/SimpleDropdown/index.tsx index 3debb04a9c..f8ec76f9e6 100644 --- a/frontend/src/components/Common/SimpleDropdown/index.tsx +++ b/frontend/src/components/Common/SimpleDropdown/index.tsx @@ -1,5 +1,4 @@ import { FormControl, MenuItem, Select, Typography } from '@material-ui/core'; -import React from 'react'; import { useSafeTranslation } from 'i18n'; type OptionLabel = string; diff --git a/frontend/src/components/Common/Switch/index.tsx b/frontend/src/components/Common/Switch/index.tsx index b51cf61995..a3311775bf 100644 --- a/frontend/src/components/Common/Switch/index.tsx +++ b/frontend/src/components/Common/Switch/index.tsx @@ -3,17 +3,12 @@ import { Switch as SwitchUI, Typography, createStyles, + makeStyles, } from '@material-ui/core'; -import { WithStyles, withStyles } from '@material-ui/styles'; import { cyanBlue } from 'muiTheme'; -function Switch({ - checked, - onChange, - classes, - title, - ariaLabel, -}: PrintConfigProps) { +function Switch({ checked, onChange, title, ariaLabel }: PrintConfigProps) { + const classes = useStyles(); return ( <> +const useStyles = makeStyles(() => createStyles({ switch: { padding: '7px', @@ -67,9 +62,10 @@ const styles = () => opacity: 1, }, }, - }); + }), +); -export interface PrintConfigProps extends WithStyles { +export interface PrintConfigProps { checked: boolean; onChange: ( event: React.ChangeEvent, @@ -79,4 +75,4 @@ export interface PrintConfigProps extends WithStyles { ariaLabel?: string; } -export default withStyles(styles)(Switch); +export default Switch; diff --git a/frontend/src/components/Login/index.tsx b/frontend/src/components/Login/index.tsx index 54faf7a4c7..5836a1f58c 100644 --- a/frontend/src/components/Login/index.tsx +++ b/frontend/src/components/Login/index.tsx @@ -1,18 +1,17 @@ -import React from 'react'; import { createStyles, - WithStyles, - withStyles, Typography, Button, Grid, + makeStyles, } from '@material-ui/core'; import { useMsal } from '@azure/msal-react'; import { msalRequest } from 'config'; import { colors } from 'muiTheme'; -const Login = ({ classes }: LoginProps) => { +function Login() { + const classes = useStyles(); const { instance } = useMsal(); return ( @@ -45,9 +44,9 @@ const Login = ({ classes }: LoginProps) => {
); -}; +} -const styles = () => +const useStyles = makeStyles(() => createStyles({ container: { width: '100vw', @@ -66,8 +65,7 @@ const styles = () => width: '90%', opacity: '0.5', }, - }); + }), +); -export interface LoginProps extends WithStyles {} - -export default withStyles(styles)(Login); +export default Login; diff --git a/frontend/src/components/MapView/AlertForm/__snapshots__/index.test.tsx.snap b/frontend/src/components/MapView/AlertForm/__snapshots__/index.test.tsx.snap index 5a753b391c..a2f83e9bb6 100644 --- a/frontend/src/components/MapView/AlertForm/__snapshots__/index.test.tsx.snap +++ b/frontend/src/components/MapView/AlertForm/__snapshots__/index.test.tsx.snap @@ -3,10 +3,10 @@ exports[`renders as expected 1`] = `
@@ -21,7 +21,7 @@ exports[`renders as expected 1`] = ` /> Create Alert @@ -36,13 +36,13 @@ exports[`renders as expected 1`] = ` paperprops="[object Object]" >
()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; +const EMAIL_REGEX: RegExp = + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; // This should probably be determined on a case-by-case basis, // depending on if the downstream API has the capability. @@ -41,7 +42,8 @@ const ALERT_FORM_ENABLED = true; const boundaryLayer = getBoundaryLayerSingleton(); -function AlertForm({ classes, isOpen, setOpen }: AlertFormProps) { +function AlertForm({ isOpen, setOpen }: AlertFormProps) { + const classes = useStyles(); const boundaryLayerData = useSelector(layerDataSelector(boundaryLayer.id)) as | LayerData | undefined; @@ -68,9 +70,10 @@ function AlertForm({ classes, isOpen, setOpen }: AlertFormProps) { return Object.fromEntries( boundaryLayerData.data.features .filter(feature => feature.properties !== null) - .map(feature => { - return [feature.properties?.[boundaryLayer.adminCode], feature]; - }), + .map(feature => [ + feature.properties?.[boundaryLayer.adminCode], + feature, + ]), ); }, [boundaryLayerData]); @@ -84,9 +87,9 @@ function AlertForm({ classes, isOpen, setOpen }: AlertFormProps) { throw new Error('Please select at least one region boundary.'); } - const features = regionsList.map(region => { - return regionCodesToFeatureData[region]; - }); + const features = regionsList.map( + region => regionCodesToFeatureData[region], + ); // Generate a copy of admin layer data (to preserve top-level properties) // and replace the 'features' property with just the selected regions. @@ -105,36 +108,35 @@ function AlertForm({ classes, isOpen, setOpen }: AlertFormProps) { setEmail(newEmail); }; - const onOptionChange = ( - setterFunc: Dispatch>, - ) => (event: React.ChangeEvent) => { - const value = event.target.value as T; - setterFunc(value); - return value; - }; + const onOptionChange = + (setterFunc: Dispatch>) => + (event: React.ChangeEvent) => { + const value = event.target.value as T; + setterFunc(value); + return value; + }; // specially for threshold values, also does error checking const onThresholdOptionChange = useCallback( - (thresholdType: 'above' | 'below') => ( - event: React.ChangeEvent, - ) => { - const setterFunc = - thresholdType === 'above' ? setAboveThreshold : setBelowThreshold; - const changedOption = onOptionChange(setterFunc)(event); - // setting a value doesn't update the existing value until next render, - // therefore we must decide whether to access the old one or the newly change one here. - const aboveThresholdValue = parseFloat( - thresholdType === 'above' ? changedOption : aboveThreshold, - ); - const belowThresholdValue = parseFloat( - thresholdType === 'below' ? changedOption : belowThreshold, - ); - if (belowThresholdValue > aboveThresholdValue) { - setThresholdError('Below threshold is larger than above threshold!'); - } else { - setThresholdError(null); - } - }, + (thresholdType: 'above' | 'below') => + (event: React.ChangeEvent) => { + const setterFunc = + thresholdType === 'above' ? setAboveThreshold : setBelowThreshold; + const changedOption = onOptionChange(setterFunc)(event); + // setting a value doesn't update the existing value until next render, + // therefore we must decide whether to access the old one or the newly change one here. + const aboveThresholdValue = parseFloat( + thresholdType === 'above' ? changedOption : aboveThreshold, + ); + const belowThresholdValue = parseFloat( + thresholdType === 'below' ? changedOption : belowThreshold, + ); + if (belowThresholdValue > aboveThresholdValue) { + setThresholdError('Below threshold is larger than above threshold!'); + } else { + setThresholdError(null); + } + }, [aboveThreshold, belowThreshold], ); @@ -331,7 +333,7 @@ function AlertForm({ classes, isOpen, setOpen }: AlertFormProps) { ); } -const styles = (theme: Theme) => +const useStyles = makeStyles((theme: Theme) => createStyles({ alertLabel: { marginLeft: '10px' }, alertTriggerButton: { @@ -400,11 +402,12 @@ const styles = (theme: Theme) => alertFormResponseText: { marginLeft: '15px', }, - }); + }), +); -interface AlertFormProps extends WithStyles { +interface AlertFormProps { isOpen: boolean; setOpen: (isOpen: boolean) => void; } -export default withStyles(styles)(AlertForm); +export default AlertForm; diff --git a/frontend/src/components/MapView/BoundaryInfoBox/index.tsx b/frontend/src/components/MapView/BoundaryInfoBox/index.tsx index 8e7e1fa284..fdeee61126 100644 --- a/frontend/src/components/MapView/BoundaryInfoBox/index.tsx +++ b/frontend/src/components/MapView/BoundaryInfoBox/index.tsx @@ -1,11 +1,9 @@ -import React from 'react'; import { createStyles, Paper, Theme, TextField, - WithStyles, - withStyles, + makeStyles, } from '@material-ui/core'; import { useSelector } from 'react-redux'; import { @@ -13,7 +11,8 @@ import { zoomSelector, } from 'context/mapBoundaryInfoStateSlice'; -function LocationBox({ classes }: LocationBoxProps) { +function LocationBox() { + const classes = useStyles(); const bounds = useSelector(boundsSelector); const zoom = useSelector(zoomSelector); const boundsStr = bounds ? JSON.stringify(bounds.toArray()) : ''; @@ -33,7 +32,7 @@ function LocationBox({ classes }: LocationBoxProps) { ); } -const styles = (theme: Theme) => +const useStyles = makeStyles((theme: Theme) => createStyles({ container: { position: 'absolute', @@ -43,8 +42,7 @@ const styles = (theme: Theme) => zIndex: theme.zIndex.modal, backgroundColor: theme.palette.primary.main, }, - }); + }), +); -export interface LocationBoxProps extends WithStyles {} - -export default withStyles(styles)(LocationBox); +export default LocationBox; diff --git a/frontend/src/components/MapView/DateSelector/DateSelectorInput/__snapshots__/index.test.tsx.snap b/frontend/src/components/MapView/DateSelector/DateSelectorInput/__snapshots__/index.test.tsx.snap index 0145b6d255..1c563127f8 100644 --- a/frontend/src/components/MapView/DateSelector/DateSelectorInput/__snapshots__/index.test.tsx.snap +++ b/frontend/src/components/MapView/DateSelector/DateSelectorInput/__snapshots__/index.test.tsx.snap @@ -3,7 +3,7 @@ exports[`renders as expected 1`] = `
Some Value diff --git a/frontend/src/components/MapView/DateSelector/DateSelectorInput/index.test.tsx b/frontend/src/components/MapView/DateSelector/DateSelectorInput/index.test.tsx index b021096a8d..78e00f3583 100644 --- a/frontend/src/components/MapView/DateSelector/DateSelectorInput/index.test.tsx +++ b/frontend/src/components/MapView/DateSelector/DateSelectorInput/index.test.tsx @@ -1,5 +1,4 @@ import { render } from '@testing-library/react'; -import React from 'react'; import DateSelectorInput from '.'; test('renders as expected', () => { diff --git a/frontend/src/components/MapView/DateSelector/DateSelectorInput/index.tsx b/frontend/src/components/MapView/DateSelector/DateSelectorInput/index.tsx index 4cb57939db..54022cbc45 100644 --- a/frontend/src/components/MapView/DateSelector/DateSelectorInput/index.tsx +++ b/frontend/src/components/MapView/DateSelector/DateSelectorInput/index.tsx @@ -1,16 +1,12 @@ -import React, { forwardRef, Ref } from 'react'; -import { - Button, - createStyles, - WithStyles, - withStyles, -} from '@material-ui/core'; +import { forwardRef, Ref } from 'react'; +import { Button, createStyles, makeStyles } from '@material-ui/core'; const DateSelectorInput = forwardRef( ( - { value, onClick, classes }: DateSelectorInputProps, + { value, onClick }: DateSelectorInputProps, ref?: Ref, ) => { + const classes = useStyles(); return ( - + )} { includeDates={[...includedDates, today]} /> - + {!smUp && ( - + )} {/* Desktop */} - + {!xsDown && ( - + )} { onDrag={onPointerDrag} >
-
@@ -492,18 +498,18 @@ const DateSelector = memo(({ classes }: DateSelectorProps) => {
- + {!xsDown && ( - + )}
); }); -const styles = (theme: Theme) => +const useStyles = makeStyles((theme: Theme) => createStyles({ container: { position: 'absolute', @@ -581,8 +587,7 @@ const styles = (theme: Theme) => height: '16px', cursor: 'grab', }, - }); - -export interface DateSelectorProps extends WithStyles {} + }), +); -export default withStyles(styles)(DateSelector); +export default DateSelector; diff --git a/frontend/src/components/MapView/DownloadCsvButton/index.tsx b/frontend/src/components/MapView/DownloadCsvButton/index.tsx index a340ac35c7..87891a8229 100644 --- a/frontend/src/components/MapView/DownloadCsvButton/index.tsx +++ b/frontend/src/components/MapView/DownloadCsvButton/index.tsx @@ -1,16 +1,14 @@ import { Button, Typography, - WithStyles, createStyles, - withStyles, + makeStyles, } from '@material-ui/core'; -import React from 'react'; import { t } from 'i18next'; import { downloadChartsToCsv } from 'utils/csv-utils'; import { cyanBlue } from 'muiTheme'; -const styles = () => +const useStyles = makeStyles(() => createStyles({ downloadButton: { backgroundColor: cyanBlue, @@ -22,9 +20,10 @@ const styles = () => width: '50%', '&.Mui-disabled': { opacity: 0.5 }, }, - }); + }), +); -interface DownloadChartCSVButtonProps extends WithStyles { +interface DownloadChartCSVButtonProps { filesData: { fileName: string; data: { [key: string]: any[] }; @@ -32,17 +31,13 @@ interface DownloadChartCSVButtonProps extends WithStyles { disabled?: boolean; } -const DownloadChartCSVButton = ({ +function DownloadChartCSVButton({ filesData, disabled = false, - classes, -}: DownloadChartCSVButtonProps) => { - const buildDataToDownload: () => [ - { [key: string]: any[] }, - string, - ][] = () => { - return filesData.map(fileData => [fileData.data, fileData.fileName]); - }; +}: DownloadChartCSVButtonProps) { + const classes = useStyles(); + const buildDataToDownload: () => [{ [key: string]: any[] }, string][] = () => + filesData.map(fileData => [fileData.data, fileData.fileName]); return ( ); -}; +} -export default withStyles(styles)(DownloadChartCSVButton); +export default DownloadChartCSVButton; diff --git a/frontend/src/components/MapView/Layers/AdminLevelDataLayer/index.tsx b/frontend/src/components/MapView/Layers/AdminLevelDataLayer/index.tsx index 170c067c33..9d731a9015 100644 --- a/frontend/src/components/MapView/Layers/AdminLevelDataLayer/index.tsx +++ b/frontend/src/components/MapView/Layers/AdminLevelDataLayer/index.tsx @@ -1,4 +1,4 @@ -import React, { memo, useEffect } from 'react'; +import { memo, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Source, Layer, MapLayerMouseEvent } from 'react-map-gl/maplibre'; import { @@ -25,164 +25,102 @@ import { import { fillPaintData } from 'components/MapView/Layers/styles'; import { availableDatesSelector } from 'context/serverStateSlice'; import { getPossibleDatesForLayer, getRequestDate } from 'utils/server-utils'; -import { - addPopupParams, - legendToStops, -} from 'components/MapView/Layers/layer-utils'; -import { convertSvgToPngBase64Image, getSVGShape } from 'utils/image-utils'; -import { Map, FillLayerSpecification } from 'maplibre-gl'; +import { addPopupParams } from 'components/MapView/Layers/layer-utils'; +import { FillLayerSpecification } from 'maplibre-gl'; import { opacitySelector } from 'context/opacityStateSlice'; +import { addFillPatternImagesInMap } from './utils'; -export const createFillPatternsForLayerLegends = async ( - layer: AdminLevelDataLayerProps, -) => { - return Promise.all( - legendToStops(layer.legend).map(async (legendToStop, index) => { - return convertSvgToPngBase64Image( - getSVGShape( - legendToStop[1] as string, - layer.fillPattern || layer.legend[index]?.fillPattern, - ), - ); - }), - ); -}; - -export const addFillPatternImageInMap = ( - layer: AdminLevelDataLayerProps, - map: Map | undefined, - index: number, - convertedImage?: string, -) => { - if ( - !map || - !convertedImage || - (!layer.fillPattern && !layer.legend.some(l => l.fillPattern)) - ) { - return; - } - map.loadImage(convertedImage, (err: any, image) => { - // Throw an error if something goes wrong. - if (err) { - throw err; - } - if (!image) { - return; - } - // Add the image to the map style if it doesn't already exist - const imageId = `fill-pattern-${layer.id}-legend-${index}`; - if (!map.hasImage(imageId)) { - // Add the image since it doesn't exist - map.addImage(imageId, image, { pixelRatio: 4 }); - } - }); -}; - -export const addFillPatternImagesInMap = async ( - layer: AdminLevelDataLayerProps, - map: any, -) => { - const fillPatternsForLayer = await createFillPatternsForLayerLegends(layer); - fillPatternsForLayer.forEach((base64Image, index) => { - addFillPatternImageInMap(layer, map, index, base64Image); - }); -}; +const onClick = + ({ + layer, + dispatch, + t, + }: MapEventWrapFunctionProps) => + (evt: MapLayerMouseEvent) => { + addPopupParams(layer, dispatch, evt, t, true); + }; -const onClick = ({ - layer, - dispatch, - t, -}: MapEventWrapFunctionProps) => ( - evt: MapLayerMouseEvent, -) => { - addPopupParams(layer, dispatch, evt, t, true); -}; +const AdminLevelDataLayers = memo( + ({ layer, before }: { layer: AdminLevelDataLayerProps; before?: string }) => { + const dispatch = useDispatch(); + const map = useSelector(mapSelector); + const serverAvailableDates = useSelector(availableDatesSelector); -const AdminLevelDataLayers = ({ - layer, - before, -}: { - layer: AdminLevelDataLayerProps; - before?: string; -}) => { - const dispatch = useDispatch(); - const map = useSelector(mapSelector); - const serverAvailableDates = useSelector(availableDatesSelector); + const boundaryId = layer.boundary || firstBoundaryOnView(map); - const boundaryId = layer.boundary || firstBoundaryOnView(map); + const selectedDate = useDefaultDate(layer.id); + useMapCallback('click', getLayerMapId(layer.id), layer, onClick); + const layerAvailableDates = getPossibleDatesForLayer( + layer, + serverAvailableDates, + ); + const queryDate = getRequestDate(layerAvailableDates, selectedDate); + const opacityState = useSelector(opacitySelector(layer.id)); + const layerData = useSelector(layerDataSelector(layer.id, queryDate)) as + | LayerData + | undefined; + const { data } = layerData || {}; - const selectedDate = useDefaultDate(layer.id); - useMapCallback('click', getLayerMapId(layer.id), layer, onClick); - const layerAvailableDates = getPossibleDatesForLayer( - layer, - serverAvailableDates, - ); - const queryDate = getRequestDate(layerAvailableDates, selectedDate); - const opacityState = useSelector(opacitySelector(layer.id)); - const layerData = useSelector(layerDataSelector(layer.id, queryDate)) as - | LayerData - | undefined; - const { data } = layerData || {}; + useEffect(() => { + addFillPatternImagesInMap(layer, map); + }, [layer, map]); - useEffect(() => { - addFillPatternImagesInMap(layer, map); - }, [layer, map]); + useEffect(() => { + // before loading layer check if it has unique boundary? + const boundaryLayers = getBoundaryLayers(); + const boundaryLayer = LayerDefinitions[ + boundaryId as LayerKey + ] as BoundaryLayerProps; - useEffect(() => { - // before loading layer check if it has unique boundary? - const boundaryLayers = getBoundaryLayers(); - const boundaryLayer = LayerDefinitions[ - boundaryId as LayerKey - ] as BoundaryLayerProps; + if ('boundary' in layer) { + if (Object.keys(LayerDefinitions).includes(boundaryId)) { + boundaryLayers.map(l => dispatch(removeLayer(l))); + dispatch(addLayer({ ...boundaryLayer, isPrimary: true })); - if ('boundary' in layer) { - if (Object.keys(LayerDefinitions).includes(boundaryId)) { - boundaryLayers.map(l => dispatch(removeLayer(l))); - dispatch(addLayer({ ...boundaryLayer, isPrimary: true })); - - // load unique boundary only once - // to avoid double loading which proven to be performance issue - if (!isLayerOnView(map, boundaryId)) { - dispatch(loadLayerData({ layer: boundaryLayer })); + // load unique boundary only once + // to avoid double loading which proven to be performance issue + if (!isLayerOnView(map, boundaryId)) { + dispatch(loadLayerData({ layer: boundaryLayer })); + } + } else { + dispatch( + addNotification({ + message: `Invalid unique boundary: ${boundaryId} for ${layer.id}`, + type: 'error', + }), + ); } - } else { - dispatch( - addNotification({ - message: `Invalid unique boundary: ${boundaryId} for ${layer.id}`, - type: 'error', - }), - ); } - } + if (!data) { + dispatch(loadLayerData({ layer, date: queryDate })); + } + }, [boundaryId, dispatch, data, layer, map, queryDate]); + if (!data) { - dispatch(loadLayerData({ layer, date: queryDate })); + return null; } - }, [boundaryId, dispatch, data, layer, map, queryDate]); - if (!data) { - return null; - } - - if (!isLayerOnView(map, boundaryId)) { - return null; - } + if (!isLayerOnView(map, boundaryId)) { + return null; + } - return ( - - - - ); -}; + return ( + + + + ); + }, +); -export default memo(AdminLevelDataLayers); +export default AdminLevelDataLayers; diff --git a/frontend/src/components/MapView/Layers/AdminLevelDataLayer/utils.ts b/frontend/src/components/MapView/Layers/AdminLevelDataLayer/utils.ts new file mode 100644 index 0000000000..8885110f6e --- /dev/null +++ b/frontend/src/components/MapView/Layers/AdminLevelDataLayer/utils.ts @@ -0,0 +1,58 @@ +import { AdminLevelDataLayerProps } from 'config/types'; +import { Map } from 'maplibre-gl'; +import { legendToStops } from 'components/MapView/Layers/layer-utils'; +import { convertSvgToPngBase64Image, getSVGShape } from 'utils/image-utils'; + +const createFillPatternsForLayerLegends = async ( + layer: AdminLevelDataLayerProps, +) => + Promise.all( + legendToStops(layer.legend).map(async (legendToStop, index) => + convertSvgToPngBase64Image( + getSVGShape( + legendToStop[1] as string, + layer.fillPattern || layer.legend[index]?.fillPattern, + ), + ), + ), + ); + +const addFillPatternImageInMap = ( + layer: AdminLevelDataLayerProps, + map: Map | undefined, + index: number, + convertedImage?: string, +) => { + if ( + !map || + !convertedImage || + (!layer.fillPattern && !layer.legend.some(l => l.fillPattern)) + ) { + return; + } + map.loadImage(convertedImage, (err: any, image) => { + // Throw an error if something goes wrong. + if (err) { + throw err; + } + if (!image) { + return; + } + // Add the image to the map style if it doesn't already exist + const imageId = `fill-pattern-${layer.id}-legend-${index}`; + if (!map.hasImage(imageId)) { + // Add the image since it doesn't exist + map.addImage(imageId, image, { pixelRatio: 4 }); + } + }); +}; + +export const addFillPatternImagesInMap = async ( + layer: AdminLevelDataLayerProps, + map: any, +) => { + const fillPatternsForLayer = await createFillPatternsForLayerLegends(layer); + fillPatternsForLayer.forEach((base64Image, index) => { + addFillPatternImageInMap(layer, map, index, base64Image); + }); +}; diff --git a/frontend/src/components/MapView/Layers/AnalysisLayer/index.tsx b/frontend/src/components/MapView/Layers/AnalysisLayer/index.tsx index 7ec9ea820f..560af2de0e 100644 --- a/frontend/src/components/MapView/Layers/AnalysisLayer/index.tsx +++ b/frontend/src/components/MapView/Layers/AnalysisLayer/index.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { get } from 'lodash'; import { Layer, Source } from 'react-map-gl/maplibre'; import { useSelector } from 'react-redux'; @@ -32,122 +31,122 @@ import { useMapCallback, } from 'utils/map-utils'; import { opacitySelector } from 'context/opacityStateSlice'; -import { invertLegendColors } from 'components/MapView/Legends/LegendItemsList'; import { getFormattedDate } from 'utils/date-utils'; +import { invertLegendColors } from 'components/MapView/Legends/utils'; -export const layerId = getLayerMapId('analysis'); +const layerId = getLayerMapId('analysis'); -const onClick = (analysisData: AnalysisResult | undefined) => ({ - dispatch, - t, -}: MapEventWrapFunctionProps) => (evt: MapLayerMouseEvent) => { - const coordinates = getEvtCoords(evt); +const onClick = + (analysisData: AnalysisResult | undefined) => + ({ dispatch, t }: MapEventWrapFunctionProps) => + (evt: MapLayerMouseEvent) => { + const coordinates = getEvtCoords(evt); - if (!analysisData) { - return; - } + if (!analysisData) { + return; + } - const feature = findFeature(layerId, evt); - if (!feature) { - return; - } + const feature = findFeature(layerId, evt); + if (!feature) { + return; + } - // Statistic Data - if (analysisData instanceof PolygonAnalysisResult) { - const stats = JSON.parse(feature.properties['zonal:stat:classes']); - // keys are the zonal classes like ['60 km/h', 'Uncertainty Cones'] - const keys = Object.keys(stats).filter(key => key !== 'null'); - const popupData = Object.fromEntries( - keys.map(key => [ - key, - { - // we convert the percentage from a number like 0.832 to something that is - // more intuitive and can fit in the popup better like "83%" - data: `${Math.round(stats[key].percentage * 100)}%`, - coordinates, - }, - ]), - ); - dispatch(addPopupData(popupData)); - } else { - const statisticKey = analysisData.statistic; - const precision = - analysisData instanceof ExposedPopulationResult ? 0 : undefined; - const formattedProperties = formatIntersectPercentageAttribute( - feature.properties, - ); - dispatch( - addPopupData({ - [t('Analysis layer')]: { - data: analysisData.getLayerTitle(t), - coordinates, - }, - ...(analysisData.analysisDate - ? { - [t('Date analyzed')]: { - data: getFormattedDate( - analysisData.analysisDate, - 'locale', - ) as string, - coordinates, - }, - } - : {}), - [analysisData.getStatLabel(t)]: { - data: `${getRoundedData( - formattedProperties[statisticKey], - t, - precision, - )} ${units[statisticKey] || ''}`, - coordinates, - }, - }), - ); - if (statisticKey === AggregationOperations['Area exposed']) { + // Statistic Data + if (analysisData instanceof PolygonAnalysisResult) { + const stats = JSON.parse(feature.properties['zonal:stat:classes']); + // keys are the zonal classes like ['60 km/h', 'Uncertainty Cones'] + const keys = Object.keys(stats).filter(key => key !== 'null'); + const popupData = Object.fromEntries( + keys.map(key => [ + key, + { + // we convert the percentage from a number like 0.832 to something that is + // more intuitive and can fit in the popup better like "83%" + data: `${Math.round(stats[key].percentage * 100)}%`, + coordinates, + }, + ]), + ); + dispatch(addPopupData(popupData)); + } else { + const statisticKey = analysisData.statistic; + const precision = + analysisData instanceof ExposedPopulationResult ? 0 : undefined; + const formattedProperties = formatIntersectPercentageAttribute( + feature.properties, + ); dispatch( addPopupData({ - [`${analysisData.getHazardLayer().title} (Area exposed in km²)`]: { + [t('Analysis layer')]: { + data: (analysisData as ExposedPopulationResult).getLayerTitle(t), + coordinates, + }, + ...(analysisData.analysisDate + ? { + [t('Date analyzed')]: { + data: getFormattedDate( + analysisData.analysisDate, + 'locale', + ) as string, + coordinates, + }, + } + : {}), + [analysisData.getStatLabel(t)]: { data: `${getRoundedData( - formattedProperties.stats_intersect_area || null, + formattedProperties[statisticKey], t, precision, - )} ${units.stats_intersect_area}`, + )} ${units[statisticKey] || ''}`, coordinates, }, }), ); + if (statisticKey === AggregationOperations['Area exposed']) { + dispatch( + addPopupData({ + [`${analysisData.getHazardLayer().title} (Area exposed in km²)`]: { + data: `${getRoundedData( + formattedProperties.stats_intersect_area || null, + t, + precision, + )} ${units.stats_intersect_area}`, + coordinates, + }, + }), + ); + } } - } - if (analysisData instanceof BaselineLayerResult) { - const baselineLayer = analysisData.getBaselineLayer(); - if (baselineLayer?.title) { + if (analysisData instanceof BaselineLayerResult) { + const baselineLayer = analysisData.getBaselineLayer(); + if (baselineLayer?.title) { + dispatch( + addPopupData({ + [baselineLayer.title]: { + data: getRoundedData(get(feature, 'properties.data'), t), + coordinates, + }, + }), + ); + } + } + + if (analysisData instanceof ExposedPopulationResult && analysisData.key) { dispatch( addPopupData({ - [baselineLayer.title]: { - data: getRoundedData(get(feature, 'properties.data'), t), + [analysisData.key]: { + // TODO - consider using a simple safeTranslate here instead. + data: getRoundedData( + get(feature, `properties.${analysisData.key}`), + t, + ), coordinates, }, }), ); } - } - - if (analysisData instanceof ExposedPopulationResult && analysisData.key) { - dispatch( - addPopupData({ - [analysisData.key]: { - // TODO - consider using a simple safeTranslate here instead. - data: getRoundedData( - get(feature, `properties.${analysisData.key}`), - t, - ), - coordinates, - }, - }), - ); - } -}; + }; // We use the legend values from the baseline layer function fillPaintData( diff --git a/frontend/src/components/MapView/Layers/AnticipatoryActionLayer/index.tsx b/frontend/src/components/MapView/Layers/AnticipatoryActionLayer/index.tsx index fa57a32878..49a84e997a 100644 --- a/frontend/src/components/MapView/Layers/AnticipatoryActionLayer/index.tsx +++ b/frontend/src/components/MapView/Layers/AnticipatoryActionLayer/index.tsx @@ -44,20 +44,18 @@ import { Tooltip } from '@material-ui/core'; // Use admin level 2 boundary layer for Anticipatory Action const boundaryLayer = getBoundaryLayersByAdminLevel(2); -const onDistrictClick = ({ - dispatch, -}: MapEventWrapFunctionProps) => ( - evt: MapLayerMouseEvent, -) => { - const districtId = - evt.features?.[0]?.properties?.[boundaryLayer.adminLevelLocalNames[1]]; - if (districtId) { - dispatch(setAASelectedDistrict(districtId)); - dispatch(setAAView(AAView.District)); - } -}; +const onDistrictClick = + ({ dispatch }: MapEventWrapFunctionProps) => + (evt: MapLayerMouseEvent) => { + const districtId = + evt.features?.[0]?.properties?.[boundaryLayer.adminLevelLocalNames[1]]; + if (districtId) { + dispatch(setAASelectedDistrict(districtId)); + dispatch(setAAView(AAView.District)); + } + }; -function AnticipatoryActionLayer({ layer, before }: LayersProps) { +const AnticipatoryActionLayer = React.memo(({ layer, before }: LayersProps) => { useDefaultDate(layer.id); const boundaryLayerState = useSelector( layerDataSelector(boundaryLayer.id), @@ -83,9 +81,9 @@ function AnticipatoryActionLayer({ layer, before }: LayersProps) { } if (selectedWindow) { return Object.fromEntries( - Object.entries( - renderedDistricts[selectedWindow], - ).map(([dist, values]) => [dist, values[0]]), + Object.entries(renderedDistricts[selectedWindow]).map( + ([dist, values]) => [dist, values[0]], + ), ); } return {}; @@ -222,11 +220,10 @@ function AnticipatoryActionLayer({ layer, before }: LayersProps) { )} ); -} - +}); export interface LayersProps { layer: AnticipatoryActionLayerProps; before?: string; } -export default React.memo(AnticipatoryActionLayer); +export default AnticipatoryActionLayer; diff --git a/frontend/src/components/MapView/Layers/BoundaryDropdown.tsx b/frontend/src/components/MapView/Layers/BoundaryDropdown.tsx deleted file mode 100644 index aea0d97087..0000000000 --- a/frontend/src/components/MapView/Layers/BoundaryDropdown.tsx +++ /dev/null @@ -1,546 +0,0 @@ -import { - CircularProgress, - FormControl, - InputAdornment, - InputLabel, - makeStyles, - MenuItem, - Select, - SelectProps, - TextField, - TextFieldProps, - Theme, - useMediaQuery, -} from '@material-ui/core'; -import { sortBy } from 'lodash'; -import React, { forwardRef, useEffect, useState } from 'react'; -import i18n from 'i18next'; -import { useDispatch, useSelector } from 'react-redux'; -import { Search } from '@material-ui/icons'; -import bbox from '@turf/bbox'; -import { FixedSizeList as List } from 'react-window'; -import { - BoundaryLayerProps, - AdminCodeString, - AdminLevelType, -} from 'config/types'; -import { - getSelectedBoundaries, - setIsSelectionMode, - setSelectedBoundaries as setSelectedBoundariesRedux, -} from 'context/mapSelectionLayerStateSlice'; -import { getBoundaryLayerSingleton } from 'config/utils'; -import { layerDataSelector } from 'context/mapStateSlice/selectors'; -import { LayerData } from 'context/layers/layer-data'; -import { isEnglishLanguageSelected, useSafeTranslation } from 'i18n'; -import { BBox } from '@turf/helpers'; -import { Map as MaplibreMap } from 'maplibre-gl'; - -const boundaryLayer = getBoundaryLayerSingleton(); - -const useStyles = makeStyles({ - searchField: { - '&>div': { - color: 'black', - }, - }, - formControl: { - width: '140px', - marginLeft: '10px', - }, - icon: { - alignSelf: 'end', - marginBottom: '0.4em', - }, - menuItem0: { - textTransform: 'uppercase', - letterSpacing: '3px', - fontSize: '0.7em', - '&$selected': { - backgroundColor: '#ADD8E6', - }, - }, - menuItem1: { - paddingLeft: '2em', - '&$selected': { - backgroundColor: '#ADD8E6', - }, - }, - menuItem2: { - paddingLeft: '3em', - fontSize: '0.9em', - '&$selected': { - backgroundColor: '#ADD8E6', - }, - }, - menuItem3: { - paddingLeft: '4em', - fontSize: '0.9em', - '&$selected': { - backgroundColor: '#ADD8E6', - }, - }, -}); -const TIMEOUT_ANIMATION_DELAY = 10; -const SearchField = forwardRef( - ( - { - // important this isn't called `value` since this would confuse since this isn't a menu item. - const out = ( - - {labelMessage} - - - ); - - return out; -} - -interface BoundaryDropdownProps { - className: string; - labelMessage?: string; - map?: MaplibreMap | undefined; - onlyNewCategory?: boolean; - selectAll?: boolean; - size?: 'small' | 'medium'; - selectedBoundaries?: AdminCodeString[]; - setSelectedBoundaries?: ( - boundaries: AdminCodeString[], - appendMany?: boolean, - ) => void; - selectProps?: SelectProps; - goto?: boolean; - multiple?: boolean; -} - -/** - * A HOC (higher order component) that connects the boundary dropdown to redux state - */ -function BoundaryDropdown({ - ...rest -}: Omit< - BoundaryDropdownProps, - 'selectedBoundaries' | 'setSelectedBoundaries' | 'labelMessage' | 'selectAll' ->) { - const { t } = useSafeTranslation(); - const isMobile = useMediaQuery((theme: Theme) => - theme.breakpoints.only('xs'), - ); - const labelMessage = t(`${isMobile ? 'Tap' : 'Click'} the map to select`); - - const dispatch = useDispatch(); - const selectedBoundaries = useSelector(getSelectedBoundaries); - // toggle the selection mode as this component is created and destroyed. - // (users can only click the map to select while this component is visible) - useEffect(() => { - dispatch(setIsSelectionMode(true)); - return () => { - dispatch(setIsSelectionMode(false)); - }; - }, [dispatch]); - return ( - { - dispatch(setSelectedBoundariesRedux(newSelectedBoundaries)); - }} - labelMessage={labelMessage} - selectAll - /> - ); -} - -export default BoundaryDropdown; diff --git a/frontend/src/components/MapView/Layers/BoundaryDropdown/BoundaryDropdownOptions.tsx b/frontend/src/components/MapView/Layers/BoundaryDropdown/BoundaryDropdownOptions.tsx new file mode 100644 index 0000000000..9e31ddc171 --- /dev/null +++ b/frontend/src/components/MapView/Layers/BoundaryDropdown/BoundaryDropdownOptions.tsx @@ -0,0 +1,271 @@ +import { + InputAdornment, + makeStyles, + MenuItem, + TextField, + TextFieldProps, +} from '@material-ui/core'; +import React from 'react'; +import { Map as MaplibreMap } from 'maplibre-gl'; +import { useSafeTranslation } from 'i18n'; +import { useSelector } from 'react-redux'; +import { layerDataSelector } from 'context/mapStateSlice/selectors'; +import { getBoundaryLayerSingleton } from 'config/utils'; +import { Search } from '@material-ui/icons'; +import { LayerData } from 'context/layers/layer-data'; +import { FixedSizeList as List } from 'react-window'; +import { BBox } from 'geojson'; +import bbox from '@turf/bbox'; +import { BoundaryLayerProps } from 'config/types'; +import { + BoundaryDropdownProps, + flattenAreaTree, + getAdminBoundaryTree, + TIMEOUT_ANIMATION_DELAY, +} from './utils'; + +const boundaryLayer = getBoundaryLayerSingleton(); + +const SearchField = React.forwardRef( + ( + { + // important this isn't called `value` since this would confuse since this isn't a menu item. + const out = ( + + {labelMessage} + + + ); + + return out; +} + +/** + * A HOC (higher order component) that connects the boundary dropdown to redux state + */ +function BoundaryDropdown({ + ...rest +}: Omit< + BoundaryDropdownProps, + 'selectedBoundaries' | 'setSelectedBoundaries' | 'labelMessage' | 'selectAll' +>) { + const { t } = useSafeTranslation(); + const isMobile = useMediaQuery((theme: Theme) => + theme.breakpoints.only('xs'), + ); + const labelMessage = t(`${isMobile ? 'Tap' : 'Click'} the map to select`); + + const dispatch = useDispatch(); + const selectedBoundaries = useSelector(getSelectedBoundaries); + // toggle the selection mode as this component is created and destroyed. + // (users can only click the map to select while this component is visible) + React.useEffect(() => { + dispatch(setIsSelectionMode(true)); + return () => { + dispatch(setIsSelectionMode(false)); + }; + }, [dispatch]); + return ( + { + dispatch(setSelectedBoundariesRedux(newSelectedBoundaries)); + }} + labelMessage={labelMessage} + selectAll + /> + ); +} + +export default BoundaryDropdown; diff --git a/frontend/src/components/MapView/Layers/BoundaryDropdown/utils.ts b/frontend/src/components/MapView/Layers/BoundaryDropdown/utils.ts new file mode 100644 index 0000000000..4f7dfe03d2 --- /dev/null +++ b/frontend/src/components/MapView/Layers/BoundaryDropdown/utils.ts @@ -0,0 +1,160 @@ +import { SelectProps } from '@material-ui/core'; +import { + AdminCodeString, + AdminLevelType, + BoundaryLayerProps, +} from 'config/types'; +import { LayerData } from 'context/layers/layer-data'; +import { isEnglishLanguageSelected } from 'i18n'; +import i18n from 'i18next'; +import { Map as MaplibreMap } from 'maplibre-gl'; +import { sortBy } from 'lodash'; + +/** + * A tree of admin boundary areas, starting from + * a single "root" element. + */ +export interface AdminBoundaryTree { + label: string; + key: AdminCodeString; // FIXME: duplicate of adminCode below? + adminCode: AdminCodeString; + level: AdminLevelType; + // children are indexed by AdminCodeStrings, not strings + // but typescript won't allow being more specific + children: { [code: string]: AdminBoundaryTree }; +} + +/** + * Build a tree representing the hierarchy of admin + * boundaries for the given data layer. + */ +export function getAdminBoundaryTree( + data: LayerData['data'] | undefined, + layer: BoundaryLayerProps, + i18nLocale: typeof i18n, +): AdminBoundaryTree { + const locationLevelNames = isEnglishLanguageSelected(i18nLocale) + ? layer.adminLevelNames + : layer.adminLevelLocalNames; + const { adminLevelCodes } = layer; + const { features } = data || {}; + + const rootNode = { + adminCode: 'top' as AdminCodeString, + level: 0 as AdminLevelType, + key: 'root' as AdminCodeString, + label: 'Placeholder tree element', + children: {}, + }; + if (features === undefined) { + return rootNode; + } + + const addBranchToTree = ( + partialTree: AdminBoundaryTree, + levelsLeft: AdminCodeString[], + feature: any, // TODO: maplibre: feature + level: AdminLevelType, + ): AdminBoundaryTree => { + const fp = feature.properties; + if (levelsLeft.length === 0) { + return partialTree; + } + const [currentLevelCode, ...otherLevelsCodes] = levelsLeft; + const newBranch = addBranchToTree( + partialTree.children[fp[currentLevelCode]] ?? { + adminCode: fp[currentLevelCode], + key: fp[layer.adminLevelNames[level]], + label: fp[locationLevelNames[level]], + level: (level + 1) as AdminLevelType, + children: {}, + }, + otherLevelsCodes, + feature, + (level + 1) as AdminLevelType, + ); + const newChildren = { + ...partialTree.children, + [fp[currentLevelCode]]: newBranch, + }; + return { ...partialTree, children: newChildren }; + }; + + return features.reduce( + (outputTree, feature) => + addBranchToTree( + outputTree, + adminLevelCodes, + feature, + 0 as AdminLevelType, + ), + rootNode, + ); +} + +export interface BoundaryDropdownProps { + className: string; + labelMessage?: string; + map?: MaplibreMap | undefined; + selectAll?: boolean; + size?: 'small' | 'medium'; + selectedBoundaries?: AdminCodeString[]; + setSelectedBoundaries?: ( + boundaries: AdminCodeString[], + appendMany?: boolean, + ) => void; + selectProps?: SelectProps; + goto?: boolean; + multiple?: boolean; +} + +export const TIMEOUT_ANIMATION_DELAY = 10; + +/** + * Flattened version of the tree above, used to build + * dropdowns. + */ +interface FlattenedAdminBoundary { + label: string; + key: AdminCodeString; + adminCode: AdminCodeString; + level: AdminLevelType; +} + +/** + * Flatten an admin tree into a list of admin areas, sorted + * "as you would expect": sub-areas follow their parent area, + * ordered alphabetically. + * Returned array includes parents and children of matched + * elements. + */ +export function flattenAreaTree( + tree: AdminBoundaryTree, + search: string = '', +): FlattenedAdminBoundary[] { + function flattenSubTree( + localSearch: string, + subTree: AdminBoundaryTree, + ): FlattenedAdminBoundary[] { + const { children, ...node } = subTree; + // if current node matches the search string, include it and all its children + // without filtering them, otherwise keep searching through the children + const boundFlatten = node.label + .toLowerCase() + .includes(localSearch.toLowerCase()) + ? flattenSubTree.bind(null, '') + : flattenSubTree.bind(null, localSearch); + const childrenToShow: FlattenedAdminBoundary[] = sortBy( + Object.values(children), + 'label', + ).flatMap(boundFlatten); + if ( + childrenToShow.length > 0 || + node.label.toLowerCase().includes(localSearch.toLowerCase()) + ) { + return [node, childrenToShow].flat(); + } + return childrenToShow.flat(); + } + return flattenSubTree(search, tree); +} diff --git a/frontend/src/components/MapView/Layers/BoundaryLayer/index.tsx b/frontend/src/components/MapView/Layers/BoundaryLayer/index.tsx index e266cccd6b..12f41421d1 100644 --- a/frontend/src/components/MapView/Layers/BoundaryLayer/index.tsx +++ b/frontend/src/components/MapView/Layers/BoundaryLayer/index.tsx @@ -1,4 +1,4 @@ -import React, { memo, useEffect, useState } from 'react'; +import { memo, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { BoundaryLayerProps, MapEventWrapFunctionProps } from 'config/types'; import { LayerData } from 'context/layers/layer-data'; @@ -36,55 +36,51 @@ interface ComponentProps { before?: string; } -const onClick = ({ - dispatch, - layer, - t, -}: MapEventWrapFunctionProps) => ( - evt: MapLayerMouseEvent, -) => { - const isPrimaryLayer = isPrimaryBoundaryLayer(layer); - if (!isPrimaryLayer) { - return; - } - - const layerId = getLayerMapId(layer.id, 'fill'); - - const feature = findFeature(layerId, evt); - if (!feature) { - return; - } - - // send the selection to the map selection layer. No-op if selection mode isn't on. - dispatch(toggleSelectedBoundary(feature.properties[layer.adminCode])); +const onClick = + ({ dispatch, layer }: MapEventWrapFunctionProps) => + (evt: MapLayerMouseEvent) => { + const isPrimaryLayer = isPrimaryBoundaryLayer(layer); + if (!isPrimaryLayer) { + return; + } - const coordinates = getEvtCoords(evt); - const locationSelectorKey = layer.adminCode; - const locationAdminCode = feature.properties[layer.adminCode]; - const locationName = getFullLocationName(layer.adminLevelNames, feature); + const layerId = getLayerMapId(layer.id, 'fill'); - const locationLocalName = getFullLocationName( - layer.adminLevelLocalNames, - feature, - ); + const feature = findFeature(layerId, evt); + if (!feature) { + return; + } - dispatch( - showPopup({ - coordinates, - locationSelectorKey, - locationAdminCode, - locationName, - locationLocalName, - }), - ); -}; + // send the selection to the map selection layer. No-op if selection mode isn't on. + dispatch(toggleSelectedBoundary(feature.properties[layer.adminCode])); + + const coordinates = getEvtCoords(evt); + const locationSelectorKey = layer.adminCode; + const locationAdminCode = feature.properties[layer.adminCode]; + const locationName = getFullLocationName(layer.adminLevelNames, feature); + + const locationLocalName = getFullLocationName( + layer.adminLevelLocalNames, + feature, + ); + + dispatch( + showPopup({ + coordinates, + locationSelectorKey, + locationAdminCode, + locationName, + locationLocalName, + }), + ); + }; const onMouseEnter = () => (evt: MapLayerMouseEvent) => onToggleHover('pointer', evt.target); const onMouseLeave = () => (evt: MapLayerMouseEvent) => onToggleHover('', evt.target); -const BoundaryLayer = ({ layer, before }: ComponentProps) => { +const BoundaryLayer = memo(({ layer, before }: ComponentProps) => { const dispatch = useDispatch(); const selectedMap = useSelector(mapSelector); const [isZoomLevelSufficient, setIsZoomLevelSufficient] = useState( @@ -166,6 +162,6 @@ const BoundaryLayer = ({ layer, before }: ComponentProps) => { /> ); -}; +}); -export default memo(BoundaryLayer); +export default BoundaryLayer; diff --git a/frontend/src/components/MapView/Layers/CompositeLayer/index.tsx b/frontend/src/components/MapView/Layers/CompositeLayer/index.tsx index 944e3be6ed..18d0012498 100644 --- a/frontend/src/components/MapView/Layers/CompositeLayer/index.tsx +++ b/frontend/src/components/MapView/Layers/CompositeLayer/index.tsx @@ -1,8 +1,7 @@ -import { WithStyles, createStyles, withStyles } from '@material-ui/core'; import { CompositeLayerProps, LegendDefinition } from 'config/types'; import { LayerData, loadLayerData } from 'context/layers/layer-data'; import { layerDataSelector } from 'context/mapStateSlice/selectors'; -import React, { memo, useEffect, useState } from 'react'; +import { memo, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Source, Layer } from 'react-map-gl/maplibre'; import { getLayerMapId } from 'utils/map-utils'; @@ -17,9 +16,7 @@ import { geoToH3, h3ToGeoBoundary } from 'h3-js'; // ts-ignore import { opacitySelector } from 'context/opacityStateSlice'; import { legendToStops } from '../layer-utils'; -const styles = () => createStyles({}); - -interface Props extends WithStyles { +interface Props { layer: CompositeLayerProps; before?: string; } @@ -40,7 +37,7 @@ const paintProps: ( ], }); -const CompositeLayer = ({ layer, before }: Props) => { +const CompositeLayer = memo(({ layer, before }: Props) => { // look to refacto with impactLayer and maybe other layers const [adminBoundaryLimitPolygon, setAdminBoundaryPolygon] = useState(null); const selectedDate = useDefaultDate(layer.dateLayer); @@ -49,16 +46,16 @@ const CompositeLayer = ({ layer, before }: Props) => { const dispatch = useDispatch(); const { data } = - (useSelector(layerDataSelector(layer.id)) as LayerData< - CompositeLayerProps - >) || {}; + (useSelector( + layerDataSelector(layer.id), + ) as LayerData) || {}; const layerAvailableDates = serverAvailableDates[layer.dateLayer]; const queryDate = getRequestDate(layerAvailableDates, selectedDate); useEffect(() => { // admin-boundary-unified-polygon.json is generated using "yarn preprocess-layers" - // which runs ./scripts/preprocess-layers.js + // which runs ./src/scripts/preprocess-layers.js fetch(`data/${safeCountry}/admin-boundary-unified-polygon.json`) .then(response => response.json()) .then(polygonData => setAdminBoundaryPolygon(polygonData)) @@ -118,6 +115,6 @@ const CompositeLayer = ({ layer, before }: Props) => { } return null; -}; +}); -export default memo(withStyles(styles)(CompositeLayer)); +export default CompositeLayer; diff --git a/frontend/src/components/MapView/Layers/ImpactLayer/index.tsx b/frontend/src/components/MapView/Layers/ImpactLayer/index.tsx index 185c28b88c..d7274c47d6 100644 --- a/frontend/src/components/MapView/Layers/ImpactLayer/index.tsx +++ b/frontend/src/components/MapView/Layers/ImpactLayer/index.tsx @@ -1,7 +1,7 @@ -import React, { memo, useEffect } from 'react'; +import { memo, useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { get } from 'lodash'; -import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core'; +import { createStyles, Theme, makeStyles } from '@material-ui/core'; import { getExtent, Extent } from 'components/MapView/Layers/raster-utils'; import { legendToStops } from 'components/MapView/Layers/layer-utils'; import { ImpactLayerProps, MapEventWrapFunctionProps } from 'config/types'; @@ -41,58 +41,55 @@ function getHazardData(evt: any, operation: string, t?: i18nTranslator) { return getRoundedData(data, t); } -const onClick = ({ - layer, - t, - dispatch, -}: MapEventWrapFunctionProps) => ( - evt: MapLayerMouseEvent, -) => { - const hazardLayerDef = LayerDefinitions[layer.hazardLayer]; - const operation = layer.operation || 'median'; - const hazardTitle = `${ - hazardLayerDef.title ? t(hazardLayerDef.title) : '' - } (${t(operation)})`; - - const layerId = getLayerMapId(layer.id); - const feature = findFeature(layerId, evt); - if (!feature) { - return; - } +const onClick = + ({ layer, t, dispatch }: MapEventWrapFunctionProps) => + (evt: MapLayerMouseEvent) => { + const hazardLayerDef = LayerDefinitions[layer.hazardLayer]; + const operation = layer.operation || 'median'; + const hazardTitle = `${ + hazardLayerDef.title ? t(hazardLayerDef.title) : '' + } (${t(operation)})`; + + const layerId = getLayerMapId(layer.id); + const feature = findFeature(layerId, evt); + if (!feature) { + return; + } - const coordinates = getEvtCoords(evt); + const coordinates = getEvtCoords(evt); - const popupData = { - [layer.title]: { - data: getRoundedData(get(feature, 'properties.impactValue'), t), - coordinates, - }, - [hazardTitle]: { - data: getHazardData(evt, operation, t), - coordinates, - }, - }; - // by default add `impactValue` to the tooltip - dispatch(addPopupData(popupData)); - // then add feature_info_props as extra fields to the tooltip - dispatch( - addPopupData( - getFeatureInfoPropsData( - layer.featureInfoProps || {}, + const popupData = { + [layer.title]: { + data: getRoundedData(get(feature, 'properties.impactValue'), t), + coordinates, + }, + [hazardTitle]: { + data: getHazardData(evt, operation, t), coordinates, - feature, + }, + }; + // by default add `impactValue` to the tooltip + dispatch(addPopupData(popupData)); + // then add feature_info_props as extra fields to the tooltip + dispatch( + addPopupData( + getFeatureInfoPropsData( + layer.featureInfoProps || {}, + coordinates, + feature, + ), ), - ), - ); -}; + ); + }; -const ImpactLayer = ({ classes, layer, before }: ComponentProps) => { +const ImpactLayer = memo(({ layer, before }: ComponentProps) => { + const classes = useStyles(); const map = useSelector(mapSelector); const { startDate: selectedDate } = useSelector(dateRangeSelector); const { data, date } = - (useSelector(layerDataSelector(layer.id, selectedDate)) as LayerData< - ImpactLayerProps - >) || {}; + (useSelector( + layerDataSelector(layer.id, selectedDate), + ) as LayerData) || {}; const dispatch = useDispatch(); const { t } = useSafeTranslation(); const opacityState = useSelector(opacitySelector(layer.id)); @@ -158,9 +155,9 @@ const ImpactLayer = ({ classes, layer, before }: ComponentProps) => { /> ); -}; +}); -const styles = (theme: Theme) => +const useStyles = makeStyles((theme: Theme) => createStyles({ message: { position: 'absolute', @@ -177,11 +174,12 @@ const styles = (theme: Theme) => backgroundColor: theme.palette.grey.A100, borderRadius: theme.spacing(2), }, - }); + }), +); -interface ComponentProps extends WithStyles { +interface ComponentProps { layer: ImpactLayerProps; before?: string; } -export default memo(withStyles(styles)(ImpactLayer)); +export default ImpactLayer; diff --git a/frontend/src/components/MapView/Layers/LayerDropdown.tsx b/frontend/src/components/MapView/Layers/LayerDropdown.tsx index 167acf3ab2..3f9d52d2d6 100644 --- a/frontend/src/components/MapView/Layers/LayerDropdown.tsx +++ b/frontend/src/components/MapView/Layers/LayerDropdown.tsx @@ -7,7 +7,7 @@ import { TextField, Typography, } from '@material-ui/core'; -import React, { ReactElement } from 'react'; +import { ReactElement } from 'react'; import { menuList } from 'components/MapView/LeftPanel/utils'; import { LayerKey, LayerType } from 'config/types'; import { getDisplayBoundaryLayers, LayerDefinitions } from 'config/utils'; @@ -57,7 +57,7 @@ function LayerDropdown({ const adminBoundaries = getDisplayBoundaryLayers(); const AdminBoundaryCategory = { title: 'Admin Levels', - layers: adminBoundaries.map((aboundary, index) => ({ + layers: adminBoundaries.map((aboundary, _index) => ({ title: t( `Level ${aboundary.adminLevelCodes.length - (multiCountry ? 1 : 0)}`, ), @@ -91,9 +91,9 @@ function LayerDropdown({ if (layerCategory.layers.some(f => f.group)) { const layers = layerCategory.layers.map(layer => { if (layer.group && !layer.group.activateAll) { - return layer.group.layers.map(layerKey => { - return LayerDefinitions[layerKey.id as LayerKey]; - }); + return layer.group.layers.map( + layerKey => LayerDefinitions[layerKey.id as LayerKey], + ); } return layer; }); diff --git a/frontend/src/components/MapView/Layers/PointDataLayer/index.tsx b/frontend/src/components/MapView/Layers/PointDataLayer/index.tsx index efc8da9265..1fcfb1ccd0 100644 --- a/frontend/src/components/MapView/Layers/PointDataLayer/index.tsx +++ b/frontend/src/components/MapView/Layers/PointDataLayer/index.tsx @@ -1,4 +1,4 @@ -import React, { memo, useEffect } from 'react'; +import { memo, useEffect } from 'react'; import { Layer, Source } from 'react-map-gl/maplibre'; import { Point } from 'geojson'; @@ -39,32 +39,28 @@ import { findFeature, getLayerMapId, useMapCallback } from 'utils/map-utils'; import { getFormattedDate } from 'utils/date-utils'; import { geoToH3, h3ToGeoBoundary } from 'h3-js'; -const onClick = ({ - layer, - dispatch, - t, -}: MapEventWrapFunctionProps) => ( - evt: MapLayerMouseEvent, -) => { - addPopupParams(layer, dispatch, evt, t, false); - - const layerId = getLayerMapId(layer.id); - const feature = findFeature(layerId, evt); - if (layer.loader === PointDataLoader.EWS) { - dispatch(clearDataset()); - if (!feature?.properties) { - return; +const onClick = + ({ layer, dispatch, t }: MapEventWrapFunctionProps) => + (evt: MapLayerMouseEvent) => { + addPopupParams(layer, dispatch, evt, t, false); + + const layerId = getLayerMapId(layer.id); + const feature = findFeature(layerId, evt); + if (layer.loader === PointDataLoader.EWS) { + dispatch(clearDataset()); + if (!feature?.properties) { + return; + } + const ewsDatasetParams = createEWSDatasetParams( + feature?.properties, + layer.data, + ); + dispatch(setEWSParams(ewsDatasetParams)); } - const ewsDatasetParams = createEWSDatasetParams( - feature?.properties, - layer.data, - ); - dispatch(setEWSParams(ewsDatasetParams)); - } -}; + }; // Point Data, takes any GeoJSON of points and shows it. -const PointDataLayer = ({ layer, before }: LayersProps) => { +const PointDataLayer = memo(({ layer, before }: LayersProps) => { const layerId = getLayerMapId(layer.id); const selectedDate = useDefaultDate(layer.id); @@ -81,11 +77,8 @@ const PointDataLayer = ({ layer, before }: LayersProps) => { | LayerData | undefined; const dispatch = useDispatch(); - const { - updateHistory, - removeKeyFromUrl, - removeLayerFromUrl, - } = useUrlHistory(); + const { updateHistory, removeKeyFromUrl, removeLayerFromUrl } = + useUrlHistory(); const { data } = layerData || {}; @@ -222,11 +215,11 @@ const PointDataLayer = ({ layer, before }: LayersProps) => { /> ); -}; +}); export interface LayersProps { layer: PointDataLayerProps; before?: string; } -export default memo(PointDataLayer); +export default PointDataLayer; diff --git a/frontend/src/components/MapView/Layers/SelectionLayer/index.tsx b/frontend/src/components/MapView/Layers/SelectionLayer/index.tsx index b707973b95..61ca3fd466 100644 --- a/frontend/src/components/MapView/Layers/SelectionLayer/index.tsx +++ b/frontend/src/components/MapView/Layers/SelectionLayer/index.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { useSelector } from 'react-redux'; import { Layer, Source } from 'react-map-gl/maplibre'; import { diff --git a/frontend/src/components/MapView/Layers/StaticRasterLayer/index.test.tsx b/frontend/src/components/MapView/Layers/StaticRasterLayer/index.test.tsx index 7b3258b8d1..b46fa15510 100644 --- a/frontend/src/components/MapView/Layers/StaticRasterLayer/index.test.tsx +++ b/frontend/src/components/MapView/Layers/StaticRasterLayer/index.test.tsx @@ -1,5 +1,5 @@ import timezoneMock from 'timezone-mock'; -import { createStaticRasterLayerUrl } from '.'; +import { createStaticRasterLayerUrl } from './utils'; import { timezones } from '../../../../../test/helpers'; const f = () => { diff --git a/frontend/src/components/MapView/Layers/StaticRasterLayer/index.tsx b/frontend/src/components/MapView/Layers/StaticRasterLayer/index.tsx index f0294fcb91..47dd316700 100644 --- a/frontend/src/components/MapView/Layers/StaticRasterLayer/index.tsx +++ b/frontend/src/components/MapView/Layers/StaticRasterLayer/index.tsx @@ -1,50 +1,39 @@ import { StaticRasterLayerProps } from 'config/types'; import { opacitySelector } from 'context/opacityStateSlice'; -import React, { memo } from 'react'; +import { memo } from 'react'; import { Layer, Source } from 'react-map-gl/maplibre'; import { useSelector } from 'react-redux'; -import { getFormattedDate } from 'utils/date-utils'; import { getLayerMapId } from 'utils/map-utils'; -import { DateFormat } from 'utils/name-utils'; import { useDefaultDate } from 'utils/useDefaultDate'; +import { createStaticRasterLayerUrl } from './utils'; -export const createStaticRasterLayerUrl = ( - baseUrl: string, - dates: string[] | undefined, - selectedDate: number | undefined, -) => - dates - ? baseUrl.replace( - `{${DateFormat.DefaultSnakeCase}}`, - getFormattedDate(selectedDate, 'snake') as string, - ) - : baseUrl; +const StaticRasterLayer = memo( + ({ + layer: { id, baseUrl, opacity, minZoom, maxZoom, dates }, + before, + }: LayersProps) => { + const selectedDate = useDefaultDate(id); + const url = createStaticRasterLayerUrl(baseUrl, dates, selectedDate); + const opacityState = useSelector(opacitySelector(id)); -const StaticRasterLayer = ({ - layer: { id, baseUrl, opacity, minZoom, maxZoom, dates }, - before, -}: LayersProps) => { - const selectedDate = useDefaultDate(id); - const url = createStaticRasterLayerUrl(baseUrl, dates, selectedDate); - const opacityState = useSelector(opacitySelector(id)); - - return ( - - - - ); -}; + return ( + + + + ); + }, +); export interface LayersProps { layer: StaticRasterLayerProps; before?: string; } -export default memo(StaticRasterLayer); +export default StaticRasterLayer; diff --git a/frontend/src/components/MapView/Layers/StaticRasterLayer/utils.ts b/frontend/src/components/MapView/Layers/StaticRasterLayer/utils.ts new file mode 100644 index 0000000000..31bf202a0b --- /dev/null +++ b/frontend/src/components/MapView/Layers/StaticRasterLayer/utils.ts @@ -0,0 +1,14 @@ +import { getFormattedDate } from 'utils/date-utils'; +import { DateFormat } from 'utils/name-utils'; + +export const createStaticRasterLayerUrl = ( + baseUrl: string, + dates: string[] | undefined, + selectedDate: number | undefined, +) => + dates + ? baseUrl.replace( + `{${DateFormat.DefaultSnakeCase}}`, + getFormattedDate(selectedDate, 'snake') as string, + ) + : baseUrl; diff --git a/frontend/src/components/MapView/Layers/WMSLayer/index.tsx b/frontend/src/components/MapView/Layers/WMSLayer/index.tsx index d66e3cd2e5..4aeb11daed 100644 --- a/frontend/src/components/MapView/Layers/WMSLayer/index.tsx +++ b/frontend/src/components/MapView/Layers/WMSLayer/index.tsx @@ -1,4 +1,4 @@ -import React, { memo } from 'react'; +import { memo } from 'react'; import { useSelector } from 'react-redux'; import { Layer, Source } from 'react-map-gl/maplibre'; import { WMSLayerProps } from 'config/types'; @@ -13,62 +13,64 @@ import { getLayerMapId } from 'utils/map-utils'; import { appConfig } from 'config'; import { opacitySelector } from 'context/opacityStateSlice'; -const WMSLayers = ({ - layer: { id, baseUrl, serverLayerName, additionalQueryParams, opacity }, - before, -}: LayersProps) => { - const selectedDate = useDefaultDate(id); - const serverAvailableDates = useSelector(availableDatesSelector); - const opacityState = useSelector(opacitySelector(id)); +const WMSLayers = memo( + ({ + layer: { id, baseUrl, serverLayerName, additionalQueryParams, opacity }, + before, + }: LayersProps) => { + const selectedDate = useDefaultDate(id); + const serverAvailableDates = useSelector(availableDatesSelector); + const opacityState = useSelector(opacitySelector(id)); - const expansionFactor = 2; - // eslint-disable-next-line - const expandedBoundingBox = expandBoundingBox( - appConfig.map.boundingBox, - expansionFactor, - ); + const expansionFactor = 2; + // @ts-expect-error #TS6133 see TODO bellow + const _expandedBoundingBox = expandBoundingBox( + appConfig.map.boundingBox, + expansionFactor, + ); - if (!selectedDate) { - return null; - } - const layerAvailableDates = serverAvailableDates[id]; - const queryDate = getRequestDate(layerAvailableDates, selectedDate); - const queryDateString = (queryDate ? new Date(queryDate) : new Date()) - .toISOString() - .slice(0, 10); + if (!selectedDate) { + return null; + } + const layerAvailableDates = serverAvailableDates[id]; + const queryDate = getRequestDate(layerAvailableDates, selectedDate); + const queryDateString = (queryDate ? new Date(queryDate) : new Date()) + .toISOString() + .slice(0, 10); - return ( - - - - ); -}; + // refresh tiles every time date changes + key={queryDateString} + tiles={[ + `${getWMSUrl(baseUrl, serverLayerName, { + ...additionalQueryParams, + ...(selectedDate && { + time: queryDateString, + }), + })}&bbox={bbox-epsg-3857}`, + ]} + tileSize={256} + // TODO - activate after reviewing bbox for all countries + // bounds={expandedBoundingBox} + > + + + ); + }, +); export interface LayersProps { layer: WMSLayerProps; before?: string; } -export default memo(WMSLayers); +export default WMSLayers; diff --git a/frontend/src/components/MapView/Layers/layer-utils.tsx b/frontend/src/components/MapView/Layers/layer-utils.tsx index fc5eb5917f..99f08eca88 100644 --- a/frontend/src/components/MapView/Layers/layer-utils.tsx +++ b/frontend/src/components/MapView/Layers/layer-utils.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import Tooltip from '@material-ui/core/Tooltip'; import { get } from 'lodash'; import { Dispatch } from 'redux'; diff --git a/frontend/src/components/MapView/Layers/raster-utils.ts b/frontend/src/components/MapView/Layers/raster-utils.ts index 5fd2f67c48..d1441f96ef 100644 --- a/frontend/src/components/MapView/Layers/raster-utils.ts +++ b/frontend/src/components/MapView/Layers/raster-utils.ts @@ -1,8 +1,8 @@ import bbox from '@turf/bbox'; import booleanPointInPolygon from '@turf/boolean-point-in-polygon'; -import { Feature, MultiPolygon, point } from '@turf/helpers'; +import { point } from '@turf/helpers'; import { buffer } from 'd3-fetch'; -import * as GeoTIFF from 'geotiff'; +import { fromArrayBuffer, GeoTIFFImage } from 'geotiff'; import { createGetMapUrl } from 'prism-common'; import { Dispatch } from 'redux'; import { RASTER_API_URL } from 'utils/constants'; @@ -10,6 +10,7 @@ import { fetchWithTimeout } from 'utils/fetch-with-timeout'; import { LocalError } from 'utils/error-utils'; import { addNotification } from 'context/notificationStateSlice'; import { Map as MaplibreMap } from 'maplibre-gl'; +import { Feature, MultiPolygon } from 'geojson'; export type TransformMatrix = [number, number, number, number, number, number]; export type TypedArray = @@ -32,39 +33,6 @@ export type GeoJsonBoundary = Feature; // GDAL style extent: xmin ymin xmax ymax export type Extent = [number, number, number, number]; -// Placeholder for Geotiff image (since library doesn't contain types) -export type GeoTiffImage = { - getBoundingBox: () => Extent; - getBytesPerPixel: () => number; - getFileDirectory: () => { ModelPixelScale: number[] }; - getHeight: () => number; - getOrigin: () => [number, number, number]; - getResolution: () => [number, number, number]; - getSamplesPerPixel: () => number; - getTiePoints: () => { - i: number; - j: number; - k: number; - x: number; - y: number; - z: number; - }[]; - getTileHeight: () => number; - getTileWidth: () => number; - getWidth: () => number; - pixelIsArea: () => boolean; - readRasters: (options?: { - window?: Extent; - samples?: number[]; - interleave?: boolean; - pool?: number; - width?: number; - height?: number; - resampleMethod?: string; - fillValue?: number | number[]; - }) => Promise; -}; - export function getWMSUrl( baseUrl: string, layerName: string, @@ -82,7 +50,7 @@ export function getWMSUrl( }); } -export function getTransform(geoTiffImage: GeoTiffImage): TransformMatrix { +export function getTransform(geoTiffImage: GeoTIFFImage): TransformMatrix { const tiepoint = geoTiffImage.getTiePoints()[0]; const pixelScale = geoTiffImage.getFileDirectory().ModelPixelScale; return [ @@ -97,8 +65,8 @@ export function getTransform(geoTiffImage: GeoTiffImage): TransformMatrix { export async function loadGeoTiff(path: string) { const raw = await buffer(path); - const tiff = await GeoTIFF.fromArrayBuffer(raw); - const image = (await tiff.getImage()) as GeoTiffImage; + const tiff = await fromArrayBuffer(raw); + const image = await tiff.getImage(); const rasters = await image.readRasters(); const transform = getTransform(image); return { image, rasters, transform }; @@ -135,7 +103,7 @@ export function geoCoordsToRowCol( export function featureIntersectsImage( feature: GeoJsonBoundary, - image: GeoTiffImage, + image: GeoTIFFImage, ) { const featureExtent = bbox(feature); const imageExtent = image.getBoundingBox(); diff --git a/frontend/src/components/MapView/Layers/styles.ts b/frontend/src/components/MapView/Layers/styles.ts index 2281fd36d4..9c375ac1cd 100644 --- a/frontend/src/components/MapView/Layers/styles.ts +++ b/frontend/src/components/MapView/Layers/styles.ts @@ -15,8 +15,8 @@ const dataFieldColor = ( legend: LegendDefinitionItem[], dataField: string, dataFieldType?: DataFieldType, -) => { - return dataFieldType === DataFieldType.TEXT +) => + dataFieldType === DataFieldType.TEXT ? [ 'match', ['get', dataField], @@ -34,7 +34,6 @@ const dataFieldColor = ( property: dataField, stops: legendToStops(legend), }; -}; export const circlePaint = ({ opacity, @@ -55,16 +54,15 @@ export const fillPaintCategorical = ({ legend, dataField, dataFieldType, -}: PointDataLayerProps): FillLayerSpecification['paint'] => { - return { - 'fill-opacity': opacity || 0.5, - 'fill-color': dataFieldColor(legend, dataField, dataFieldType) as any, - }; -}; +}: PointDataLayerProps): FillLayerSpecification['paint'] => ({ + 'fill-opacity': opacity || 0.5, + 'fill-color': dataFieldColor(legend, dataField, dataFieldType) as any, +}); // We use the legend values from the config to define "intervals". export const fillPaintData = ( { opacity, legend, id }: CommonLayerProps, + // eslint-disable-next-line default-param-last property: string = 'data', fillPattern?: 'right' | 'left', ): FillLayerSpecification['paint'] => { diff --git a/frontend/src/components/MapView/LeftPanel/AnalysisPanel/AnalysisTable/ExposureAnalysisTable/index.tsx b/frontend/src/components/MapView/LeftPanel/AnalysisPanel/AnalysisTable/ExposureAnalysisTable/index.tsx index 3de3420fb3..2004be4de7 100644 --- a/frontend/src/components/MapView/LeftPanel/AnalysisPanel/AnalysisTable/ExposureAnalysisTable/index.tsx +++ b/frontend/src/components/MapView/LeftPanel/AnalysisPanel/AnalysisTable/ExposureAnalysisTable/index.tsx @@ -11,8 +11,7 @@ import { TableSortLabel, Theme, Typography, - withStyles, - WithStyles, + makeStyles, } from '@material-ui/core'; import { useDispatch, useSelector } from 'react-redux'; @@ -26,7 +25,6 @@ import { hidePopup } from 'context/tooltipStateSlice'; const ExposureAnalysisTable = memo( ({ - classes, tableData, columns, sortColumn, @@ -35,6 +33,7 @@ const ExposureAnalysisTable = memo( }: ExposureAnalysisTableProps) => { // only display local names if local language is selected, otherwise display english name const { t } = useSafeTranslation(); + const classes = useStyles(); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); @@ -42,7 +41,7 @@ const ExposureAnalysisTable = memo( const dispatch = useDispatch(); - const handleChangePage = useCallback((event: unknown, newPage: number) => { + const handleChangePage = useCallback((_event: unknown, newPage: number) => { setPage(newPage); }, []); @@ -56,34 +55,29 @@ const ExposureAnalysisTable = memo( // Whether the table sort label is active const tableSortLabelIsActive = useCallback( - (column: Column) => { - return sortColumn === column.id; - }, + (column: Column) => sortColumn === column.id, [sortColumn], ); // table sort label direction const tableSortLabelDirection = useCallback( - (column: Column) => { - return sortColumn === column.id && !isAscending ? 'desc' : 'asc'; - }, + (column: Column) => + sortColumn === column.id && !isAscending ? 'desc' : 'asc', [isAscending, sortColumn], ); // on table sort label click const onTableSortLabelClick = useCallback( - (column: Column) => { - return () => { - handleChangeOrderBy(column.id); - }; + (column: Column) => () => { + handleChangeOrderBy(column.id); }, [handleChangeOrderBy], ); // The rendered table header cells - const renderedTableHeaderCells = useMemo(() => { - return columns.map(column => { - return ( + const renderedTableHeaderCells = useMemo( + () => + columns.map(column => ( - ); - }); - }, [ - classes.tableHead, - classes.tableHeaderText, - columns, - onTableSortLabelClick, - t, - tableSortLabelDirection, - tableSortLabelIsActive, - ]); + )), + [ + classes.tableHead, + classes.tableHeaderText, + columns, + onTableSortLabelClick, + t, + tableSortLabelDirection, + tableSortLabelIsActive, + ], + ); const renderedTableBodyCellValue = useCallback( (value: string | number, column: Column) => { @@ -119,42 +113,37 @@ const ExposureAnalysisTable = memo( // The rendered table body cells const renderedTableBodyCells = useCallback( - (row: AnalysisTableRow) => { - return columns.map(column => { - return ( - - - {renderedTableBodyCellValue(row[column.id], column)} - - - ); - }); - }, + (row: AnalysisTableRow) => + columns.map(column => ( + + + {renderedTableBodyCellValue(row[column.id], column)} + + + )), [classes.tableBodyText, columns, renderedTableBodyCellValue], ); const handleClickTableBodyRow = useCallback( - row => { - return async () => { - if (!row.coordinates || !map) { - return; - } - await dispatch(hidePopup()); - map.fire('click', { - lngLat: row.coordinates, - point: map.project(row.coordinates), - }); - }; + (row: any) => async () => { + if (!row.coordinates || !map) { + return; + } + await dispatch(hidePopup()); + map.fire('click', { + lngLat: row.coordinates, + point: map.project(row.coordinates), + }); }, [dispatch, map], ); // The rendered table body rows - const renderedTableBodyRows = useMemo(() => { - return tableData - .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - .map((row, index) => { - return ( + const renderedTableBodyRows = useMemo( + () => + tableData + .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((row, index) => ( {renderedTableBodyCells(row)} - ); - }); - }, [ - handleClickTableBodyRow, - page, - renderedTableBodyCells, - rowsPerPage, - tableData, - ]); + )), + [ + handleClickTableBodyRow, + page, + renderedTableBodyCells, + rowsPerPage, + tableData, + ], + ); return ( <> @@ -200,11 +189,11 @@ const ExposureAnalysisTable = memo( onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage={t('Rows Per Page')} // Temporary manual translation before we upgrade to MUI 5. - labelDisplayedRows={({ from, to, count }) => { - return `${from}–${to} ${t('of')} ${ + labelDisplayedRows={({ from, to, count }) => + `${from}–${to} ${t('of')} ${ count !== -1 ? count : `${t('more than')} ${to}` - }`; - }} + }` + } classes={{ root: classes.tablePagination, select: classes.select, @@ -227,7 +216,7 @@ const ExposureAnalysisTable = memo( }, ); -const styles = (theme: Theme) => +const useStyles = makeStyles((theme: Theme) => createStyles({ tableHead: { backgroundColor: '#EBEBEB', @@ -273,10 +262,10 @@ const styles = (theme: Theme) => flex: '1 1 5%', maxWidth: '5%', }, - }); + }), +); -interface ExposureAnalysisTableProps extends WithStyles { - maxResults: number; +interface ExposureAnalysisTableProps { tableData: AnalysisTableRow[]; columns: Column[]; sortColumn: string | number | undefined; @@ -284,4 +273,4 @@ interface ExposureAnalysisTableProps extends WithStyles { handleChangeOrderBy: (newExposureAnalysisSortColumn: Column['id']) => void; } -export default withStyles(styles)(ExposureAnalysisTable); +export default ExposureAnalysisTable; diff --git a/frontend/src/components/MapView/LeftPanel/AnalysisPanel/AnalysisTable/index.tsx b/frontend/src/components/MapView/LeftPanel/AnalysisPanel/AnalysisTable/index.tsx index 4eb2de58fa..340753c636 100644 --- a/frontend/src/components/MapView/LeftPanel/AnalysisPanel/AnalysisTable/index.tsx +++ b/frontend/src/components/MapView/LeftPanel/AnalysisPanel/AnalysisTable/index.tsx @@ -1,6 +1,7 @@ import React, { memo, useCallback, useMemo, useState } from 'react'; import { createStyles, + makeStyles, Table, TableBody, TableCell, @@ -11,8 +12,6 @@ import { TableSortLabel, Theme, Typography, - withStyles, - WithStyles, } from '@material-ui/core'; import { useDispatch, useSelector } from 'react-redux'; import { TableRow as AnalysisTableRow } from 'context/analysisResultStateSlice'; @@ -23,7 +22,6 @@ import { hidePopup } from 'context/tooltipStateSlice'; const AnalysisTable = memo( ({ - classes, tableData, columns, sortColumn, @@ -32,13 +30,14 @@ const AnalysisTable = memo( }: AnalysisTableProps) => { // only display local names if local language is selected, otherwise display english name const { t } = useSafeTranslation(); + const classes = useStyles(); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); const map = useSelector(mapSelector); const dispatch = useDispatch(); - const handleChangePage = useCallback((event: unknown, newPage: number) => { + const handleChangePage = useCallback((_event: unknown, newPage: number) => { setPage(newPage); }, []); @@ -52,33 +51,28 @@ const AnalysisTable = memo( // Whether the table sort label is active const tableSortLabelIsActive = useCallback( - (column: Column) => { - return sortColumn === column.id; - }, + (column: Column) => sortColumn === column.id, [sortColumn], ); // table sort label direction const tableSortLabelDirection = useCallback( - (column: Column) => { - return sortColumn === column.id && !isAscending ? 'desc' : 'asc'; - }, + (column: Column) => + sortColumn === column.id && !isAscending ? 'desc' : 'asc', [isAscending, sortColumn], ); // on table sort label click const onTableSortLabelClick = useCallback( - (column: Column) => { - return () => { - handleChangeOrderBy(column.id); - }; + (column: Column) => () => { + handleChangeOrderBy(column.id); }, [handleChangeOrderBy], ); - const renderedTableHeaderCells = useMemo(() => { - return columns.map(column => { - return ( + const renderedTableHeaderCells = useMemo( + () => + columns.map(column => ( - ); - }); - }, [ - classes.tableHead, - classes.tableHeaderText, - columns, - onTableSortLabelClick, - t, - tableSortLabelDirection, - tableSortLabelIsActive, - ]); + )), + [ + classes.tableHead, + classes.tableHeaderText, + columns, + onTableSortLabelClick, + t, + tableSortLabelDirection, + tableSortLabelIsActive, + ], + ); const handleClickTableBodyRow = useCallback( - row => { - return async () => { - if (!row.coordinates || !map) { - return; - } - await dispatch(hidePopup()); - map.fire('click', { - lngLat: row.coordinates, - point: map.project(row.coordinates), - }); - }; + (row: any) => async () => { + if (!row.coordinates || !map) { + return; + } + await dispatch(hidePopup()); + map.fire('click', { + lngLat: row.coordinates, + point: map.project(row.coordinates), + }); }, [dispatch, map], ); const renderedTableRowStyles = useCallback( - (row: AnalysisTableRow, index: number) => { - return { - cursor: row.coordinates ? 'pointer' : 'default', - backgroundColor: index % 2 === 0 ? 'white' : '#EBEBEB', - }; - }, + (row: AnalysisTableRow, index: number) => ({ + cursor: row.coordinates ? 'pointer' : 'default', + backgroundColor: index % 2 === 0 ? 'white' : '#EBEBEB', + }), [], ); @@ -139,25 +129,22 @@ const AnalysisTable = memo( ); const renderedTableBodyCells = useCallback( - (row: AnalysisTableRow) => { - return columns.map(column => { - return ( - - - {renderedTableBodyCellValue(row[column.id], column)} - - - ); - }); - }, + (row: AnalysisTableRow) => + columns.map(column => ( + + + {renderedTableBodyCellValue(row[column.id], column)} + + + )), [classes.tableBodyText, columns, renderedTableBodyCellValue], ); - const renderedTableBodyRows = useMemo(() => { - return tableData - .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - .map((row, index) => { - return ( + const renderedTableBodyRows = useMemo( + () => + tableData + .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((row, index) => ( {renderedTableBodyCells(row)} - ); - }); - }, [ - handleClickTableBodyRow, - page, - renderedTableBodyCells, - renderedTableRowStyles, - rowsPerPage, - tableData, - ]); + )), + [ + handleClickTableBodyRow, + page, + renderedTableBodyCells, + renderedTableRowStyles, + rowsPerPage, + tableData, + ], + ); return ( <> @@ -199,11 +186,11 @@ const AnalysisTable = memo( onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage={t('Rows Per Page')} // Temporary manual translation before we upgrade to MUI 5. - labelDisplayedRows={({ from, to, count }) => { - return `${from}–${to} ${t('of')} ${ + labelDisplayedRows={({ from, to, count }) => + `${from}–${to} ${t('of')} ${ count !== -1 ? count : `${t('more than')} ${to}` - }`; - }} + }` + } classes={{ root: classes.tablePagination, select: classes.select, @@ -226,7 +213,7 @@ const AnalysisTable = memo( }, ); -const styles = (theme: Theme) => +const useStyles = makeStyles((theme: Theme) => createStyles({ tableContainer: { marginTop: 10, @@ -272,9 +259,10 @@ const styles = (theme: Theme) => flex: '1 1 5%', maxWidth: '5%', }, - }); + }), +); -interface AnalysisTableProps extends WithStyles { +interface AnalysisTableProps { tableData: AnalysisTableRow[]; columns: Column[]; sortColumn: string | number | undefined; @@ -282,4 +270,4 @@ interface AnalysisTableProps extends WithStyles { handleChangeOrderBy: (newAnalysisColumn: Column['id']) => void; } -export default withStyles(styles)(AnalysisTable); +export default AnalysisTable; diff --git a/frontend/src/components/MapView/LeftPanel/AnalysisPanel/ExposureAnalysisActions/index.tsx b/frontend/src/components/MapView/LeftPanel/AnalysisPanel/ExposureAnalysisActions/index.tsx index 1a94891481..79bb314b2a 100644 --- a/frontend/src/components/MapView/LeftPanel/AnalysisPanel/ExposureAnalysisActions/index.tsx +++ b/frontend/src/components/MapView/LeftPanel/AnalysisPanel/ExposureAnalysisActions/index.tsx @@ -1,12 +1,5 @@ -import React, { useCallback, useState, MouseEvent, useMemo } from 'react'; -import { - Button, - createStyles, - Theme, - Typography, - withStyles, - WithStyles, -} from '@material-ui/core'; +import { useCallback, useState, MouseEvent, useMemo } from 'react'; +import { Button, Typography } from '@material-ui/core'; import { snakeCase } from 'lodash'; import { useSelector } from 'react-redux'; import { @@ -44,13 +37,10 @@ function ExposureAnalysisActions({ const API_URL = 'https://prism-api.ovio.org/report'; - const exposureAnalysisColumnsToRender = getExposureAnalysisColumnsToRender( - columns, - ); - const exposureAnalysisTableRowsToRender = getExposureAnalysisTableDataRowsToRender( - columns, - tableData, - ); + const exposureAnalysisColumnsToRender = + getExposureAnalysisColumnsToRender(columns); + const exposureAnalysisTableRowsToRender = + getExposureAnalysisTableDataRowsToRender(columns, tableData); const exposureAnalysisCsvData = getExposureAnalysisCsvData( exposureAnalysisColumnsToRender, exposureAnalysisTableRowsToRender, @@ -60,11 +50,8 @@ function ExposureAnalysisActions({ // We use find here because exposure reports and layers have 1 - 1 sync. // TODO Future enhancement if exposure reports are more than one for specific layer const foundReportKeyBasedOnLayerId = Object.keys(ReportsDefinitions).find( - reportDefinitionKey => { - return ( - ReportsDefinitions[reportDefinitionKey].layerId === exposureLayerId - ); - }, + reportDefinitionKey => + ReportsDefinitions[reportDefinitionKey].layerId === exposureLayerId, ); return ReportsDefinitions[foundReportKeyBasedOnLayerId as string]; }, [exposureLayerId]); @@ -126,10 +113,8 @@ function ExposureAnalysisActions({ setDownloadReportIsLoading(false); }; - const handleToggleReport = (toggle: boolean) => { - return () => { - setOpenReport(toggle); - }; + const handleToggleReport = (toggle: boolean) => () => { + setOpenReport(toggle); }; return ( @@ -142,22 +127,6 @@ function ExposureAnalysisActions({ {t('Download as CSV')} )} - ); } -const styles = (theme: Theme) => - createStyles({ - tableContainer: { - height: '60vh', - maxWidth: '90%', - marginTop: 5, - zIndex: theme.zIndex.modal + 1, - }, - tableHead: { - backgroundColor: '#EBEBEB', - boxShadow: 'inset 0px -1px 0px rgba(0, 0, 0, 0.25)', - }, - tableHeaderText: { - color: 'black', - fontWeight: 500, - }, - tableBodyText: { - color: 'black', - }, - innerAnalysisButton: { - backgroundColor: theme.surfaces?.dark, - }, - tablePagination: { - display: 'flex', - justifyContent: 'center', - color: 'black', - }, - select: { - flex: '1 1 10%', - maxWidth: '10%', - marginRight: 0, - }, - caption: { - flex: '1 2 30%', - marginLeft: 0, - }, - backButton: { - flex: '1 1 5%', - maxWidth: '10%', - }, - nextButton: { - flex: '1 1 5%', - maxWidth: '10%', - }, - spacer: { - flex: '1 1 5%', - maxWidth: '5%', - }, - }); - -interface ExposureAnalysisActionsProps extends WithStyles { +interface ExposureAnalysisActionsProps { analysisButton?: string; bottomButton?: string; clearAnalysis: () => void; @@ -235,4 +170,4 @@ interface ExposureAnalysisActionsProps extends WithStyles { columns: Column[]; } -export default withStyles(styles)(ExposureAnalysisActions); +export default ExposureAnalysisActions; diff --git a/frontend/src/components/MapView/LeftPanel/AnalysisPanel/index.tsx b/frontend/src/components/MapView/LeftPanel/AnalysisPanel/index.tsx index 6dd084e595..3459a1248f 100644 --- a/frontend/src/components/MapView/LeftPanel/AnalysisPanel/index.tsx +++ b/frontend/src/components/MapView/LeftPanel/AnalysisPanel/index.tsx @@ -34,7 +34,6 @@ import { import { useDispatch, useSelector } from 'react-redux'; import DatePicker from 'react-datepicker'; import { isNil, orderBy, range } from 'lodash'; -import { TFunctionKeys } from 'i18next'; import { mapSelector, layersSelector, @@ -150,10 +149,8 @@ const AnalysisPanel = memo(() => { Column['id'] >(exposureAnalysisResultSortByKey); // exposure analysis sort order - const [ - exposureAnalysisIsAscending, - setExposureAnalysisIsAscending, - ] = useState(exposureAnalysisResultSortOrder === 'asc'); + const [exposureAnalysisIsAscending, setExposureAnalysisIsAscending] = + useState(exposureAnalysisResultSortOrder === 'asc'); // defaults the sort column of every other analysis table to 'name' const [analysisSortColumn, setAnalysisSortColumn] = useState( analysisResultSortByKey, @@ -222,15 +219,20 @@ const AnalysisPanel = memo(() => { const hazardDataType: HazardDataType | null = selectedHazardLayer ? selectedHazardLayer.geometry || RasterType.Raster : null; - const availableHazardDates = selectedHazardLayer - ? Array.from( - new Set( - getPossibleDatesForLayer(selectedHazardLayer, availableDates)?.map( - d => d.queryDate, - ), - ), - ).map(d => new Date(d)) || [] - : []; + const availableHazardDates = React.useMemo( + () => + selectedHazardLayer + ? Array.from( + new Set( + getPossibleDatesForLayer( + selectedHazardLayer, + availableDates, + )?.map(d => d.queryDate), + ), + ).map(d => new Date(d)) || [] + : [], + [availableDates, selectedHazardLayer], + ); const BASELINE_URL_LAYER_KEY = 'baselineLayerId'; const preSelectedBaselineLayer = selectedLayers.find( @@ -296,13 +298,15 @@ const AnalysisPanel = memo(() => { }, [analysisResult]); // The analysis table data - const analysisTableData = useMemo(() => { - return orderBy( - analysisResult?.tableData, - analysisSortColumn, - analysisIsAscending ? 'asc' : 'desc', - ); - }, [analysisIsAscending, analysisResult, analysisSortColumn]); + const analysisTableData = useMemo( + () => + orderBy( + analysisResult?.tableData, + analysisSortColumn, + analysisIsAscending ? 'asc' : 'desc', + ), + [analysisIsAscending, analysisResult, analysisSortColumn], + ); // handler of general analysis tables sort order const handleAnalysisTableOrderBy = useCallback( @@ -321,69 +325,69 @@ const AnalysisPanel = memo(() => { ); const onOptionChange = useCallback( - (setterFunc: Dispatch>) => ( - event: React.ChangeEvent, - ) => { - const value = event.target.value as T; - setterFunc(value); - return value; - }, + (setterFunc: Dispatch>) => + (event: React.ChangeEvent) => { + const value = event.target.value as T; + setterFunc(value); + return value; + }, [], ); // specially for threshold values, also does error checking const onThresholdOptionChange = useCallback( - (thresholdType: 'above' | 'below') => ( - event: React.ChangeEvent, - ) => { - const setterFunc = - thresholdType === 'above' ? setAboveThreshold : setBelowThreshold; - const changedOption = onOptionChange(setterFunc)(event); - // setting a value doesn't update the existing value until next render, therefore we must decide whether to access the old one or the newly change one here. - const aboveThresholdValue = parseFloat( - thresholdType === 'above' ? changedOption : aboveThreshold, - ); - const belowThresholdValue = parseFloat( - thresholdType === 'below' ? changedOption : belowThreshold, - ); - if (belowThresholdValue > aboveThresholdValue) { - setThresholdError('Below threshold is larger than above threshold!'); - } else { - setThresholdError(null); - } - }, + (thresholdType: 'above' | 'below') => + (event: React.ChangeEvent) => { + const setterFunc = + thresholdType === 'above' ? setAboveThreshold : setBelowThreshold; + const changedOption = onOptionChange(setterFunc)(event); + // setting a value doesn't update the existing value until next render, therefore we must decide whether to access the old one or the newly change one here. + const aboveThresholdValue = parseFloat( + thresholdType === 'above' ? changedOption : aboveThreshold, + ); + const belowThresholdValue = parseFloat( + thresholdType === 'below' ? changedOption : belowThreshold, + ); + if (belowThresholdValue > aboveThresholdValue) { + setThresholdError('Below threshold is larger than above threshold!'); + } else { + setThresholdError(null); + } + }, [aboveThreshold, belowThreshold, onOptionChange], ); - const statisticOptions = useMemo(() => { - return Object.entries(AggregationOperations) - .filter(([, value]) => value !== AggregationOperations.Sum) // sum is used only for exposure analysis. - .map(([key, value]) => ( - - } - label={ - - {t(key)} - - } - /> - )); - }, [ - classes.analysisPanelParamText, - classes.radioOptions, - classes.radioOptionsChecked, - t, - ]); + const statisticOptions = useMemo( + () => + Object.entries(AggregationOperations) + .filter(([, value]) => value !== AggregationOperations.Sum) // sum is used only for exposure analysis. + .map(([key, value]) => ( + + } + label={ + + {t(key)} + + } + /> + )), + [ + classes.analysisPanelParamText, + classes.radioOptions, + classes.radioOptionsChecked, + t, + ], + ); const activateUniqueBoundary = useCallback( (forceAdminLevel?: BoundaryLayerProps) => { @@ -670,7 +674,6 @@ const AnalysisPanel = memo(() => { { - {t(selectedHazardLayer?.title as TFunctionKeys)} + {t(selectedHazardLayer?.title as any)}
( - -); +function FontAwesomeIconWrap(props: FontAwesomeIconProps) { + return ; +} export interface Action { name: string; icon: React.JSX.Element; } -const DoubleIcon = (props: FontAwesomeIconProps) => ( -
- - -
-); +function DoubleIcon(props: FontAwesomeIconProps) { + return ( +
+ + +
+ ); +} // Reusable action items with full names export const AActions = { @@ -201,7 +204,7 @@ const actionsMap: ActionsMap = { export function getActionsByPhaseCategoryAndWindow( phase: AAPhaseType, category: AACategoryType, - win: typeof AAWindowKeys[number], + win: (typeof AAWindowKeys)[number], ): Action[] { if (category === 'Mild') { return [AActions.naMild]; diff --git a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/DistrictView/ActionsModal/index.test.tsx b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/DistrictView/ActionsModal/index.test.tsx index 53837da0b5..a5fa5eafbd 100644 --- a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/DistrictView/ActionsModal/index.test.tsx +++ b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/DistrictView/ActionsModal/index.test.tsx @@ -1,5 +1,4 @@ import { render } from '@testing-library/react'; -import React from 'react'; import ActionsModal from '.'; import { AActions } from './actions'; diff --git a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/DistrictView/ActionsModal/index.tsx b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/DistrictView/ActionsModal/index.tsx index 632ee3f89c..ff0ab2c884 100644 --- a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/DistrictView/ActionsModal/index.tsx +++ b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/DistrictView/ActionsModal/index.tsx @@ -11,7 +11,6 @@ import { } from '@material-ui/core'; import { Cancel, Close } from '@material-ui/icons'; import { useSafeTranslation } from 'i18n'; -import React from 'react'; import { black, cyanBlue } from 'muiTheme'; import { Action } from './actions'; diff --git a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/DistrictView/index.test.tsx b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/DistrictView/index.test.tsx index 93b4cbc38e..c74c4b2db3 100644 --- a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/DistrictView/index.test.tsx +++ b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/DistrictView/index.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render } from '@testing-library/react'; import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; @@ -16,6 +15,7 @@ const filters: AnticipatoryActionState['filters'] = { categories: { Severe: true, Moderate: true, + Normal: true, Mild: true, na: true, ny: true, diff --git a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/DistrictView/index.tsx b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/DistrictView/index.tsx index 402955465b..e2dc5e3ed9 100644 --- a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/DistrictView/index.tsx +++ b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/DistrictView/index.tsx @@ -31,7 +31,7 @@ import ActionsModal from './ActionsModal'; import { dateSorter, districtViewTransform } from './utils'; interface WindowColumnProps { - win: typeof AAWindowKeys[number]; + win: (typeof AAWindowKeys)[number]; transformed: ReturnType; rowKeys: string[]; openActionsDialog: () => void; @@ -268,9 +268,8 @@ function DistrictView({ dialogs }: DistrictViewProps) { const rawAAData = useSelector(AADataSelector); const aaFilters = useSelector(AAFiltersSelector); const selectedDistrict = useSelector(AASelectedDistrictSelector); - const [actionsModalOpen, setActionsModalOpen] = React.useState( - false, - ); + const [actionsModalOpen, setActionsModalOpen] = + React.useState(false); const [modalActions, setModalActions] = React.useState([]); const districtButtons = [ diff --git a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Forecast/index.test.tsx b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Forecast/index.test.tsx index 364f333aed..659e650583 100644 --- a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Forecast/index.test.tsx +++ b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Forecast/index.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render } from '@testing-library/react'; import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; @@ -19,6 +18,7 @@ const filters: AnticipatoryActionState['filters'] = { Mild: true, na: true, ny: true, + Normal: true, }, selectedIndex: '', selectedDate: undefined, diff --git a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Forecast/index.tsx b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Forecast/index.tsx index 4443de0e7c..f1d5e84633 100644 --- a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Forecast/index.tsx +++ b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Forecast/index.tsx @@ -4,7 +4,6 @@ import { createStyles, makeStyles, } from '@material-ui/core'; -import React from 'react'; import ChartDataLabels from 'chartjs-plugin-datalabels'; import { Scatter } from 'react-chartjs-2'; import { useDispatch, useSelector } from 'react-redux'; diff --git a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Forecast/utils.ts b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Forecast/utils.ts index 4b87d9597b..95b7d5e0ef 100644 --- a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Forecast/utils.ts +++ b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Forecast/utils.ts @@ -57,12 +57,13 @@ export function forecastTransform({ }: ForecastTransformParams) { const { selectedWindow, selectedDate } = filters; - const dateData = (selectedWindow === 'All' - ? [ - ...(data['Window 1'][selectedDistrict] || []), - ...(data['Window 2'][selectedDistrict] || []), - ] - : data[selectedWindow][selectedDistrict] || [] + const dateData = ( + selectedWindow === 'All' + ? [ + ...(data['Window 1'][selectedDistrict] || []), + ...(data['Window 2'][selectedDistrict] || []), + ] + : data[selectedWindow][selectedDistrict] || [] ).filter(x => !selectedDate || x.date <= selectedDate); // eslint-disable-next-line fp/no-mutating-methods @@ -131,7 +132,7 @@ export const chartOptions = { suggestedMin: 0, suggestedMax: 40, stepSize: 10, - callback: (value: number, index: number, values: number[]) => + callback: (value: number, _index: number, _values: number[]) => `${value}%`, }, }, @@ -163,7 +164,7 @@ export const getChartData = ( labels: Object.keys(indexes), datasets: [ { - data: Object.entries(indexes).map(([index, val], i) => ({ + data: Object.entries(indexes).map(([_index, val], i) => ({ x: i + 0.6, y: val?.probability, z: val?.showWarningSign, @@ -181,15 +182,12 @@ export const getChartData = ( offset: -2, // offset from the point align: 'left', backgroundColor: 'white', - borderColor: (ctx: any) => { - return ctx.dataset.backgroundColor; - }, + borderColor: (ctx: any) => ctx.dataset.backgroundColor, borderWidth: 1, borderRadius: 2, color: 'black', - formatter: (value: any, ctx: any) => { - return `${value.z ? '⚠️ ' : ''}${value.y}%`; - }, + formatter: (value: any, _ctx: any) => + `${value.z ? '⚠️ ' : ''}${value.y}%`, }, }, }, diff --git a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/HomeTable/__snapshots__/index.test.tsx.snap b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/HomeTable/__snapshots__/index.test.tsx.snap index 21f9c0b013..7e708892bf 100644 --- a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/HomeTable/__snapshots__/index.test.tsx.snap +++ b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/HomeTable/__snapshots__/index.test.tsx.snap @@ -12,7 +12,9 @@ exports[`renders as expected 1`] = `
-
+
-
+
@@ -97,7 +101,9 @@ exports[`renders as expected 1`] = `
-
+
@@ -144,7 +150,9 @@ exports[`renders as expected 1`] = `
-
+
@@ -191,7 +199,9 @@ exports[`renders as expected 1`] = `
-
+
@@ -239,7 +249,9 @@ exports[`renders as expected 1`] = `
-
+
@@ -286,7 +298,9 @@ exports[`renders as expected 1`] = `
-
+
diff --git a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/HomeTable/index.test.tsx b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/HomeTable/index.test.tsx index 25f600df28..95b2699987 100644 --- a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/HomeTable/index.test.tsx +++ b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/HomeTable/index.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render } from '@testing-library/react'; import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; diff --git a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/HomeTable/index.tsx b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/HomeTable/index.tsx index e1ae792e1f..ab3e32e6e5 100644 --- a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/HomeTable/index.tsx +++ b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/HomeTable/index.tsx @@ -230,7 +230,7 @@ function HomeTable({ dialogs }: HomeTableProps) { const headerRow: ExtendedRowProps = { id: -1, iconContent: null, - windows: selectedWindow === 'All' ? AAWindowKeys.map(x => []) : [[]], + windows: selectedWindow === 'All' ? AAWindowKeys.map(_x => []) : [[]], header: selectedWindow === 'All' ? [...AAWindowKeys] : [selectedWindow], }; @@ -239,10 +239,10 @@ function HomeTable({ dialogs }: HomeTableProps) { rowCategories .filter(x => categories[x.category]) .map(x => { - const getWinData = (win: typeof AAWindowKeys[number]) => + const getWinData = (win: (typeof AAWindowKeys)[number]) => Object.entries(renderedDistricts[win]) - .map(([district, distData]) => { - return distData.map(dist => { + .map(([district, distData]) => + distData.map(dist => { if (dist.category === x.category && dist.phase === x.phase) { return { name: district, @@ -254,8 +254,8 @@ function HomeTable({ dialogs }: HomeTableProps) { }; } return undefined; - }); - }) + }), + ) .flat() .filter(y => y !== undefined); diff --git a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/HowToReadModal/index.test.tsx b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/HowToReadModal/index.test.tsx index 34ec99ef0d..7ad6566bfd 100644 --- a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/HowToReadModal/index.test.tsx +++ b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/HowToReadModal/index.test.tsx @@ -1,5 +1,4 @@ import { render } from '@testing-library/react'; -import React from 'react'; import HowToReadModal from '.'; test('renders actions modal', () => { diff --git a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/HowToReadModal/index.tsx b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/HowToReadModal/index.tsx index 0f1e4e04c9..5148b9629a 100644 --- a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/HowToReadModal/index.tsx +++ b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/HowToReadModal/index.tsx @@ -11,7 +11,6 @@ import { } from '@material-ui/core'; import { Cancel, Close, HelpOutline } from '@material-ui/icons'; import { useSafeTranslation } from 'i18n'; -import React from 'react'; import { black, cyanBlue } from 'muiTheme'; import { safeCountry } from 'config'; @@ -47,8 +46,7 @@ const content = [ ]), { title: 'Ready, Set and Go phases', - text: - 'The "Ready, Set & Go!" system uses seasonal forecasts with longer lead time for preparedness (Ready phase) and shorter lead times for activation and mobilization (Set & Go! phases).', + text: 'The "Ready, Set & Go!" system uses seasonal forecasts with longer lead time for preparedness (Ready phase) and shorter lead times for activation and mobilization (Set & Go! phases).', }, ]; diff --git a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Timeline/index.test.tsx b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Timeline/index.test.tsx index 32128a60a0..431bb36e0a 100644 --- a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Timeline/index.test.tsx +++ b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Timeline/index.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render } from '@testing-library/react'; import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; @@ -16,6 +15,7 @@ const filters: AnticipatoryActionState['filters'] = { categories: { Severe: true, Moderate: true, + Normal: true, Mild: true, na: true, ny: true, diff --git a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Timeline/index.tsx b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Timeline/index.tsx index c57fce7d7d..78de490ac7 100644 --- a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Timeline/index.tsx +++ b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Timeline/index.tsx @@ -16,7 +16,6 @@ import { AnticipatoryActionDataRow, } from 'context/anticipatoryActionStateSlice/types'; import { lightGrey } from 'muiTheme'; -import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useSafeTranslation } from 'i18n'; import { Equalizer, Reply } from '@material-ui/icons'; @@ -164,7 +163,7 @@ function Timeline({ dialogs }: TimelineProps) { rowData.status.phase, )}
- {months.map(([date, label]) => { + {months.map(([date, _label]) => { const elem = rowData.data.find(z => z.date === date); if (!elem) { return ( diff --git a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Timeline/utils.ts b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Timeline/utils.ts index 747270ff00..cce5816e48 100644 --- a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Timeline/utils.ts +++ b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/Timeline/utils.ts @@ -36,9 +36,8 @@ export function timelineTransform({ }: TimelineTransformParams) { const { selectedWindow, selectedIndex, categories } = filters; - const windowData = (selectedWindow === 'All' - ? AAWindowKeys - : [selectedWindow] + const windowData = ( + selectedWindow === 'All' ? AAWindowKeys : [selectedWindow] ).map(win => { const districtData = !!selectedDistrict && data[win][selectedDistrict]; if (!districtData) { @@ -82,7 +81,7 @@ export function timelineTransform({ }); return [win, { months, rows: Object.fromEntries(categoriesMap) }]; }) as [ - typeof AAWindowKeys[number], + (typeof AAWindowKeys)[number], { months: string[][]; rows: { [id: string]: TimelineRow }; @@ -90,7 +89,7 @@ export function timelineTransform({ ][]; const allRows = windowData - .map(([win, x]) => + .map(([_win, x]) => Object.entries(x?.rows || {}).map(([id, info]) => [ id, { status: info.status, data: [] }, diff --git a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/index.tsx b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/index.tsx index 53c7862f80..ffb269aa67 100644 --- a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/index.tsx +++ b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/index.tsx @@ -69,9 +69,8 @@ function AnticipatoryActionPanel() { const monitoredDistricts = useSelector(AAMonitoredDistrictsSelector); const AAAvailableDates = useSelector(AAAvailableDatesSelector); const selectedDistrict = useSelector(AASelectedDistrictSelector); - const { categories: categoryFilters, selectedIndex } = useSelector( - AAFiltersSelector, - ); + const { categories: categoryFilters, selectedIndex } = + useSelector(AAFiltersSelector); const { startDate: selectedDate } = useSelector(dateRangeSelector); const aaData = useSelector(AADataSelector); const view = useSelector(AAViewSelector); @@ -204,7 +203,7 @@ function AnticipatoryActionPanel() { + onChange={(_e, val) => dispatch(setAAFilters({ selectedWindow: val as any })) } > diff --git a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/test.utils.ts b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/test.utils.ts index 49af3d0858..8e6d7738e9 100644 --- a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/test.utils.ts +++ b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/test.utils.ts @@ -315,42 +315,43 @@ export const mockAAData: AnticipatoryActionState['data'] = { }, }; -export const mockAARenderedDistricts: AnticipatoryActionState['renderedDistricts'] = { - 'Window 1': { - Caia: [ - { - category: 'ny', - phase: 'ny', - isNew: false, - }, - ], - Changara: [ - { - category: 'Moderate', - phase: 'Ready', - isNew: false, - }, - { - category: 'Mild', - phase: 'Ready', - isNew: false, - }, - ], - }, - 'Window 2': { - Caia: [ - { - category: 'ny', - phase: 'ny', - isNew: false, - }, - ], - Changara: [ - { - category: 'ny', - phase: 'ny', - isNew: false, - }, - ], - }, -}; +export const mockAARenderedDistricts: AnticipatoryActionState['renderedDistricts'] = + { + 'Window 1': { + Caia: [ + { + category: 'ny', + phase: 'ny', + isNew: false, + }, + ], + Changara: [ + { + category: 'Moderate', + phase: 'Ready', + isNew: false, + }, + { + category: 'Mild', + phase: 'Ready', + isNew: false, + }, + ], + }, + 'Window 2': { + Caia: [ + { + category: 'ny', + phase: 'ny', + isNew: false, + }, + ], + Changara: [ + { + category: 'ny', + phase: 'ny', + isNew: false, + }, + ], + }, + }; diff --git a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/utils.tsx b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/utils.tsx index 49825927a5..2643632a62 100644 --- a/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/utils.tsx +++ b/frontend/src/components/MapView/LeftPanel/AnticipatoryActionPanel/utils.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-refresh/only-export-components */ import { Checkbox, CheckboxProps, @@ -13,7 +14,6 @@ import { withStyles, } from '@material-ui/core'; import { black, borderGray, cyanBlue, lightGrey } from 'muiTheme'; -import React from 'react'; import { useSafeTranslation } from 'i18n'; import { AACategoryType, @@ -104,16 +104,14 @@ export const StyledCheckboxLabel = withStyles({ ...props }: Omit & { checkBoxProps: CheckboxProps; - }) => { - return ( - {label}} - control={} - {...props} - /> - ); - }, + }) => ( + {label}} + control={} + {...props} + /> + ), ); export const StyledSelect = withStyles({ diff --git a/frontend/src/components/MapView/LeftPanel/ChartsPanel/ChartSection/index.test.tsx b/frontend/src/components/MapView/LeftPanel/ChartsPanel/ChartSection/index.test.tsx index da47e57970..df692e186b 100644 --- a/frontend/src/components/MapView/LeftPanel/ChartsPanel/ChartSection/index.test.tsx +++ b/frontend/src/components/MapView/LeftPanel/ChartsPanel/ChartSection/index.test.tsx @@ -1,5 +1,5 @@ import timezoneMock from 'timezone-mock'; -import { generateDateStrings } from '.'; +import { generateDateStrings } from './utils'; import { timezones } from '../../../../../../test/helpers'; const f = () => { diff --git a/frontend/src/components/MapView/LeftPanel/ChartsPanel/ChartSection/index.tsx b/frontend/src/components/MapView/LeftPanel/ChartsPanel/ChartSection/index.tsx index d3c5c9360c..1d4b2f1d21 100644 --- a/frontend/src/components/MapView/LeftPanel/ChartsPanel/ChartSection/index.tsx +++ b/frontend/src/components/MapView/LeftPanel/ChartsPanel/ChartSection/index.tsx @@ -2,10 +2,10 @@ import { CircularProgress, createStyles, Typography, - WithStyles, - withStyles, Box, + makeStyles, } from '@material-ui/core'; + import { GeoJsonProperties } from 'geojson'; import { omit } from 'lodash'; import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; @@ -28,6 +28,7 @@ import { getChartAdminBoundaryParams } from 'utils/admin-utils'; import Chart, { ChartProps } from 'components/Common/Chart'; import { createCsvDataFromDataKeyMap, createDataKeyMap } from 'utils/csv-utils'; import { getFormattedDate } from 'utils/date-utils'; +import { generateDateStrings } from './utils'; /** * This function removes the first occurrence of a specific number from an array. @@ -45,33 +46,6 @@ function removeFirstOccurrence(arr: number[], numberToRemove: number) { return arr; } -// returns startDate and endDate as part of result -export function generateDateStrings(startDate: Date, endDate: Date) { - const result = []; - const interval = [1, 11, 21]; - const currentDate = new Date(startDate); - currentDate.setUTCHours(12, 0, 0, 0); - endDate.setUTCHours(12, 0, 0, 0); - - while (currentDate <= endDate) { - // eslint-disable-next-line fp/no-mutation, no-plusplus - for (let i = 0; i < 3; i++) { - currentDate.setDate(interval[i]); - const formattedDate = currentDate.toISOString().split('T')[0]; - - if (currentDate > startDate && currentDate <= endDate) { - // eslint-disable-next-line fp/no-mutating-methods - result.push(formattedDate); - } - } - - currentDate.setDate(1); - currentDate.setMonth(currentDate.getMonth() + 1); - } - - return result; -} - function extendDatasetRows( chartDataset: TableData, minDate?: string, @@ -132,8 +106,8 @@ const ChartSection = memo( maxChartValue, minChartValue, chartProps, - classes, }: ChartSectionProps) => { + const classes = useStyles(); const dispatch = useDispatch(); const { t, i18n: i18nLocale } = useSafeTranslation(); const [chartDataset, setChartDataset] = useState(); @@ -252,9 +226,8 @@ const ChartSection = memo( setMaxDataTicks, ]); - const [chartDataSetIsLoading, setChartDataSetIsLoading] = useState( - false, - ); + const [chartDataSetIsLoading, setChartDataSetIsLoading] = + useState(false); const [chartDataSetError, setChartDataSetError] = useState< string | undefined >(undefined); @@ -277,16 +250,16 @@ const ChartSection = memo( code: adminCode, name: adminName, localName: adminLocalName, - } = useMemo(() => { - return ( + } = useMemo( + () => params.boundaryProps[adminKey] || { code: appConfig.countryAdmin0Id, - } - ); - }, [adminKey, params]); + }, + [adminKey, params], + ); - const requestParams: DatasetRequestParams = useMemo(() => { - return { + const requestParams: DatasetRequestParams = useMemo( + () => ({ id: adminKey, level: adminLevel.toString(), adminCode: adminCode || appConfig.countryAdmin0Id, @@ -296,18 +269,19 @@ const ChartSection = memo( datasetFields: params.datasetFields, startDate, endDate, - }; - }, [ - adminCode, - adminKey, - adminLevel, - startDate, - endDate, - params.boundaryProps, - params.datasetFields, - params.serverLayerName, - params.url, - ]); + }), + [ + adminCode, + adminKey, + adminLevel, + startDate, + endDate, + params.boundaryProps, + params.datasetFields, + params.serverLayerName, + params.url, + ], + ); const getData = useCallback(async () => { setChartDataSetIsLoading(true); @@ -364,40 +338,38 @@ const ChartSection = memo( }; }, [chartLayer.title, dataForCsv, getData]); - const chartType = useMemo(() => { - return chartLayer.chartData!.type; - }, [chartLayer.chartData]); - - const colors = useMemo(() => { - return params.datasetFields?.map(row => row.color); - }, [params.datasetFields]); - - const minValue = useMemo(() => { - return Math.min( - ...(params.datasetFields - ?.filter((row: DatasetField) => { - return row?.minValue !== undefined; - }) - .map((row: DatasetField) => { - return row.minValue; - }) as number[]), - ); - }, [params.datasetFields]); - - const maxValue = useMemo(() => { - return Math.max( - ...(params.datasetFields - ?.filter((row: DatasetField) => { - return row?.maxValue !== undefined; - }) - .map((row: DatasetField) => { - return row.maxValue; - }) as number[]), - ); - }, [params.datasetFields]); + const chartType = useMemo( + () => chartLayer.chartData!.type, + [chartLayer.chartData], + ); - const config: ChartConfig = useMemo(() => { - return { + const colors = useMemo( + () => params.datasetFields?.map(row => row.color), + [params.datasetFields], + ); + + const minValue = useMemo( + () => + Math.min( + ...(params.datasetFields + ?.filter((row: DatasetField) => row?.minValue !== undefined) + .map((row: DatasetField) => row.minValue) as number[]), + ), + [params.datasetFields], + ); + + const maxValue = useMemo( + () => + Math.max( + ...(params.datasetFields + ?.filter((row: DatasetField) => row?.maxValue !== undefined) + .map((row: DatasetField) => row.maxValue) as number[]), + ), + [params.datasetFields], + ); + + const config: ChartConfig = useMemo( + () => ({ type: chartType, stacked: false, category: CHART_DATA_PREFIXES.date, @@ -407,12 +379,11 @@ const ChartSection = memo( minValue: minChartValue || minValue, maxValue: maxChartValue || maxValue, colors, - }; - }, [chartType, colors, maxChartValue, maxValue, minChartValue, minValue]); + }), + [chartType, colors, maxChartValue, maxValue, minChartValue, minValue], + ); - const title = useMemo(() => { - return chartLayer.title; - }, [chartLayer.title]); + const title = useMemo(() => chartLayer.title, [chartLayer.title]); const subtitle = useMemo(() => { if (isEnglishLanguageSelected(i18nLocale)) { @@ -474,7 +445,7 @@ const ChartSection = memo( }, ); -const styles = () => +const useStyles = makeStyles(() => createStyles({ errorContainer: { display: 'flex', @@ -490,9 +461,10 @@ const styles = () => justifyContent: 'center', alignItems: 'center', }, - }); + }), +); -export interface ChartSectionProps extends WithStyles { +export interface ChartSectionProps { chartLayer: WMSLayerProps; adminProperties: GeoJsonProperties; adminLevel: AdminLevelType; @@ -513,4 +485,4 @@ export interface ChartSectionProps extends WithStyles { chartProps?: Partial; } -export default withStyles(styles)(ChartSection); +export default ChartSection; diff --git a/frontend/src/components/MapView/LeftPanel/ChartsPanel/ChartSection/utils.ts b/frontend/src/components/MapView/LeftPanel/ChartsPanel/ChartSection/utils.ts new file mode 100644 index 0000000000..44ab561659 --- /dev/null +++ b/frontend/src/components/MapView/LeftPanel/ChartsPanel/ChartSection/utils.ts @@ -0,0 +1,26 @@ +// returns startDate and endDate as part of result +export function generateDateStrings(startDate: Date, endDate: Date) { + const result = []; + const interval = [1, 11, 21]; + const currentDate = new Date(startDate); + currentDate.setUTCHours(12, 0, 0, 0); + endDate.setUTCHours(12, 0, 0, 0); + + while (currentDate <= endDate) { + // eslint-disable-next-line fp/no-mutation, no-plusplus + for (let i = 0; i < 3; i++) { + currentDate.setDate(interval[i]); + const formattedDate = currentDate.toISOString().split('T')[0]; + + if (currentDate > startDate && currentDate <= endDate) { + // eslint-disable-next-line fp/no-mutating-methods + result.push(formattedDate); + } + } + + currentDate.setDate(1); + currentDate.setMonth(currentDate.getMonth() + 1); + } + + return result; +} diff --git a/frontend/src/components/MapView/LeftPanel/ChartsPanel/DateSlider/index.tsx b/frontend/src/components/MapView/LeftPanel/ChartsPanel/DateSlider/index.tsx index 3cfed7bf1f..51c5da681a 100644 --- a/frontend/src/components/MapView/LeftPanel/ChartsPanel/DateSlider/index.tsx +++ b/frontend/src/components/MapView/LeftPanel/ChartsPanel/DateSlider/index.tsx @@ -42,12 +42,20 @@ function DateSlider({ return ( - + {t('Start')} @@ -57,7 +65,13 @@ function DateSlider({ - + {t('End')} diff --git a/frontend/src/components/MapView/LeftPanel/ChartsPanel/LocationSelector/index.tsx b/frontend/src/components/MapView/LeftPanel/ChartsPanel/LocationSelector/index.tsx index 638945fb1c..8d354ec025 100644 --- a/frontend/src/components/MapView/LeftPanel/ChartsPanel/LocationSelector/index.tsx +++ b/frontend/src/components/MapView/LeftPanel/ChartsPanel/LocationSelector/index.tsx @@ -12,7 +12,7 @@ import { BoundaryLayerProps, PanelSize, AdminCodeString } from 'config/types'; import { getAdminBoundaryTree, AdminBoundaryTree, -} from 'components/MapView/Layers/BoundaryDropdown'; +} from 'components/MapView/Layers/BoundaryDropdown/utils'; import { useSafeTranslation } from 'i18n'; import { BoundaryLayerData } from 'context/layers/boundary'; @@ -109,11 +109,10 @@ const LocationSelector = memo( const selectedAdmin1Area = () => admin0BoundaryTree?.[admin1Key]; - const orderedAdmin2areas: () => AdminBoundaryTree[] = () => { - return data && admin1Key + const orderedAdmin2areas: () => AdminBoundaryTree[] = () => + data && admin1Key ? sortBy(Object.values(selectedAdmin1Area().children), 'label') : []; - }; const selectedAdmin2Area = () => selectedAdmin1Area().children[admin2Key]; @@ -124,13 +123,11 @@ const LocationSelector = memo( return adminBoundaryTree.children[admin0keyValue].label as ReactNode; }; - const renderAdmin1Value = (admin1keyValue: any) => { - return admin0BoundaryTree[admin1keyValue]?.label; - }; + const renderAdmin1Value = (admin1keyValue: any) => + admin0BoundaryTree[admin1keyValue]?.label; - const renderAdmin2Value = (admin2KeyValue: any) => { - return selectedAdmin1Area().children[admin2KeyValue].label; - }; + const renderAdmin2Value = (admin2KeyValue: any) => + selectedAdmin1Area().children[admin2KeyValue].label; const onChangeAdmin0Area = (event: React.ChangeEvent) => { const admin0Id = event.target.value; diff --git a/frontend/src/components/MapView/LeftPanel/ChartsPanel/index.tsx b/frontend/src/components/MapView/LeftPanel/ChartsPanel/index.tsx index 78e853edd0..4285fd8d12 100644 --- a/frontend/src/components/MapView/LeftPanel/ChartsPanel/index.tsx +++ b/frontend/src/components/MapView/LeftPanel/ChartsPanel/index.tsx @@ -26,7 +26,6 @@ import React, { useState, } from 'react'; import { useSelector } from 'react-redux'; -import { TFunctionKeys } from 'i18next'; import { appConfig } from 'config'; import { AdminCodeString, @@ -233,7 +232,7 @@ const ChartsPanel = memo(() => { ); const [selectedLayerTitles, setSelectedLayerTitles] = useState< - string[] | TFunctionKeys[] + string[] | any[] >([]); const yearsToFetchDataFor = 5; @@ -251,9 +250,8 @@ const ChartsPanel = memo(() => { new Date().getTime() - oneYearInMs - oneDayInMs, ); const [adminProperties, setAdminProperties] = useState(); - const [secondAdminProperties, setSecondAdminProperties] = useState< - GeoJsonProperties - >(); + const [secondAdminProperties, setSecondAdminProperties] = + useState(); const oneYearInTicks = 34; // maxDataTicks used for setting slider max ticks const [maxDataTicks, setMaxDataTicks] = useState(0); @@ -324,9 +322,7 @@ const ChartsPanel = memo(() => { ); const getCountryName: (admProps: GeoJsonProperties) => string = useCallback( - admProps => { - return multiCountry ? admProps?.admin0Name : country; - }, + admProps => (multiCountry ? admProps?.admin0Name : country), [country], ); @@ -342,14 +338,14 @@ const ChartsPanel = memo(() => { }`; }; - const showChartsPanel = useMemo(() => { - return ( + const showChartsPanel = useMemo( + () => adminProperties && startDate1 && tabPanelType === tabValue && - selectedLayerTitles.length >= 1 - ); - }, [adminProperties, startDate1, selectedLayerTitles.length, tabValue]); + selectedLayerTitles.length >= 1, + [adminProperties, startDate1, selectedLayerTitles.length, tabValue], + ); useEffect(() => { if (!adminProperties && countryAdmin0Id && data) { @@ -363,13 +359,23 @@ const ChartsPanel = memo(() => { } }, [secondAdminProperties, countryAdmin0Id, data]); - const singleDownloadChartPrefix = adminProperties - ? [ - getCountryName(adminProperties), - selectedAdmin1Area, - selectedAdmin2Area, - ].map(x => t(x)) - : []; + const singleDownloadChartPrefix = React.useMemo( + () => + adminProperties + ? [ + getCountryName(adminProperties), + selectedAdmin1Area, + selectedAdmin2Area, + ].map(x => t(x)) + : [], + [ + adminProperties, + getCountryName, + selectedAdmin1Area, + selectedAdmin2Area, + t, + ], + ); const firstCSVFilename = adminProperties ? buildCsvFileName([ @@ -714,13 +720,10 @@ const ChartsPanel = memo(() => { }, [compareLocations, comparePeriods, selectedLayerTitles]); const chartsSelectRenderValue = useCallback( - selected => { - return selected - .map((selectedValue: string | TFunctionKeys) => { - return t(selectedValue); - }) - .join(', '); - }, + (selected: any) => + selected + .map((selectedValue: string | any) => t(selectedValue)) + .join(', '), [t], ); diff --git a/frontend/src/components/MapView/LeftPanel/TablesPanel/DataTable/index.tsx b/frontend/src/components/MapView/LeftPanel/TablesPanel/DataTable/index.tsx index e06cab61f4..fc24a7ae3f 100644 --- a/frontend/src/components/MapView/LeftPanel/TablesPanel/DataTable/index.tsx +++ b/frontend/src/components/MapView/LeftPanel/TablesPanel/DataTable/index.tsx @@ -1,5 +1,5 @@ import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { createStyles, withStyles, WithStyles } from '@material-ui/styles'; +import { createStyles } from '@material-ui/styles'; import { Box, CircularProgress, @@ -13,6 +13,7 @@ import { TableSortLabel, Theme, Typography, + makeStyles, } from '@material-ui/core'; import { orderBy } from 'lodash'; import { useSafeTranslation } from 'i18n'; @@ -23,30 +24,18 @@ import LoadingBlinkingDots from 'components/Common/LoadingBlinkingDots'; import { getTableCellVal } from 'utils/data-utils'; const DataTable = memo( - ({ - classes, - title, - legendText, - chart, - tableData, - tableLoading, - }: DataTableProps) => { + ({ title, legendText, chart, tableData, tableLoading }: DataTableProps) => { + const classes = useStyles(); const { t } = useSafeTranslation(); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); const [isAscending, setIsAscending] = useState(true); - const rows = useMemo(() => { - return tableData.rows; - }, [tableData.rows]); + const rows = useMemo(() => tableData.rows, [tableData.rows]); - const tableRowsToRender = useMemo(() => { - return rows.slice(1); - }, [rows]); + const tableRowsToRender = useMemo(() => rows.slice(1), [rows]); - const columns = useMemo(() => { - return tableData.columns; - }, [tableData.columns]); + const columns = useMemo(() => tableData.columns, [tableData.columns]); // defaults to the first item of the columns collection const [sortColumn, setSortColumn] = useState(columns[0]); @@ -55,21 +44,15 @@ const DataTable = memo( setSortColumn(columns[0]); }, [columns]); - const sortedTableRowsToRender = useMemo(() => { - return orderBy( - tableRowsToRender, - sortColumn, - isAscending ? 'asc' : 'desc', - ); - }, [isAscending, sortColumn, tableRowsToRender]); + const sortedTableRowsToRender = useMemo( + () => + orderBy(tableRowsToRender, sortColumn, isAscending ? 'asc' : 'desc'), + [isAscending, sortColumn, tableRowsToRender], + ); - const renderedTitle = useMemo(() => { - return title ?? ''; - }, [title]); + const renderedTitle = useMemo(() => title ?? '', [title]); - const renderedLegendText = useMemo(() => { - return legendText ?? ''; - }, [legendText]); + const renderedLegendText = useMemo(() => legendText ?? '', [legendText]); // handler of sort order const handleTableOrderBy = useCallback( @@ -81,7 +64,7 @@ const DataTable = memo( [isAscending, sortColumn], ); - const handleChangePage = useCallback((event: unknown, newPage: number) => { + const handleChangePage = useCallback((_event: unknown, newPage: number) => { setPage(newPage); }, []); @@ -111,61 +94,58 @@ const DataTable = memo( // Whether the table sort label is active const tableSortLabelIsActive = useCallback( - (column: string) => { - return sortColumn === column; - }, + (column: string) => sortColumn === column, [sortColumn], ); // table sort label direction const tableSortLabelDirection = useCallback( - (column: string) => { - return sortColumn === column && !isAscending ? 'desc' : 'asc'; - }, + (column: string) => + sortColumn === column && !isAscending ? 'desc' : 'asc', [isAscending, sortColumn], ); // on table sort label click const onTableSortLabelClick = useCallback( - (column: string) => { - return () => { - handleTableOrderBy(column); - }; + (column: string) => () => { + handleTableOrderBy(column); }, [handleTableOrderBy], ); - const renderedTableHeaderCells = useMemo(() => { - return columns.map((column: string) => { - const formattedColValue = getTableCellVal(rows[0], column, t); - return ( - - - - {formattedColValue} - - - - ); - }); - }, [ - classes.tableHead, - classes.tableHeaderText, - columns, - onTableSortLabelClick, - rows, - t, - tableSortLabelDirection, - tableSortLabelIsActive, - ]); + const renderedTableHeaderCells = useMemo( + () => + columns.map((column: string) => { + const formattedColValue = getTableCellVal(rows[0], column, t); + return ( + + + + {formattedColValue} + + + + ); + }), + [ + classes.tableHead, + classes.tableHeaderText, + columns, + onTableSortLabelClick, + rows, + t, + tableSortLabelDirection, + tableSortLabelIsActive, + ], + ); const renderedTableBodyCells = useCallback( - (row: TableRowType) => { - return columns.map(column => { + (row: TableRowType) => + columns.map(column => { const formattedColValue = getTableCellVal(row, column, t); return ( @@ -174,19 +154,20 @@ const DataTable = memo( ); - }); - }, + }), [classes.tableBodyText, columns, t], ); - const renderedTableBodyRows = useMemo(() => { - return sortedTableRowsToRender - .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - .map((row: TableRowType, rowIndex: number) => { - const key = `TableRow-${(row as unknown) as string}}-${rowIndex}`; - return {renderedTableBodyCells(row)}; - }); - }, [page, renderedTableBodyCells, rowsPerPage, sortedTableRowsToRender]); + const renderedTableBodyRows = useMemo( + () => + sortedTableRowsToRender + .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((row: TableRowType, rowIndex: number) => { + const key = `TableRow-${row as unknown as string}}-${rowIndex}`; + return {renderedTableBodyCells(row)}; + }), + [page, renderedTableBodyCells, rowsPerPage, sortedTableRowsToRender], + ); const renderedTable = useMemo(() => { if (!tableData) { @@ -212,11 +193,11 @@ const DataTable = memo( onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage={t('Rows Per Page')} // Temporary manual translation before we upgrade to MUI 5. - labelDisplayedRows={({ from, to, count }) => { - return `${from}–${to} ${t('of')} ${ + labelDisplayedRows={({ from, to, count }) => + `${from}–${to} ${t('of')} ${ count !== -1 ? count : `${t('more than')} ${to}` - }`; - }} + }` + } classes={{ root: classes.tablePagination, }} @@ -296,8 +277,8 @@ const DataTable = memo( }, ); -const styles = (theme: Theme) => { - return createStyles({ +const useStyles = makeStyles((theme: Theme) => + createStyles({ dataTableRoot: { display: 'flex', flexDirection: 'column', @@ -361,10 +342,10 @@ const styles = (theme: Theme) => { color: 'black', flexShrink: 0, }, - }); -}; + }), +); -interface DataTableProps extends WithStyles { +interface DataTableProps { title?: string; legendText?: string; chart?: ChartConfig; @@ -372,4 +353,4 @@ interface DataTableProps extends WithStyles { tableLoading: boolean; } -export default withStyles(styles)(DataTable); +export default DataTable; diff --git a/frontend/src/components/MapView/LeftPanel/TablesPanel/TablesActions/index.tsx b/frontend/src/components/MapView/LeftPanel/TablesPanel/TablesActions/index.tsx index 39f0c0c91a..1a2baeea7d 100644 --- a/frontend/src/components/MapView/LeftPanel/TablesPanel/TablesActions/index.tsx +++ b/frontend/src/components/MapView/LeftPanel/TablesPanel/TablesActions/index.tsx @@ -1,16 +1,12 @@ -import React, { memo, useCallback, useMemo } from 'react'; -import { createStyles, withStyles, WithStyles } from '@material-ui/styles'; -import { Button, Typography } from '@material-ui/core'; +import { memo, useCallback, useMemo } from 'react'; +import { createStyles } from '@material-ui/styles'; +import { Button, Typography, makeStyles } from '@material-ui/core'; import { useSafeTranslation } from 'i18n'; import { cyanBlue } from 'muiTheme'; const TablesActions = memo( - ({ - classes, - showTable, - handleShowTable, - csvTableData, - }: TablesActionsProps) => { + ({ showTable, handleShowTable, csvTableData }: TablesActionsProps) => { + const classes = useStyles(); const { t } = useSafeTranslation(); const handleOnClickHideShowTable = useCallback(() => { @@ -44,8 +40,8 @@ const TablesActions = memo( }, ); -const styles = () => { - return createStyles({ +const useStyles = makeStyles(() => + createStyles({ buttonsContainer: { display: 'flex', flexDirection: 'column', @@ -70,13 +66,13 @@ const styles = () => { width: '100%', '&.Mui-disabled': { opacity: 0.5 }, }, - }); -}; + }), +); -interface TablesActionsProps extends WithStyles { +interface TablesActionsProps { showTable: boolean; handleShowTable: (show: boolean) => void; csvTableData: any; } -export default withStyles(styles)(TablesActions); +export default TablesActions; diff --git a/frontend/src/components/MapView/LeftPanel/TablesPanel/index.tsx b/frontend/src/components/MapView/LeftPanel/TablesPanel/index.tsx index a8d5e08a69..c3622f9887 100644 --- a/frontend/src/components/MapView/LeftPanel/TablesPanel/index.tsx +++ b/frontend/src/components/MapView/LeftPanel/TablesPanel/index.tsx @@ -1,4 +1,4 @@ -import React, { +import { ChangeEvent, memo, useCallback, @@ -37,9 +37,7 @@ import DataTable from './DataTable'; import { tablesMenuItems } from '../utils'; const tableCategories = tablesMenuItems - .map((menuItem: MenuItemType) => { - return menuItem.layersCategories; - }) + .map((menuItem: MenuItemType) => menuItem.layersCategories) .flat(); const tabPanelType = Panel.Tables; @@ -110,39 +108,37 @@ const TablesPanel = memo(() => { ]); const renderTablesMenuItems = useCallback( - (tables: TableType[]) => { - return tables.map((individualTable: TableType) => { - return ( - - {t(individualTable.title)} - - ); - }); - }, + (tables: TableType[]) => + tables.map((individualTable: TableType) => ( + + {t(individualTable.title)} + + )), [t], ); - const renderedTextFieldItems = useMemo(() => { - return tableCategories.map((tableCategory: LayersCategoryType) => { - return [ + const renderedTextFieldItems = useMemo( + () => + tableCategories.map((tableCategory: LayersCategoryType) => [ {t(tableCategory.title)} , renderTablesMenuItems(tableCategory.tables), - ]; - }); - }, [renderTablesMenuItems, t]); + ]), + [renderTablesMenuItems, t], + ); - const renderedTextFieldBody = useMemo(() => { - return [ + const renderedTextFieldBody = useMemo( + () => [ {t('Choose Table')} , renderedTextFieldItems, - ]; - }, [renderedTextFieldItems, t]); + ], + [renderedTextFieldItems, t], + ); const handleTableDropdownChange = useCallback( (event: ChangeEvent) => { diff --git a/frontend/src/components/MapView/LeftPanel/index.tsx b/frontend/src/components/MapView/LeftPanel/index.tsx index 2e596b05b0..219c211ab5 100644 --- a/frontend/src/components/MapView/LeftPanel/index.tsx +++ b/frontend/src/components/MapView/LeftPanel/index.tsx @@ -37,25 +37,23 @@ interface TabPanelProps { value: Panel; } -const TabPanel = memo(({ children, value, index, ...other }: TabPanelProps) => { - return ( -
- {children} -
- ); -}); +const TabPanel = memo(({ children, value, index, ...other }: TabPanelProps) => ( +
+ {children} +
+)); const LeftPanel = memo(() => { const dispatch = useDispatch(); @@ -65,11 +63,8 @@ const LeftPanel = memo(() => { const AAAvailableDates = useSelector(AAAvailableDatesSelector); const selectedLayers = useSelector(layersSelector); const map = useSelector(mapSelector); - const { - updateHistory, - appendLayerToUrl, - removeLayerFromUrl, - } = useUrlHistory(); + const { updateHistory, appendLayerToUrl, removeLayerFromUrl } = + useUrlHistory(); const classes = useStyles({ tabValue }); diff --git a/frontend/src/components/MapView/LeftPanel/layersPanel/AnalysisLayerMenuItem/__snapshots__/index.test.tsx.snap b/frontend/src/components/MapView/LeftPanel/layersPanel/AnalysisLayerMenuItem/__snapshots__/index.test.tsx.snap index 65ecf2afdb..e22268b9df 100644 --- a/frontend/src/components/MapView/LeftPanel/layersPanel/AnalysisLayerMenuItem/__snapshots__/index.test.tsx.snap +++ b/frontend/src/components/MapView/LeftPanel/layersPanel/AnalysisLayerMenuItem/__snapshots__/index.test.tsx.snap @@ -75,10 +75,12 @@ exports[`renders as expected 1`] = ` style="padding-left: 12px;" >
- + + + + +
diff --git a/frontend/src/components/MapView/Legends/index.test.tsx b/frontend/src/components/MapView/Legends/index.test.tsx index 4e5ce18281..c634649092 100644 --- a/frontend/src/components/MapView/Legends/index.test.tsx +++ b/frontend/src/components/MapView/Legends/index.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render } from '@testing-library/react'; import { Provider } from 'react-redux'; @@ -12,7 +11,7 @@ test('renders as expected', () => { const { container } = render( - + , ); diff --git a/frontend/src/components/MapView/Legends/index.tsx b/frontend/src/components/MapView/Legends/index.tsx index 21f0ec868a..b52ec15112 100644 --- a/frontend/src/components/MapView/Legends/index.tsx +++ b/frontend/src/components/MapView/Legends/index.tsx @@ -1,20 +1,24 @@ import { Button, createStyles, - Hidden, IconButton, Typography, - WithStyles, - withStyles, + makeStyles, + useTheme, + useMediaQuery, } from '@material-ui/core'; import { VisibilityOutlined, VisibilityOffOutlined } from '@material-ui/icons'; -import React, { useState, memo, useCallback } from 'react'; +import { useState, memo, useCallback } from 'react'; import { useSafeTranslation } from 'i18n'; import { black, cyanBlue } from 'muiTheme'; import LegendItemsList from './LegendItemsList'; -const Legends = memo(({ classes }: LegendsProps) => { +const Legends = memo(() => { + const classes = useStyles(); const { t } = useSafeTranslation(); + const theme = useTheme(); + const smDown = useMediaQuery(theme.breakpoints.down('sm')); + const mdUp = useMediaQuery(theme.breakpoints.up('md')); const [open, setOpen] = useState(true); @@ -24,7 +28,7 @@ const Legends = memo(({ classes }: LegendsProps) => { return ( <> - + {!smDown && ( - + )} - + {!mdUp && ( { )} - + )} {open && } ); }); -const styles = () => +const useStyles = makeStyles(() => createStyles({ triggerButton: { height: '2.5em', @@ -83,8 +87,7 @@ const styles = () => top: 'calc(6vh + 16px)', }, icon: { color: 'white', fontSize: '1.5rem' }, - }); + }), +); -export interface LegendsProps extends WithStyles {} - -export default withStyles(styles)(Legends); +export default Legends; diff --git a/frontend/src/components/MapView/Legends/layerContentPreview.tsx b/frontend/src/components/MapView/Legends/layerContentPreview.tsx index b492a0b10c..2912c04e43 100644 --- a/frontend/src/components/MapView/Legends/layerContentPreview.tsx +++ b/frontend/src/components/MapView/Legends/layerContentPreview.tsx @@ -1,11 +1,10 @@ -import React, { useState, memo, useMemo, useCallback } from 'react'; +import { useState, memo, useMemo, useCallback } from 'react'; import { IconButton, createStyles, Grid, Theme, - WithStyles, - withStyles, + makeStyles, } from '@material-ui/core'; import InfoIcon from '@material-ui/icons/Info'; import { useDispatch } from 'react-redux'; @@ -14,7 +13,8 @@ import { LayerDefinitions, getBoundaryLayerSingleton } from 'config/utils'; import ContentDialog from 'components/NavBar/ContentDialog'; import { loadLayerContent } from 'utils/load-layer-utils'; -const LayerContentPreview = memo(({ layerId, classes }: PreviewProps) => { +const LayerContentPreview = memo(({ layerId }: PreviewProps) => { + const classes = useStyles(); const [content, setContent] = useState(undefined); const dispatch = useDispatch(); @@ -81,7 +81,7 @@ const LayerContentPreview = memo(({ layerId, classes }: PreviewProps) => { ]); }); -const styles = (theme: Theme) => +const useStyles = makeStyles((theme: Theme) => createStyles({ icon: { top: '-4px', @@ -95,10 +95,11 @@ const styles = (theme: Theme) => fontWeight: 'bold', minWidth: '600px', }, - }); + }), +); -export interface PreviewProps extends WithStyles { +export interface PreviewProps { layerId: LayerType['id'] | 'analysis'; } -export default withStyles(styles)(LayerContentPreview); +export default LayerContentPreview; diff --git a/frontend/src/components/MapView/Legends/utils.ts b/frontend/src/components/MapView/Legends/utils.ts new file mode 100644 index 0000000000..1a282a5f59 --- /dev/null +++ b/frontend/src/components/MapView/Legends/utils.ts @@ -0,0 +1,13 @@ +import { LegendDefinitionItem } from 'config/types'; + +// Invert the colors of the legend, first color becomes last and vice versa +export const invertLegendColors = ( + legendItems: LegendDefinitionItem[], +): LegendDefinitionItem[] => { + // eslint-disable-next-line fp/no-mutating-methods + const reversedColors = legendItems.map(item => item.color).reverse(); + return legendItems.map((item, index) => ({ + ...item, + color: reversedColors[index], + })); +}; diff --git a/frontend/src/components/MapView/Map/index.test.tsx b/frontend/src/components/MapView/Map/index.test.tsx index 81cd886261..bec24b8074 100644 --- a/frontend/src/components/MapView/Map/index.test.tsx +++ b/frontend/src/components/MapView/Map/index.test.tsx @@ -1,6 +1,5 @@ import { render } from '@testing-library/react'; import { Provider } from 'react-redux'; -import React from 'react'; import { store } from 'context/store'; import MapComponent from '.'; @@ -16,7 +15,7 @@ jest.mock('react-router-dom', () => ({ test('renders as expected', () => { const { container } = render( - {}} panelHidden={false} /> + {}} /> , ); expect(container).toMatchSnapshot(); diff --git a/frontend/src/components/MapView/Map/index.tsx b/frontend/src/components/MapView/Map/index.tsx index 12ab71242e..39750b5cab 100644 --- a/frontend/src/components/MapView/Map/index.tsx +++ b/frontend/src/components/MapView/Map/index.tsx @@ -40,6 +40,7 @@ import { MapSourceDataEvent, Map as MaplibreMap } from 'maplibre-gl'; import 'maplibre-gl/dist/maplibre-gl.css'; import { Panel, leftPanelTabValueSelector } from 'context/leftPanelStateSlice'; +import { mapStyle } from './utils'; interface MapComponentProps { setIsAlertFormOpen: Dispatch>; @@ -51,11 +52,6 @@ type LayerComponentsMap = { }; }; -export const mapStyle = new URL( - process.env.REACT_APP_DEFAULT_STYLE || - 'https://api.maptiler.com/maps/0ad52f6b-ccf2-4a36-a9b8-7ebd8365e56f/style.json?key=y2DTSu9yWiu755WByJr3', -); - const componentTypes: LayerComponentsMap = { boundary: { component: BoundaryLayer }, wms: { component: WMSLayer }, @@ -89,8 +85,8 @@ const MapComponent = memo(({ setIsAlertFormOpen }: MapComponentProps) => { 'label_airport', ); - const fitBoundsOptions = useMemo(() => { - return { + const fitBoundsOptions = useMemo( + () => ({ duration: 0, padding: { bottom: 150, // room for dates. @@ -98,30 +94,28 @@ const MapComponent = memo(({ setIsAlertFormOpen }: MapComponentProps) => { right: 60, top: 70, }, - }; - }, [panelHidden]); + }), + [panelHidden], + ); - const showBoundaryInfo = useMemo(() => { - return JSON.parse(process.env.REACT_APP_SHOW_MAP_INFO || 'false'); - }, []); + const showBoundaryInfo = useMemo( + () => JSON.parse(process.env.REACT_APP_SHOW_MAP_INFO || 'false'), + [], + ); const onDragEnd = useCallback( - (map: MaplibreMap) => { - return () => { - const bounds = map.getBounds(); - dispatch(setBounds(bounds)); - }; + (map: MaplibreMap) => () => { + const bounds = map.getBounds(); + dispatch(setBounds(bounds)); }, [dispatch], ); const onZoomEnd = useCallback( - (map: MaplibreMap) => { - return () => { - const bounds = map.getBounds(); - const newZoom = map.getZoom(); - dispatch(setLocation({ bounds, zoom: newZoom })); - }; + (map: MaplibreMap) => () => { + const bounds = map.getBounds(); + const newZoom = map.getZoom(); + dispatch(setLocation({ bounds, zoom: newZoom })); }, [dispatch], ); @@ -137,34 +131,30 @@ const MapComponent = memo(({ setIsAlertFormOpen }: MapComponentProps) => { ); const mapSourceListener = useCallback( - (layerIds: Set) => { - return (e: MapSourceDataEvent) => { - if (!e.sourceId || !e.sourceId.startsWith('source-')) { - return; - } - const layerId = e.sourceId.substring('source-'.length) as LayerKey; - const included = layerIds.has(layerId); - if (!included && !e.isSourceLoaded) { - layerIds.add(layerId); - dispatch(setLoadingLayerIds([...layerIds])); - } else if (included && e.isSourceLoaded) { - layerIds.delete(layerId); - dispatch(setLoadingLayerIds([...layerIds])); - } - }; + (layerIds: Set) => (e: MapSourceDataEvent) => { + if (!e.sourceId || !e.sourceId.startsWith('source-')) { + return; + } + const layerId = e.sourceId.substring('source-'.length) as LayerKey; + const included = layerIds.has(layerId); + if (!included && !e.isSourceLoaded) { + layerIds.add(layerId); + dispatch(setLoadingLayerIds([...layerIds])); + } else if (included && e.isSourceLoaded) { + layerIds.delete(layerId); + dispatch(setLoadingLayerIds([...layerIds])); + } }, [dispatch], ); const idleMapListener = useCallback( - (layerIds: Set) => { - return () => { - if (layerIds.size <= 0) { - return; - } - layerIds.clear(); - dispatch(setLoadingLayerIds([...layerIds])); - }; + (layerIds: Set) => () => { + if (layerIds.size <= 0) { + return; + } + layerIds.clear(); + dispatch(setLoadingLayerIds([...layerIds])); }, [dispatch], ); @@ -182,7 +172,7 @@ const MapComponent = memo(({ setIsAlertFormOpen }: MapComponentProps) => { // TODO: maplibre: Maybe replace this with the map provider // Saves a reference to base MaplibreGl Map object in case child layers need access beyond the React wrappers. - const onMapLoad = (e: MapEvent) => { + const onMapLoad = (_e: MapEvent) => { if (!mapRef.current) { return; } @@ -202,9 +192,11 @@ const MapComponent = memo(({ setIsAlertFormOpen }: MapComponentProps) => { const firstBoundaryId = boundaryId && getLayerMapId(boundaryId); - const mapOnClick = useCallback(() => { - return useMapOnClick(setIsAlertFormOpen, boundaryLayerId, mapRef.current); - }, [boundaryLayerId, setIsAlertFormOpen]); + const mapOnClick = useMapOnClick( + setIsAlertFormOpen, + boundaryLayerId, + mapRef.current, + ); const getBeforeId = useCallback( (index: number, aboveBoundaries: boolean = false) => { @@ -237,7 +229,7 @@ const MapComponent = memo(({ setIsAlertFormOpen }: MapComponentProps) => { }} mapStyle={mapStyle.toString()} onLoad={onMapLoad} - onClick={mapOnClick()} + onClick={mapOnClick} maxBounds={maxBounds} > {selectedLayers.map((layer, index) => { diff --git a/frontend/src/components/MapView/Map/utils.ts b/frontend/src/components/MapView/Map/utils.ts new file mode 100644 index 0000000000..fd6a38fa69 --- /dev/null +++ b/frontend/src/components/MapView/Map/utils.ts @@ -0,0 +1,4 @@ +export const mapStyle = new URL( + process.env.REACT_APP_DEFAULT_STYLE || + 'https://api.maptiler.com/maps/0ad52f6b-ccf2-4a36-a9b8-7ebd8365e56f/style.json?key=y2DTSu9yWiu755WByJr3', +); diff --git a/frontend/src/components/MapView/MapTooltip/PointDataChart/PopupPointDataChart.tsx b/frontend/src/components/MapView/MapTooltip/PointDataChart/PopupPointDataChart.tsx index 1db5675b38..be44e7acd0 100644 --- a/frontend/src/components/MapView/MapTooltip/PointDataChart/PopupPointDataChart.tsx +++ b/frontend/src/components/MapView/MapTooltip/PointDataChart/PopupPointDataChart.tsx @@ -6,11 +6,11 @@ import { datasetSelector, } from 'context/datasetStateSlice'; import { t } from 'i18next'; -import React, { memo } from 'react'; +import { memo } from 'react'; import { useSelector } from 'react-redux'; -import { WithStyles, createStyles, withStyles } from '@material-ui/core'; +import { createStyles, makeStyles } from '@material-ui/core'; -const styles = () => +const useStyles = makeStyles(() => createStyles({ chartContainer: { display: 'flex', @@ -23,14 +23,17 @@ const styles = () => height: '200px', width: '400px', }, - }); + }), +); -interface PopupDatasetChartProps extends WithStyles {} - -const PopupPointDataChart = ({ classes }: PopupDatasetChartProps) => { - const { data: dataset, datasetParams, title, chartType } = useSelector( - datasetSelector, - ); +const PopupPointDataChart = memo(() => { + const classes = useStyles(); + const { + data: dataset, + datasetParams, + title, + chartType, + } = useSelector(datasetSelector); const config: ChartConfig = { type: chartType, stacked: false, @@ -62,6 +65,6 @@ const PopupPointDataChart = ({ classes }: PopupDatasetChartProps) => {
); -}; +}); -export default memo(withStyles(styles)(PopupPointDataChart)); +export default PopupPointDataChart; diff --git a/frontend/src/components/MapView/MapTooltip/PointDataChart/usePointDataChart.tsx b/frontend/src/components/MapView/MapTooltip/PointDataChart/usePointDataChart.tsx index 8a8aa15ecc..55ccf33283 100644 --- a/frontend/src/components/MapView/MapTooltip/PointDataChart/usePointDataChart.tsx +++ b/frontend/src/components/MapView/MapTooltip/PointDataChart/usePointDataChart.tsx @@ -11,9 +11,11 @@ import { isAdminBoundary } from '../../utils'; const usePointDataChart = () => { const dispatch = useDispatch(); - const { data: dataset, datasetParams, isLoading } = useSelector( - datasetSelector, - ); + const { + data: dataset, + datasetParams, + isLoading, + } = useSelector(datasetSelector); const { startDate: selectedDate } = useSelector(dateRangeSelector); useEffect(() => { @@ -22,9 +24,8 @@ const usePointDataChart = () => { } if (isAdminBoundary(datasetParams)) { - const { code: adminCode, level } = datasetParams.boundaryProps[ - datasetParams.id - ]; + const { code: adminCode, level } = + datasetParams.boundaryProps[datasetParams.id]; const requestParams: DatasetRequestParams = { id: datasetParams.id, level, diff --git a/frontend/src/components/MapView/MapTooltip/PopupCharts/PopupAnalysisCharts.tsx b/frontend/src/components/MapView/MapTooltip/PopupCharts/PopupAnalysisCharts.tsx index db321fd70b..e133f2a748 100644 --- a/frontend/src/components/MapView/MapTooltip/PopupCharts/PopupAnalysisCharts.tsx +++ b/frontend/src/components/MapView/MapTooltip/PopupCharts/PopupAnalysisCharts.tsx @@ -1,4 +1,4 @@ -import { WithStyles, createStyles, withStyles } from '@material-ui/core'; +import { createStyles, makeStyles } from '@material-ui/core'; import ChartSection from 'components/MapView/LeftPanel/ChartsPanel/ChartSection'; import { oneYearInMs } from 'components/MapView/LeftPanel/utils'; import { @@ -14,7 +14,7 @@ import { dateRangeSelector, layerDataSelector, } from 'context/mapStateSlice/selectors'; -import React, { useRef } from 'react'; +import { useRef } from 'react'; import { useSelector } from 'react-redux'; import { appConfig } from 'config'; import { useSafeTranslation } from 'i18n'; @@ -22,7 +22,7 @@ import PopupChartWrapper from './PopupChartWrapper'; const { country } = appConfig; -const styles = () => +const useStyles = makeStyles(() => createStyles({ chartContainer: { display: 'flex', @@ -34,7 +34,8 @@ const styles = () => width: '400px', flexGrow: 1, }, - }); + }), +); const boundaryLayer = getBoundaryLayersByAdminLevel(); @@ -56,21 +57,21 @@ const getProperties = ( return features.properties; }; -interface PopupChartProps extends WithStyles { +interface PopupChartProps { filteredChartLayers: WMSLayerProps[]; adminCode: AdminCodeString; adminSelectorKey: string; adminLevel: AdminLevelType; adminLevelsNames: () => string[]; } -const PopupAnalysisCharts = ({ +function PopupAnalysisCharts({ filteredChartLayers, adminCode, adminSelectorKey, adminLevel, adminLevelsNames, - classes, -}: PopupChartProps) => { +}: PopupChartProps) { + const classes = useStyles(); const { t } = useSafeTranslation(); const dataForCsv = useRef<{ [key: string]: any[] }>({}); const boundaryLayerData = useSelector(layerDataSelector(boundaryLayer.id)) as @@ -113,6 +114,6 @@ const PopupAnalysisCharts = ({ ))} ); -}; +} -export default withStyles(styles)(PopupAnalysisCharts); +export default PopupAnalysisCharts; diff --git a/frontend/src/components/MapView/MapTooltip/PopupCharts/PopupChartWrapper.tsx b/frontend/src/components/MapView/MapTooltip/PopupCharts/PopupChartWrapper.tsx index a9d46adbf0..79bc6be03c 100644 --- a/frontend/src/components/MapView/MapTooltip/PopupCharts/PopupChartWrapper.tsx +++ b/frontend/src/components/MapView/MapTooltip/PopupCharts/PopupChartWrapper.tsx @@ -1,7 +1,7 @@ -import { WithStyles, createStyles, withStyles } from '@material-ui/core'; -import React, { ReactNode, memo } from 'react'; +import { createStyles, makeStyles } from '@material-ui/core'; +import { ReactNode, memo } from 'react'; -const styles = () => +const useStyles = makeStyles(() => createStyles({ chartsContainer: { position: 'relative', @@ -14,16 +14,21 @@ const styles = () => flexDirection: 'column', gap: '8px', }, - }); + }), +); -interface PopupChartWrapperProps extends WithStyles { +interface PopupChartWrapperProps { children: ReactNode; } -const PopupChartWrapper = ({ children, classes }: PopupChartWrapperProps) => ( -
-
{children}
-
-); +const PopupChartWrapper = memo(({ children }: PopupChartWrapperProps) => { + const classes = useStyles(); + + return ( +
+
{children}
+
+ ); +}); -export default memo(withStyles(styles)(PopupChartWrapper)); +export default PopupChartWrapper; diff --git a/frontend/src/components/MapView/MapTooltip/PopupCharts/PopupChartsList.tsx b/frontend/src/components/MapView/MapTooltip/PopupCharts/PopupChartsList.tsx index 2b4c9aaad2..23298d0f44 100644 --- a/frontend/src/components/MapView/MapTooltip/PopupCharts/PopupChartsList.tsx +++ b/frontend/src/components/MapView/MapTooltip/PopupCharts/PopupChartsList.tsx @@ -1,16 +1,11 @@ import { faChartBar } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { - Button, - WithStyles, - createStyles, - withStyles, -} from '@material-ui/core'; +import { Button, createStyles, makeStyles } from '@material-ui/core'; import { AdminLevelType, WMSLayerProps } from 'config/types'; import { t } from 'i18next'; import React, { memo } from 'react'; -const styles = () => +const useStyles = makeStyles(() => createStyles({ selectChartContainer: { display: 'flex', @@ -32,9 +27,10 @@ const styles = () => textOverflow: 'ellipsis', maxWidth: '280px', }, - }); + }), +); -interface PopupChartsListProps extends WithStyles { +interface PopupChartsListProps { filteredChartLayers: WMSLayerProps[]; adminLevelsNames: () => string[]; setAdminLevel: React.Dispatch< @@ -43,39 +39,41 @@ interface PopupChartsListProps extends WithStyles { availableAdminLevels: AdminLevelType[]; } -const PopupChartsList = ({ - filteredChartLayers, - adminLevelsNames, - setAdminLevel, - availableAdminLevels, - classes, -}: PopupChartsListProps) => { - return ( -
- {filteredChartLayers.map(layer => - adminLevelsNames().map((level, index) => ( -
- - )), - )} -
- ); -}; + + )), + )} +
+ ); + }, +); -export default memo(withStyles(styles)(PopupChartsList)); +export default PopupChartsList; diff --git a/frontend/src/components/MapView/MapTooltip/PopupCharts/index.tsx b/frontend/src/components/MapView/MapTooltip/PopupCharts/index.tsx index 7a62801f82..be8952516c 100644 --- a/frontend/src/components/MapView/MapTooltip/PopupCharts/index.tsx +++ b/frontend/src/components/MapView/MapTooltip/PopupCharts/index.tsx @@ -20,51 +20,53 @@ interface PopupChartsProps { availableAdminLevels: AdminLevelType[]; } -const PopupCharts = ({ - setPopupTitle, - adminCode, - adminSelectorKey, - adminLevel, - setAdminLevel, - adminLevelsNames, - availableAdminLevels, -}: PopupChartsProps) => { - const mapState = useSelector(layersSelector); +const PopupCharts = memo( + ({ + setPopupTitle, + adminCode, + adminSelectorKey, + adminLevel, + setAdminLevel, + adminLevelsNames, + availableAdminLevels, + }: PopupChartsProps) => { + const mapState = useSelector(layersSelector); - const mapStateIds = mapState.map(item => item.id); - const filteredChartLayers = chartLayers.filter(item => - mapStateIds.includes(item.id), - ); + const mapStateIds = mapState.map(item => item.id); + const filteredChartLayers = chartLayers.filter(item => + mapStateIds.includes(item.id), + ); - useEffect(() => { - if (adminLevel !== undefined) { - setPopupTitle(adminLevelsNames().join(', ')); - } else { - setPopupTitle(''); - } - }, [adminLevel, adminLevelsNames, setPopupTitle]); + useEffect(() => { + if (adminLevel !== undefined) { + setPopupTitle(adminLevelsNames().join(', ')); + } else { + setPopupTitle(''); + } + }, [adminLevel, adminLevelsNames, setPopupTitle]); - return ( - <> - {adminLevel === undefined && ( - - )} - {adminLevel !== undefined && ( - - )} - - ); -}; + return ( + <> + {adminLevel === undefined && ( + + )} + {adminLevel !== undefined && ( + + )} + + ); + }, +); -export default memo(PopupCharts); +export default PopupCharts; diff --git a/frontend/src/components/MapView/MapTooltip/PopupContent.tsx b/frontend/src/components/MapView/MapTooltip/PopupContent.tsx index 90beaa2b05..64015f04dc 100644 --- a/frontend/src/components/MapView/MapTooltip/PopupContent.tsx +++ b/frontend/src/components/MapView/MapTooltip/PopupContent.tsx @@ -1,9 +1,4 @@ -import { - Typography, - WithStyles, - createStyles, - withStyles, -} from '@material-ui/core'; +import { Typography, createStyles, makeStyles } from '@material-ui/core'; import { ClassNameMap } from '@material-ui/styles'; import { PopupData, PopupMetaData } from 'context/tooltipStateSlice'; import { Position } from 'geojson'; @@ -12,7 +7,7 @@ import { isEmpty, isEqual, sum } from 'lodash'; import React, { Fragment, memo } from 'react'; import { TFunction } from 'utils/data-utils'; -const styles = () => +const useStyles = makeStyles(() => createStyles({ phasePopulationTable: { tableLayout: 'fixed', @@ -29,7 +24,8 @@ const styles = () => text: { marginBottom: '4px', }, - }); + }), +); // This function prepares phasePopulationTable for rendering and is specific // to the data structure of the phase classification layer. @@ -112,16 +108,13 @@ const generatePhasePopulationTable = ( return phasePopulationTable; }; -interface PopupContentProps extends WithStyles { +interface PopupContentProps { popupData: PopupData & PopupMetaData; coordinates: Position | undefined; } -const PopupContent = ({ - popupData, - coordinates, - classes, -}: PopupContentProps) => { +const PopupContent = memo(({ popupData, coordinates }: PopupContentProps) => { + const classes = useStyles(); const { t } = useSafeTranslation(); const phasePopulationTable = generatePhasePopulationTable( @@ -205,6 +198,6 @@ const PopupContent = ({ })} ); -}; +}); -export default memo(withStyles(styles)(PopupContent)); +export default PopupContent; diff --git a/frontend/src/components/MapView/MapTooltip/RedirectToDMP.tsx b/frontend/src/components/MapView/MapTooltip/RedirectToDMP.tsx index baac5c1bd5..69debe2e1b 100644 --- a/frontend/src/components/MapView/MapTooltip/RedirectToDMP.tsx +++ b/frontend/src/components/MapView/MapTooltip/RedirectToDMP.tsx @@ -1,15 +1,9 @@ import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { - Link, - Typography, - WithStyles, - createStyles, - withStyles, -} from '@material-ui/core'; -import React, { memo } from 'react'; +import { Link, Typography, createStyles, makeStyles } from '@material-ui/core'; +import { memo } from 'react'; -const styles = () => +const useStyles = makeStyles(() => createStyles({ externalLinkContainer: { display: 'flex', @@ -19,9 +13,10 @@ const styles = () => marginBottom: '8px', alignItems: 'center', }, - }); + }), +); -interface RedirectToDMPProps extends WithStyles { +interface RedirectToDMPProps { dmpDisTyp: string | undefined; dmpSubmissionId: string | undefined; } @@ -39,27 +34,26 @@ const computeDisasterTypeFromDistTyp = (distTyp: string) => { return 'INCIDENT'; }; -const RedirectToDMP = ({ - dmpDisTyp, - dmpSubmissionId, - classes, -}: RedirectToDMPProps) => { - if (!dmpDisTyp) { - return null; - } - return ( - - - Report details - - - - ); -}; +const RedirectToDMP = memo( + ({ dmpDisTyp, dmpSubmissionId }: RedirectToDMPProps) => { + const classes = useStyles(); + if (!dmpDisTyp) { + return null; + } + return ( + + + Report details + + + + ); + }, +); -export default memo(withStyles(styles)(RedirectToDMP)); +export default RedirectToDMP; diff --git a/frontend/src/components/MapView/MapTooltip/index.tsx b/frontend/src/components/MapView/MapTooltip/index.tsx index 6d79914bce..351b9230dc 100644 --- a/frontend/src/components/MapView/MapTooltip/index.tsx +++ b/frontend/src/components/MapView/MapTooltip/index.tsx @@ -1,12 +1,11 @@ -import React, { memo, useCallback, useMemo, useState } from 'react'; +import { memo, useCallback, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Popup } from 'react-map-gl/maplibre'; import { createStyles, - withStyles, - WithStyles, Typography, IconButton, + makeStyles, } from '@material-ui/core'; import { hidePopup, tooltipSelector } from 'context/tooltipStateSlice'; import { isEnglishLanguageSelected, useSafeTranslation } from 'i18n'; @@ -21,7 +20,7 @@ import PopupContent from './PopupContent'; import PopupPointDataChart from './PointDataChart/PopupPointDataChart'; import usePointDataChart from './PointDataChart/usePointDataChart'; -const styles = () => +const useStyles = makeStyles(() => createStyles({ phasePopulationTable: { tableLayout: 'fixed', @@ -62,16 +61,16 @@ const styles = () => right: 0, top: 0, }, - }); + }), +); const { multiCountry } = appConfig; const availableAdminLevels: AdminLevelType[] = multiCountry ? [0, 1, 2] : [1, 2]; -interface TooltipProps extends WithStyles {} - -const MapTooltip = ({ classes }: TooltipProps) => { +const MapTooltip = memo(() => { + const classes = useStyles(); const dispatch = useDispatch(); const popup = useSelector(tooltipSelector); const { i18n } = useSafeTranslation(); @@ -111,9 +110,12 @@ const MapTooltip = ({ classes }: TooltipProps) => { return null; } + const key = JSON.stringify(popup.coordinates); + if (dataset) { return ( { return ( { ); -}; +}); -export default memo(withStyles(styles)(MapTooltip)); +export default MapTooltip; diff --git a/frontend/src/components/MapView/OtherFeatures/index.tsx b/frontend/src/components/MapView/OtherFeatures/index.tsx index 96eaf57a25..e75e1f3a3c 100644 --- a/frontend/src/components/MapView/OtherFeatures/index.tsx +++ b/frontend/src/components/MapView/OtherFeatures/index.tsx @@ -1,32 +1,34 @@ -import { Box, WithStyles, createStyles, withStyles } from '@material-ui/core'; -import React, { memo, useMemo } from 'react'; +import { Box, createStyles, makeStyles } from '@material-ui/core'; +import { memo, useMemo } from 'react'; import useLayers from 'utils/layers-utils'; import DateSelector from '../DateSelector'; import BoundaryInfoBox from '../BoundaryInfoBox'; -const styles = createStyles({ - container: { - height: '100%', - width: '100%', - position: 'absolute', - top: 0, - right: 0, - }, - optionContainer: { - position: 'relative', - height: '100%', - display: 'flex', - }, -}); - -interface OtherFeaturesProps extends WithStyles {} +const useStyles = makeStyles(() => + createStyles({ + container: { + height: '100%', + width: '100%', + position: 'absolute', + top: 0, + right: 0, + }, + optionContainer: { + position: 'relative', + height: '100%', + display: 'flex', + }, + }), +); -const OtherFeatures = ({ classes }: OtherFeaturesProps) => { +const OtherFeatures = memo(() => { const { selectedLayerDates } = useLayers(); + const classes = useStyles(); - const showBoundaryInfo = useMemo(() => { - return JSON.parse(process.env.REACT_APP_SHOW_MAP_INFO || 'false'); - }, []); + const showBoundaryInfo = useMemo( + () => JSON.parse(process.env.REACT_APP_SHOW_MAP_INFO || 'false'), + [], + ); return ( @@ -36,6 +38,6 @@ const OtherFeatures = ({ classes }: OtherFeaturesProps) => { ); -}; +}); -export default memo(withStyles(styles)(OtherFeatures)); +export default OtherFeatures; diff --git a/frontend/src/components/MapView/__snapshots__/index.test.tsx.snap b/frontend/src/components/MapView/__snapshots__/index.test.tsx.snap index dc47108706..945ea39ae1 100644 --- a/frontend/src/components/MapView/__snapshots__/index.test.tsx.snap +++ b/frontend/src/components/MapView/__snapshots__/index.test.tsx.snap @@ -3,7 +3,7 @@ exports[`renders as expected 1`] = `