From d90ee8f4b222cf6fa5c633f295433f8ceb77faf8 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 18 Jan 2024 11:26:03 -0800 Subject: [PATCH 01/85] stubbing out survey page --- app/src/features/surveys/view/SurveyMap.tsx | 125 +++++++++++++++++++ app/src/features/surveys/view/SurveyPage.tsx | 25 ++-- 2 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 app/src/features/surveys/view/SurveyMap.tsx diff --git a/app/src/features/surveys/view/SurveyMap.tsx b/app/src/features/surveys/view/SurveyMap.tsx new file mode 100644 index 0000000000..2a71e722a0 --- /dev/null +++ b/app/src/features/surveys/view/SurveyMap.tsx @@ -0,0 +1,125 @@ +import { Box, Paper, Toolbar } from '@mui/material'; +import Button from '@mui/material/Button'; +import BaseLayerControls from 'components/map/components/BaseLayerControls'; +import { SetMapBounds } from 'components/map/components/Bounds'; +import FullScreenScrollingEventHandler from 'components/map/components/FullScreenScrollingEventHandler'; +import { MapBaseCss } from 'components/map/styles/MapBaseCss'; +import { ALL_OF_BC_BOUNDARY, MAP_DEFAULT_CENTER } from 'constants/spatial'; +import { ObservationsContext } from 'contexts/observationsContext'; +import { Position } from 'geojson'; +import { LatLngBoundsExpression } from 'leaflet'; +import { useContext, useMemo, useState } from 'react'; +import { GeoJSON, LayersControl, MapContainer as LeafletMapContainer } from 'react-leaflet'; +import { calculateUpdatedMapBounds } from 'utils/mapBoundaryUploadHelpers'; +import { coloredPoint, INonEditableGeometries } from 'utils/mapUtils'; +import { v4 as uuidv4 } from 'uuid'; +import NoSurveySectionData from '../components/NoSurveySectionData'; + +export enum SurveyMapData { + OBSERVATIONS = 'Observations', + TELEMETRY = 'Telemetry', + MARKED_ANIMALS = 'Marked Animals' +} + +const SurveyMap = () => { + const observationsContext = useContext(ObservationsContext); + // set default bounds to encompass all of BC + const [bounds] = useState(calculateUpdatedMapBounds([ALL_OF_BC_BOUNDARY])); + + const surveyObservations: INonEditableGeometries[] = useMemo(() => { + const observations = observationsContext.observationsDataLoader.data?.surveyObservations; + + if (!observations) { + return []; + } + + return observations + .filter((observation) => observation.latitude !== undefined && observation.longitude !== undefined) + .map((observation) => { + return { + feature: { + type: 'Feature', + properties: {}, + geometry: { + type: 'Point', + coordinates: [observation.longitude, observation.latitude] as Position + } + }, + popupComponent: undefined + }; + }); + }, [observationsContext.observationsDataLoader.data]); + + const [mapPoints, setMapPoints] = useState(surveyObservations); + + const setCurrentDataSet = (dataset: SurveyMapData) => { + console.log(`Current Data Selected: ${dataset}`); + switch (dataset) { + case SurveyMapData.OBSERVATIONS: + setMapPoints(surveyObservations); + break; + case SurveyMapData.TELEMETRY: + setMapPoints([]); + break; + case SurveyMapData.MARKED_ANIMALS: + setMapPoints([]); + break; + + default: + setMapPoints([]); + break; + } + }; + + return ( + + + + + + + + + + + + + + + + {mapPoints?.map((nonEditableGeo: INonEditableGeometries) => ( + coloredPoint({ latlng, fillColor: '#1f7dff', borderColor: '#ffffff' })}> + {nonEditableGeo.popupComponent} + + ))} + + + + + + + + + + + + + ); +}; + +export default SurveyMap; diff --git a/app/src/features/surveys/view/SurveyPage.tsx b/app/src/features/surveys/view/SurveyPage.tsx index 1dd18d5371..972e9d1ab9 100644 --- a/app/src/features/surveys/view/SurveyPage.tsx +++ b/app/src/features/surveys/view/SurveyPage.tsx @@ -18,13 +18,10 @@ import { SurveyContext } from 'contexts/surveyContext'; import SurveyDetails from 'features/surveys/view/SurveyDetails'; import React, { useContext, useEffect } from 'react'; import { Link as RouterLink } from 'react-router-dom'; -import ObservationsMap from '../observations/ObservationsMap'; -import ManualTelemetrySection from '../telemetry/ManualTelemetrySection'; import SurveyStudyArea from './components/SurveyStudyArea'; -import SurveySummaryResults from './summary-results/SurveySummaryResults'; -import SurveyAnimals from './SurveyAnimals'; import SurveyAttachments from './SurveyAttachments'; import SurveyHeader from './SurveyHeader'; +import SurveyMap from './SurveyMap'; //TODO: PRODUCTION_BANDAGE: Remove @@ -85,7 +82,8 @@ const SurveyPage: React.FC = () => { Manage Observations - + {/* */} + {observationsContext.observationsDataLoader.isLoading && ( { )} - + {/* */} - + + {/* + + + + */} - + {/* - + */} - + {/* - + */} From a508373fcf9eb252ade6b40db192f9e50deec0ac Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 18 Jan 2024 13:39:45 -0800 Subject: [PATCH 02/85] stubbing out control component --- app/src/features/surveys/view/SurveyMap.tsx | 46 ++----------------- .../view/components/SurveyMapToolBar.tsx | 45 ++++++++++++++++++ 2 files changed, 49 insertions(+), 42 deletions(-) create mode 100644 app/src/features/surveys/view/components/SurveyMapToolBar.tsx diff --git a/app/src/features/surveys/view/SurveyMap.tsx b/app/src/features/surveys/view/SurveyMap.tsx index 2a71e722a0..6866266af9 100644 --- a/app/src/features/surveys/view/SurveyMap.tsx +++ b/app/src/features/surveys/view/SurveyMap.tsx @@ -1,5 +1,4 @@ -import { Box, Paper, Toolbar } from '@mui/material'; -import Button from '@mui/material/Button'; +import { Box, Paper } from '@mui/material'; import BaseLayerControls from 'components/map/components/BaseLayerControls'; import { SetMapBounds } from 'components/map/components/Bounds'; import FullScreenScrollingEventHandler from 'components/map/components/FullScreenScrollingEventHandler'; @@ -14,12 +13,7 @@ import { calculateUpdatedMapBounds } from 'utils/mapBoundaryUploadHelpers'; import { coloredPoint, INonEditableGeometries } from 'utils/mapUtils'; import { v4 as uuidv4 } from 'uuid'; import NoSurveySectionData from '../components/NoSurveySectionData'; - -export enum SurveyMapData { - OBSERVATIONS = 'Observations', - TELEMETRY = 'Telemetry', - MARKED_ANIMALS = 'Marked Animals' -} +import SurveyMapToolBar from './components/SurveyMapToolBar'; const SurveyMap = () => { const observationsContext = useContext(ObservationsContext); @@ -50,43 +44,11 @@ const SurveyMap = () => { }); }, [observationsContext.observationsDataLoader.data]); - const [mapPoints, setMapPoints] = useState(surveyObservations); - - const setCurrentDataSet = (dataset: SurveyMapData) => { - console.log(`Current Data Selected: ${dataset}`); - switch (dataset) { - case SurveyMapData.OBSERVATIONS: - setMapPoints(surveyObservations); - break; - case SurveyMapData.TELEMETRY: - setMapPoints([]); - break; - case SurveyMapData.MARKED_ANIMALS: - setMapPoints([]); - break; - - default: - setMapPoints([]); - break; - } - }; + const [mapPoints] = useState(surveyObservations); return ( - - - - - - - - + { + // const setCurrentDataSet = (dataset: SurveyMapDataSet) => { + // console.log(`Current Data Selected: ${dataset}`); + // }; + return ( + <> + + {}}> + Observations + Telemetry + Marked Animals + + + MAP + TABLE + SPLIT + + + + + ); +}; + +export default SurveyMapToolBar; From c1efc4124f54c15c752690682d959a317ee13186 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 18 Jan 2024 15:49:06 -0800 Subject: [PATCH 03/85] wip --- app/src/features/surveys/view/SurveyPage.tsx | 4 +- .../{SurveyMap.tsx => SurveySpatialData.tsx} | 4 +- .../view/components/SurveyMapToolBar.tsx | 65 ++++++++++++++----- 3 files changed, 51 insertions(+), 22 deletions(-) rename app/src/features/surveys/view/{SurveyMap.tsx => SurveySpatialData.tsx} (97%) diff --git a/app/src/features/surveys/view/SurveyPage.tsx b/app/src/features/surveys/view/SurveyPage.tsx index 972e9d1ab9..fc7b01a226 100644 --- a/app/src/features/surveys/view/SurveyPage.tsx +++ b/app/src/features/surveys/view/SurveyPage.tsx @@ -21,7 +21,7 @@ import { Link as RouterLink } from 'react-router-dom'; import SurveyStudyArea from './components/SurveyStudyArea'; import SurveyAttachments from './SurveyAttachments'; import SurveyHeader from './SurveyHeader'; -import SurveyMap from './SurveyMap'; +import SurveySpatialData from './SurveySpatialData'; //TODO: PRODUCTION_BANDAGE: Remove @@ -122,7 +122,7 @@ const SurveyPage: React.FC = () => { - + {/* diff --git a/app/src/features/surveys/view/SurveyMap.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx similarity index 97% rename from app/src/features/surveys/view/SurveyMap.tsx rename to app/src/features/surveys/view/SurveySpatialData.tsx index 6866266af9..e0da2db6d1 100644 --- a/app/src/features/surveys/view/SurveyMap.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -15,7 +15,7 @@ import { v4 as uuidv4 } from 'uuid'; import NoSurveySectionData from '../components/NoSurveySectionData'; import SurveyMapToolBar from './components/SurveyMapToolBar'; -const SurveyMap = () => { +const SurveySpatialData = () => { const observationsContext = useContext(ObservationsContext); // set default bounds to encompass all of BC const [bounds] = useState(calculateUpdatedMapBounds([ALL_OF_BC_BOUNDARY])); @@ -84,4 +84,4 @@ const SurveyMap = () => { ); }; -export default SurveyMap; +export default SurveySpatialData; diff --git a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx index f3f22b86d1..e645057a9d 100644 --- a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx +++ b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx @@ -1,5 +1,6 @@ -import { Box, ToggleButton, ToggleButtonGroup } from '@mui/material'; +import { Box, Menu, MenuItem, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material'; import Button from '@mui/material/Button'; +import { useState } from 'react'; export enum SurveyMapDataSet { OBSERVATIONS = 'Observations', @@ -14,29 +15,57 @@ export enum SurveyMapDataDisplay { } const SurveyMapToolBar = () => { - // const setCurrentDataSet = (dataset: SurveyMapDataSet) => { - // console.log(`Current Data Selected: ${dataset}`); - // }; + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const updateDataSet = (event: React.MouseEvent, newAlignment: string | null) => { + console.log(`DataSet: ${newAlignment}`); + }; + const updateLayout = (event: React.MouseEvent, newAlignment: string | null) => { + console.log(`Layout: ${newAlignment}`); + }; + + const handleMenuClick = (e: any) => { + setAnchorEl(e.currentTarget); + }; + + const handleMenuClose = () => { + setAnchorEl(null); + }; + return ( <> + + Observations + Telemetry + Marked Animals + - {}}> - Observations - Telemetry - Marked Animals - - - MAP - TABLE - SPLIT - - + + + Survey Data + + + + + + Observations + Telemetry + Marked Animals + + + MAP + TABLE + SPLIT + + ); From cca089914415eedfb8bcec827cceeeaeff9ad6f1 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 18 Jan 2024 16:38:50 -0800 Subject: [PATCH 04/85] got toggle working --- app/src/features/surveys/view/SurveyMap.tsx | 48 ++++++++++++++ .../surveys/view/SurveySpatialData.tsx | 64 ++++++++----------- .../view/components/SurveyMapToolBar.tsx | 18 ++++-- 3 files changed, 84 insertions(+), 46 deletions(-) create mode 100644 app/src/features/surveys/view/SurveyMap.tsx diff --git a/app/src/features/surveys/view/SurveyMap.tsx b/app/src/features/surveys/view/SurveyMap.tsx new file mode 100644 index 0000000000..6d38a9fb1d --- /dev/null +++ b/app/src/features/surveys/view/SurveyMap.tsx @@ -0,0 +1,48 @@ +import BaseLayerControls from 'components/map/components/BaseLayerControls'; +import { SetMapBounds } from 'components/map/components/Bounds'; +import FullScreenScrollingEventHandler from 'components/map/components/FullScreenScrollingEventHandler'; +import { MapBaseCss } from 'components/map/styles/MapBaseCss'; +import { ALL_OF_BC_BOUNDARY, MAP_DEFAULT_CENTER } from 'constants/spatial'; +import { LatLngBoundsExpression } from 'leaflet'; +import { useState } from 'react'; +import { GeoJSON, LayersControl, MapContainer as LeafletMapContainer } from 'react-leaflet'; +import { calculateUpdatedMapBounds } from 'utils/mapBoundaryUploadHelpers'; +import { coloredPoint, INonEditableGeometries } from 'utils/mapUtils'; +import { v4 as uuidv4 } from 'uuid'; + +interface ISurveyMapProps { + mapPoints: INonEditableGeometries[]; +} +const SurveyMap = (props: ISurveyMapProps) => { + const [bounds] = useState(calculateUpdatedMapBounds([ALL_OF_BC_BOUNDARY])); + return ( + <> + + + + + + {props.mapPoints?.map((nonEditableGeo: INonEditableGeometries) => ( + coloredPoint({ latlng, fillColor: '#1f7dff', borderColor: '#ffffff' })}> + {nonEditableGeo.popupComponent} + + ))} + + + + + + + ); +}; + +export default SurveyMap; diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index e0da2db6d1..c45b9352b3 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -1,24 +1,14 @@ import { Box, Paper } from '@mui/material'; -import BaseLayerControls from 'components/map/components/BaseLayerControls'; -import { SetMapBounds } from 'components/map/components/Bounds'; -import FullScreenScrollingEventHandler from 'components/map/components/FullScreenScrollingEventHandler'; -import { MapBaseCss } from 'components/map/styles/MapBaseCss'; -import { ALL_OF_BC_BOUNDARY, MAP_DEFAULT_CENTER } from 'constants/spatial'; import { ObservationsContext } from 'contexts/observationsContext'; import { Position } from 'geojson'; -import { LatLngBoundsExpression } from 'leaflet'; import { useContext, useMemo, useState } from 'react'; -import { GeoJSON, LayersControl, MapContainer as LeafletMapContainer } from 'react-leaflet'; -import { calculateUpdatedMapBounds } from 'utils/mapBoundaryUploadHelpers'; -import { coloredPoint, INonEditableGeometries } from 'utils/mapUtils'; -import { v4 as uuidv4 } from 'uuid'; +import { INonEditableGeometries } from 'utils/mapUtils'; import NoSurveySectionData from '../components/NoSurveySectionData'; -import SurveyMapToolBar from './components/SurveyMapToolBar'; +import SurveyMapToolBar, { SurveyMapDataSet } from './components/SurveyMapToolBar'; +import SurveyMap from './SurveyMap'; const SurveySpatialData = () => { const observationsContext = useContext(ObservationsContext); - // set default bounds to encompass all of BC - const [bounds] = useState(calculateUpdatedMapBounds([ALL_OF_BC_BOUNDARY])); const surveyObservations: INonEditableGeometries[] = useMemo(() => { const observations = observationsContext.observationsDataLoader.data?.surveyObservations; @@ -44,36 +34,32 @@ const SurveySpatialData = () => { }); }, [observationsContext.observationsDataLoader.data]); - const [mapPoints] = useState(surveyObservations); + const [mapPoints, setMapPoints] = useState(surveyObservations); + + const updateDataSet = (data: SurveyMapDataSet) => { + console.log(`DataSet: ${data}`); + switch (data) { + case SurveyMapDataSet.OBSERVATIONS: + setMapPoints(surveyObservations); + break; + case SurveyMapDataSet.TELEMETRY: + setMapPoints([]); + break; + case SurveyMapDataSet.MARKED_ANIMALS: + setMapPoints([]); + break; + + default: + setMapPoints([]); + break; + } + }; return ( - + {}} /> - - - - - - {mapPoints?.map((nonEditableGeo: INonEditableGeometries) => ( - coloredPoint({ latlng, fillColor: '#1f7dff', borderColor: '#ffffff' })}> - {nonEditableGeo.popupComponent} - - ))} - - - - - + diff --git a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx index e645057a9d..b1538d6af4 100644 --- a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx +++ b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx @@ -14,15 +14,19 @@ export enum SurveyMapDataDisplay { SPLIT = 'Split' } -const SurveyMapToolBar = () => { +interface ISurveyMapToolBarProps { + updateDataSet: (data: SurveyMapDataSet) => void; + updateLayout: (data: SurveyMapDataDisplay) => void; +} +const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); - const updateDataSet = (event: React.MouseEvent, newAlignment: string | null) => { - console.log(`DataSet: ${newAlignment}`); + const updateDataSet = (event: React.MouseEvent, newAlignment: SurveyMapDataSet) => { + props.updateDataSet(newAlignment); }; - const updateLayout = (event: React.MouseEvent, newAlignment: string | null) => { - console.log(`Layout: ${newAlignment}`); + const updateLayout = (event: React.MouseEvent, newAlignment: SurveyMapDataDisplay) => { + props.updateLayout(newAlignment); }; const handleMenuClick = (e: any) => { @@ -55,12 +59,12 @@ const SurveyMapToolBar = () => { - + Observations Telemetry Marked Animals - + MAP TABLE SPLIT From 40c1baeb14a4e07c027cea93a5f96254b0e29851 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 18 Jan 2024 16:52:26 -0800 Subject: [PATCH 05/85] added controls for layout --- app/src/features/surveys/view/SurveyMap.tsx | 1 + .../surveys/view/SurveySpatialData.tsx | 47 ++++++++++++++----- .../view/components/SurveyMapToolBar.tsx | 24 +++++----- 3 files changed, 49 insertions(+), 23 deletions(-) diff --git a/app/src/features/surveys/view/SurveyMap.tsx b/app/src/features/surveys/view/SurveyMap.tsx index 6d38a9fb1d..723737c8fd 100644 --- a/app/src/features/surveys/view/SurveyMap.tsx +++ b/app/src/features/surveys/view/SurveyMap.tsx @@ -23,6 +23,7 @@ const SurveyMap = (props: ISurveyMapProps) => { center={MAP_DEFAULT_CENTER} scrollWheelZoom={false} fullscreenControl={true} + // style={{ height: '100%', width: '800px' }} style={{ height: '100%' }}> diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index c45b9352b3..6753f5b508 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -4,7 +4,7 @@ import { Position } from 'geojson'; import { useContext, useMemo, useState } from 'react'; import { INonEditableGeometries } from 'utils/mapUtils'; import NoSurveySectionData from '../components/NoSurveySectionData'; -import SurveyMapToolBar, { SurveyMapDataSet } from './components/SurveyMapToolBar'; +import SurveyMapToolBar, { SurveySpatialDataLayout, SurveySpatialDataSet } from './components/SurveyMapToolBar'; import SurveyMap from './SurveyMap'; const SurveySpatialData = () => { @@ -36,16 +36,19 @@ const SurveySpatialData = () => { const [mapPoints, setMapPoints] = useState(surveyObservations); - const updateDataSet = (data: SurveyMapDataSet) => { + // TODO: this needs to be saved between page visits + const [layout, setLayout] = useState(SurveySpatialDataLayout.MAP); + + const updateDataSet = (data: SurveySpatialDataSet) => { console.log(`DataSet: ${data}`); switch (data) { - case SurveyMapDataSet.OBSERVATIONS: + case SurveySpatialDataSet.OBSERVATIONS: setMapPoints(surveyObservations); break; - case SurveyMapDataSet.TELEMETRY: + case SurveySpatialDataSet.TELEMETRY: setMapPoints([]); break; - case SurveyMapDataSet.MARKED_ANIMALS: + case SurveySpatialDataSet.MARKED_ANIMALS: setMapPoints([]); break; @@ -55,17 +58,39 @@ const SurveySpatialData = () => { } }; + const updateLayout = (data: SurveySpatialDataLayout) => { + console.log(`Layout: ${data}`); + setLayout(data); + }; + return ( - {}} /> - - - - + + + {layout === SurveySpatialDataLayout.MAP && ( + + + + )} + + {layout === SurveySpatialDataLayout.TABLE && ( - + )} + + {layout === SurveySpatialDataLayout.SPLIT && ( + + + + + + + + + + + )} ); }; diff --git a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx index b1538d6af4..2202d52548 100644 --- a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx +++ b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx @@ -2,30 +2,30 @@ import { Box, Menu, MenuItem, ToggleButton, ToggleButtonGroup, Typography } from import Button from '@mui/material/Button'; import { useState } from 'react'; -export enum SurveyMapDataSet { +export enum SurveySpatialDataSet { OBSERVATIONS = 'Observations', TELEMETRY = 'Telemetry', MARKED_ANIMALS = 'Marked Animals' } -export enum SurveyMapDataDisplay { +export enum SurveySpatialDataLayout { MAP = 'Map', TABLE = 'Table', SPLIT = 'Split' } interface ISurveyMapToolBarProps { - updateDataSet: (data: SurveyMapDataSet) => void; - updateLayout: (data: SurveyMapDataDisplay) => void; + updateDataSet: (data: SurveySpatialDataSet) => void; + updateLayout: (data: SurveySpatialDataLayout) => void; } const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); - const updateDataSet = (event: React.MouseEvent, newAlignment: SurveyMapDataSet) => { + const updateDataSet = (event: React.MouseEvent, newAlignment: SurveySpatialDataSet) => { props.updateDataSet(newAlignment); }; - const updateLayout = (event: React.MouseEvent, newAlignment: SurveyMapDataDisplay) => { + const updateLayout = (event: React.MouseEvent, newAlignment: SurveySpatialDataLayout) => { props.updateLayout(newAlignment); }; @@ -60,14 +60,14 @@ const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { - Observations - Telemetry - Marked Animals + Observations + Telemetry + Marked Animals - MAP - TABLE - SPLIT + MAP + TABLE + SPLIT From 50d61567d2902c1e84560a0c6142931d7758c86f Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 19 Jan 2024 09:54:47 -0800 Subject: [PATCH 06/85] clean up --- app/src/features/surveys/view/SurveyPage.tsx | 77 +------------------ .../surveys/view/SurveySpatialData.tsx | 1 - .../view/components/SurveyMapToolBar.tsx | 9 ++- 3 files changed, 8 insertions(+), 79 deletions(-) diff --git a/app/src/features/surveys/view/SurveyPage.tsx b/app/src/features/surveys/view/SurveyPage.tsx index fc7b01a226..00a5f5a99f 100644 --- a/app/src/features/surveys/view/SurveyPage.tsx +++ b/app/src/features/surveys/view/SurveyPage.tsx @@ -1,14 +1,7 @@ -import { mdiCog, mdiMapSearchOutline } from '@mdi/js'; -import Icon from '@mdi/react'; import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; import CircularProgress from '@mui/material/CircularProgress'; -import grey from '@mui/material/colors/grey'; import Container from '@mui/material/Container'; import Paper from '@mui/material/Paper'; -import Skeleton from '@mui/material/Skeleton'; -import Toolbar from '@mui/material/Toolbar'; -import Typography from '@mui/material/Typography'; import SurveySubmissionAlertBar from 'components/publish/SurveySubmissionAlertBar'; import { SystemRoleGuard } from 'components/security/Guards'; import { SYSTEM_ROLE } from 'constants/roles'; @@ -17,7 +10,6 @@ import { ObservationsContext } from 'contexts/observationsContext'; import { SurveyContext } from 'contexts/surveyContext'; import SurveyDetails from 'features/surveys/view/SurveyDetails'; import React, { useContext, useEffect } from 'react'; -import { Link as RouterLink } from 'react-router-dom'; import SurveyStudyArea from './components/SurveyStudyArea'; import SurveyAttachments from './SurveyAttachments'; import SurveyHeader from './SurveyHeader'; @@ -35,9 +27,6 @@ const SurveyPage: React.FC = () => { const surveyContext = useContext(SurveyContext); const observationsContext = useContext(ObservationsContext); - const numObservations: number = - observationsContext.observationsDataLoader.data?.supplementaryObservationData?.observationCount || 0; - useEffect(() => { codesContext.codesDataLoader.load(); }, [codesContext.codesDataLoader]); @@ -55,76 +44,12 @@ const SurveyPage: React.FC = () => { <> - - - - Observations ‌ - {!numObservations ? ( - ' ' - ) : ( - - ({numObservations}) - - )} - - - - {/* */} - - {observationsContext.observationsDataLoader.isLoading && ( - - - - - )} - {/* */} - - - + {/* diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 6753f5b508..5ef12c2476 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -51,7 +51,6 @@ const SurveySpatialData = () => { case SurveySpatialDataSet.MARKED_ANIMALS: setMapPoints([]); break; - default: setMapPoints([]); break; diff --git a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx index 2202d52548..ec1a665fdc 100644 --- a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx +++ b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx @@ -19,13 +19,18 @@ interface ISurveyMapToolBarProps { updateLayout: (data: SurveySpatialDataLayout) => void; } const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { + const [dataset, setDataset] = useState(SurveySpatialDataSet.OBSERVATIONS); + const [layout, setLayout] = useState(SurveySpatialDataLayout.MAP); + const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); const updateDataSet = (event: React.MouseEvent, newAlignment: SurveySpatialDataSet) => { + setDataset(newAlignment); props.updateDataSet(newAlignment); }; const updateLayout = (event: React.MouseEvent, newAlignment: SurveySpatialDataLayout) => { + setLayout(newAlignment); props.updateLayout(newAlignment); }; @@ -59,12 +64,12 @@ const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { - + Observations Telemetry Marked Animals - + MAP TABLE SPLIT From 42fa76b42b8ed440ca5aceb70959b59dd9dff4ff Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 19 Jan 2024 11:33:00 -0800 Subject: [PATCH 07/85] telemetry data showing on map --- app/src/features/projects/ProjectsRouter.tsx | 5 ++- app/src/features/surveys/view/SurveyMap.tsx | 1 + .../surveys/view/SurveySpatialData.tsx | 40 ++++++++++++++++++- .../view/components/SurveyMapToolBar.tsx | 1 + 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/app/src/features/projects/ProjectsRouter.tsx b/app/src/features/projects/ProjectsRouter.tsx index d057b4e85f..3ecda8a27f 100644 --- a/app/src/features/projects/ProjectsRouter.tsx +++ b/app/src/features/projects/ProjectsRouter.tsx @@ -4,6 +4,7 @@ import { ObservationsContextProvider } from 'contexts/observationsContext'; import { ProjectAuthStateContextProvider } from 'contexts/projectAuthStateContext'; import { ProjectContextProvider } from 'contexts/projectContext'; import { SurveyContextProvider } from 'contexts/surveyContext'; +import { TelemetryDataContextProvider } from 'contexts/telemetryDataContext'; import ProjectPage from 'features/projects/view/ProjectPage'; import CreateSurveyPage from 'features/surveys/CreateSurveyPage'; import SurveyRouter from 'features/surveys/SurveyRouter'; @@ -75,7 +76,9 @@ const ProjectsRouter: React.FC = () => { validSystemRoles={[SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.DATA_ADMINISTRATOR]}> - + + + diff --git a/app/src/features/surveys/view/SurveyMap.tsx b/app/src/features/surveys/view/SurveyMap.tsx index 723737c8fd..31fe5d9bbb 100644 --- a/app/src/features/surveys/view/SurveyMap.tsx +++ b/app/src/features/surveys/view/SurveyMap.tsx @@ -13,6 +13,7 @@ import { v4 as uuidv4 } from 'uuid'; interface ISurveyMapProps { mapPoints: INonEditableGeometries[]; } +// TODO: need a way to pass in the map dimensions depending on the screen size const SurveyMap = (props: ISurveyMapProps) => { const [bounds] = useState(calculateUpdatedMapBounds([ALL_OF_BC_BOUNDARY])); return ( diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 5ef12c2476..124a8a0dfe 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -1,7 +1,9 @@ import { Box, Paper } from '@mui/material'; import { ObservationsContext } from 'contexts/observationsContext'; +import { SurveyContext } from 'contexts/surveyContext'; +import { TelemetryDataContext } from 'contexts/telemetryDataContext'; import { Position } from 'geojson'; -import { useContext, useMemo, useState } from 'react'; +import { useContext, useEffect, useMemo, useState } from 'react'; import { INonEditableGeometries } from 'utils/mapUtils'; import NoSurveySectionData from '../components/NoSurveySectionData'; import SurveyMapToolBar, { SurveySpatialDataLayout, SurveySpatialDataSet } from './components/SurveyMapToolBar'; @@ -9,6 +11,40 @@ import SurveyMap from './SurveyMap'; const SurveySpatialData = () => { const observationsContext = useContext(ObservationsContext); + const telemetryContext = useContext(TelemetryDataContext); + const surveyContext = useContext(SurveyContext); + + useEffect(() => { + surveyContext.deploymentDataLoader.load(surveyContext.projectId, surveyContext.surveyId).then(() => { + if (surveyContext.deploymentDataLoader.data) { + const deploymentIds = surveyContext.deploymentDataLoader.data.map((item) => item.deployment_id); + telemetryContext.telemetryDataLoader.load(deploymentIds); + } + }); + }, []); + + const telemetryPoints: INonEditableGeometries[] = useMemo(() => { + const telemetryData = telemetryContext.telemetryDataLoader.data; + if (!telemetryData) { + return []; + } + + return telemetryData + .filter((telemetry) => telemetry.latitude !== undefined && telemetry.longitude !== undefined) + .map((telemetry) => { + return { + feature: { + type: 'Feature', + properties: {}, + geometry: { + type: 'Point', + coordinates: [telemetry.longitude, telemetry.latitude] as Position + } + }, + popupComponent: undefined + }; + }); + }, [telemetryContext.telemetryDataLoader.data]); const surveyObservations: INonEditableGeometries[] = useMemo(() => { const observations = observationsContext.observationsDataLoader.data?.surveyObservations; @@ -46,7 +82,7 @@ const SurveySpatialData = () => { setMapPoints(surveyObservations); break; case SurveySpatialDataSet.TELEMETRY: - setMapPoints([]); + setMapPoints(telemetryPoints); break; case SurveySpatialDataSet.MARKED_ANIMALS: setMapPoints([]); diff --git a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx index ec1a665fdc..dc9ccd39cc 100644 --- a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx +++ b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx @@ -15,6 +15,7 @@ export enum SurveySpatialDataLayout { } interface ISurveyMapToolBarProps { + //TODO: I don't want to pull the contexts into this but I will need an array of key value pairs for the options updateDataSet: (data: SurveySpatialDataSet) => void; updateLayout: (data: SurveySpatialDataLayout) => void; } From 4c149346b081b960e83f761ab782d4185a81fb0d Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 19 Jan 2024 11:37:35 -0800 Subject: [PATCH 08/85] adjusted UI --- .../features/surveys/view/SurveySpatialData.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 124a8a0dfe..62c233ffbf 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -73,7 +73,7 @@ const SurveySpatialData = () => { const [mapPoints, setMapPoints] = useState(surveyObservations); // TODO: this needs to be saved between page visits - const [layout, setLayout] = useState(SurveySpatialDataLayout.MAP); + // const [layout, setLayout] = useState(SurveySpatialDataLayout.MAP); const updateDataSet = (data: SurveySpatialDataSet) => { console.log(`DataSet: ${data}`); @@ -95,14 +95,21 @@ const SurveySpatialData = () => { const updateLayout = (data: SurveySpatialDataLayout) => { console.log(`Layout: ${data}`); - setLayout(data); + // setLayout(data); }; return ( - {layout === SurveySpatialDataLayout.MAP && ( + + + + + + + + {/* {layout === SurveySpatialDataLayout.MAP && ( @@ -125,7 +132,7 @@ const SurveySpatialData = () => { - )} + )} */} ); }; From 2f119283e0144ad1b4c1223aebc5bcc51dc0c9ec Mon Sep 17 00:00:00 2001 From: Kjartan Date: Fri, 19 Jan 2024 13:45:41 -0800 Subject: [PATCH 09/85] integrate studyarea, sample site name --- .../surveys/observations/ObservationsMap.tsx | 93 ++++++++++++++----- 1 file changed, 72 insertions(+), 21 deletions(-) diff --git a/app/src/features/surveys/observations/ObservationsMap.tsx b/app/src/features/surveys/observations/ObservationsMap.tsx index 09c95a83fc..fe791cdfb6 100644 --- a/app/src/features/surveys/observations/ObservationsMap.tsx +++ b/app/src/features/surveys/observations/ObservationsMap.tsx @@ -1,6 +1,6 @@ import { mdiRefresh } from '@mdi/js'; import Icon from '@mdi/react'; -import { IconButton } from '@mui/material'; +import { Button, IconButton } from '@mui/material'; import Box from '@mui/material/Box'; import BaseLayerControls from 'components/map/components/BaseLayerControls'; import { SetMapBounds } from 'components/map/components/Bounds'; @@ -11,16 +11,52 @@ import { ALL_OF_BC_BOUNDARY, MAP_DEFAULT_CENTER } from 'constants/spatial'; import { ObservationsContext } from 'contexts/observationsContext'; import { SurveyContext } from 'contexts/surveyContext'; import { Feature, Position } from 'geojson'; +import { useBiohubApi } from 'hooks/useBioHubApi'; import { LatLngBoundsExpression } from 'leaflet'; import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; -import { GeoJSON, LayersControl, MapContainer as LeafletMapContainer } from 'react-leaflet'; +import { GeoJSON, LayersControl, MapContainer as LeafletMapContainer, Popup } from 'react-leaflet'; +import { Link as RouterLink } from 'react-router-dom'; import { calculateUpdatedMapBounds } from 'utils/mapBoundaryUploadHelpers'; import { coloredPoint, INonEditableGeometries } from 'utils/mapUtils'; import { v4 as uuidv4 } from 'uuid'; const ObservationsMap = () => { + const biohubApi = useBiohubApi(); const observationsContext = useContext(ObservationsContext); const surveyContext = useContext(SurveyContext); + const [speciesNames, setSpeciesNames] = useState<{ id: string; label: string }[]>([]); + + const handleGetSpecies = useCallback( + async (taxonomic_ids: number[]) => { + const response = await biohubApi.taxonomy.getSpeciesFromIds(taxonomic_ids); + + setSpeciesNames(response.searchResponse); + }, + [biohubApi.taxonomy] + ); + + const speciesIds = useMemo(() => { + const observations = observationsContext.observationsDataLoader.data?.surveyObservations; + + if (!observations) { + return []; + } + + return observations.map((observation) => observation.wldtaxonomic_units_id); + }, [observationsContext.observationsDataLoader.data]); + + useEffect(() => { + handleGetSpecies(speciesIds); + }, [handleGetSpecies, speciesIds]); + + const handleCheckSpeciesName = useMemo( + (id: number) => { + const speciesName = speciesNames.find((item) => Number(item.id) === id); + + return speciesName ? speciesName.label : ''; + }, + [speciesNames] + ); const surveyObservations: INonEditableGeometries[] = useMemo(() => { const observations = observationsContext.observationsDataLoader.data?.surveyObservations; @@ -31,12 +67,10 @@ const ObservationsMap = () => { return observations .filter((observation) => observation.latitude !== undefined && observation.longitude !== undefined) - .map((observation) => { - /* + .map((observation, index) => { const link = observation.survey_observation_id ? `observations/#view-${observation.survey_observation_id}` - : 'observations' - */ + : 'observations'; return { feature: { @@ -47,40 +81,54 @@ const ObservationsMap = () => { coordinates: [observation.longitude, observation.latitude] as Position } }, - popupComponent: undefined - /*( + popupComponent: ( -
{JSON.stringify(observation)}
+
{(speciesNames.length && handleCheckSpeciesName(observation.wldtaxonomic_units_id)) || ''}
- )*/ + ) }; }); - }, [observationsContext.observationsDataLoader.data]); + }, [observationsContext.observationsDataLoader.data, handleCheckSpeciesName, speciesNames]); - const studyAreaFeatures: Feature[] = useMemo(() => { + const studyAreaFeatures: { geojson: Feature; name: string }[] = useMemo(() => { const locations = surveyContext.surveyDataLoader.data?.surveyData.locations; if (!locations) { return []; } - return locations.flatMap((item) => item.geojson); + return locations.flatMap((item) => { + return item.geojson.map((geojson) => { + return { + geojson, + name: item.name + }; + }); + }); }, [surveyContext.surveyDataLoader.data]); - const sampleSiteFeatures: Feature[] = useMemo(() => { + const sampleSiteFeatures: { geojson: Feature; name: string }[] = useMemo(() => { const sites = surveyContext.sampleSiteDataLoader.data?.sampleSites; if (!sites) { return []; } - - return sites.map((item) => item.geojson); + return sites.map((item) => { + return { + geojson: item.geojson, + name: item.name + }; + }); }, [surveyContext.sampleSiteDataLoader.data]); const getDefaultMapBounds = useCallback((): LatLngBoundsExpression | undefined => { const features = surveyObservations.map((observation) => observation.feature); - return calculateUpdatedMapBounds([...features, ...studyAreaFeatures, ...sampleSiteFeatures]); + + const studyAreaFeaturesGeoJSON = studyAreaFeatures.map((item) => item.geojson); + const sampleSiteFeaturesGeoJSON = sampleSiteFeatures.map((item) => item.geojson); + + return calculateUpdatedMapBounds([...features, ...studyAreaFeaturesGeoJSON, ...sampleSiteFeaturesGeoJSON]); }, [surveyObservations, studyAreaFeatures, sampleSiteFeatures]); // set default bounds to encompass all of BC @@ -135,14 +183,17 @@ const ObservationsMap = () => { layers={[ { layerName: 'Study Area', - features: studyAreaFeatures.map((feature) => ({ geoJSON: feature, tooltip: Study Area })) + features: studyAreaFeatures.map((feature) => ({ + geoJSON: feature.geojson, + tooltip: Study Area: {feature.name} + })) }, { layerName: 'Sample Sites', layerColors: { color: '#1f7dff', fillColor: '#1f7dff' }, features: sampleSiteFeatures.map((feature) => ({ - geoJSON: feature, - tooltip: Sample Site + geoJSON: feature.geojson, + tooltip: Sampling Site: {feature.name} })) } ]} From 987fa70ee0fa7ea12ef5685fc7f363dc8416e950 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 19 Jan 2024 13:50:46 -0800 Subject: [PATCH 10/85] added basic table data --- .../surveys/view/SurveySpatialData.tsx | 11 +++- .../surveys/view/SurveySpatialDataTable.tsx | 53 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 app/src/features/surveys/view/SurveySpatialDataTable.tsx diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 62c233ffbf..770a6a23a3 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -5,15 +5,16 @@ import { TelemetryDataContext } from 'contexts/telemetryDataContext'; import { Position } from 'geojson'; import { useContext, useEffect, useMemo, useState } from 'react'; import { INonEditableGeometries } from 'utils/mapUtils'; -import NoSurveySectionData from '../components/NoSurveySectionData'; import SurveyMapToolBar, { SurveySpatialDataLayout, SurveySpatialDataSet } from './components/SurveyMapToolBar'; import SurveyMap from './SurveyMap'; +import SurveySpatialDataTable from './SurveySpatialDataTable'; const SurveySpatialData = () => { const observationsContext = useContext(ObservationsContext); const telemetryContext = useContext(TelemetryDataContext); const surveyContext = useContext(SurveyContext); + //TODO: is this the cleanest way to do this? because this feels gross useEffect(() => { surveyContext.deploymentDataLoader.load(surveyContext.projectId, surveyContext.surveyId).then(() => { if (surveyContext.deploymentDataLoader.data) { @@ -71,6 +72,9 @@ const SurveySpatialData = () => { }, [observationsContext.observationsDataLoader.data]); const [mapPoints, setMapPoints] = useState(surveyObservations); + const [tableData, setTableData] = useState( + observationsContext.observationsDataLoader.data?.surveyObservations || [] + ); // TODO: this needs to be saved between page visits // const [layout, setLayout] = useState(SurveySpatialDataLayout.MAP); @@ -80,9 +84,11 @@ const SurveySpatialData = () => { switch (data) { case SurveySpatialDataSet.OBSERVATIONS: setMapPoints(surveyObservations); + setTableData(observationsContext.observationsDataLoader.data?.surveyObservations || []); break; case SurveySpatialDataSet.TELEMETRY: setMapPoints(telemetryPoints); + setTableData(telemetryContext.telemetryDataLoader.data || []); break; case SurveySpatialDataSet.MARKED_ANIMALS: setMapPoints([]); @@ -106,7 +112,8 @@ const SurveySpatialData = () => {
- + + {/* */} {/* {layout === SurveySpatialDataLayout.MAP && ( diff --git a/app/src/features/surveys/view/SurveySpatialDataTable.tsx b/app/src/features/surveys/view/SurveySpatialDataTable.tsx new file mode 100644 index 0000000000..e48ea35312 --- /dev/null +++ b/app/src/features/surveys/view/SurveySpatialDataTable.tsx @@ -0,0 +1,53 @@ +import Paper from '@mui/material/Paper'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import { v4 as uuid } from 'uuid'; + +interface ISurveySpatialDataTableProps { + tableData: any[]; +} + +const SurveySpatialDataTable = (props: ISurveySpatialDataTableProps) => { + const buildTablesHeaders = (data: any) => { + return Object.keys(data); + }; + + const buildTableRows = (data: any[]) => { + return data.map((item) => { + return Object.values(item); + }); + }; + + return ( + <> + + + + + {buildTablesHeaders(props.tableData[0] || []).map((header) => ( + + {header} + + ))} + + + + {buildTableRows(props.tableData).map((items) => ( + + {items.map((value: any) => ( + {value} + ))} + + ))} + +
+
+ + ); +}; + +export default SurveySpatialDataTable; From 2b7b39b4cbf4d0ac21fda1619a72d76dba85eb12 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 19 Jan 2024 15:13:47 -0800 Subject: [PATCH 11/85] basic table rows --- .../surveys/view/SurveySpatialData.tsx | 36 +++++++++++++++---- .../surveys/view/SurveySpatialDataTable.tsx | 19 +++------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 770a6a23a3..b29cf4a6e7 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -2,6 +2,7 @@ import { Box, Paper } from '@mui/material'; import { ObservationsContext } from 'contexts/observationsContext'; import { SurveyContext } from 'contexts/surveyContext'; import { TelemetryDataContext } from 'contexts/telemetryDataContext'; +import dayjs from 'dayjs'; import { Position } from 'geojson'; import { useContext, useEffect, useMemo, useState } from 'react'; import { INonEditableGeometries } from 'utils/mapUtils'; @@ -72,9 +73,8 @@ const SurveySpatialData = () => { }, [observationsContext.observationsDataLoader.data]); const [mapPoints, setMapPoints] = useState(surveyObservations); - const [tableData, setTableData] = useState( - observationsContext.observationsDataLoader.data?.surveyObservations || [] - ); + const [tableHeaders, setTableHeaders] = useState([]); + const [tableRows, setTableRows] = useState([]); // TODO: this needs to be saved between page visits // const [layout, setLayout] = useState(SurveySpatialDataLayout.MAP); @@ -84,17 +84,41 @@ const SurveySpatialData = () => { switch (data) { case SurveySpatialDataSet.OBSERVATIONS: setMapPoints(surveyObservations); - setTableData(observationsContext.observationsDataLoader.data?.surveyObservations || []); + setTableHeaders(['Species', 'Count', 'Date', 'Time', 'Lat', 'Long']); + setTableRows( + observationsContext.observationsDataLoader.data?.surveyObservations.map((item) => [ + `Moose...`, + `${item.count}`, + `${dayjs(item.observation_date).format('YYYY-MM-DD')}`, + `${dayjs(item.observation_date).format('HH:mm:ss')}`, + `${item.latitude}`, + `${item.longitude}` + ]) || [] + ); break; case SurveySpatialDataSet.TELEMETRY: setMapPoints(telemetryPoints); - setTableData(telemetryContext.telemetryDataLoader.data || []); + setTableHeaders(['Alias', 'Device ID', 'Date', 'Time', 'Lat', 'Long']); + setTableRows( + telemetryContext.telemetryDataLoader.data?.map((item) => [ + `${item.deployment_id}`, + `${item.deployment_id}`, + `${dayjs(item.acquisition_date).format('YYYY-MM-DD')}`, + `${dayjs(item.acquisition_date).format('HH:mm:ss')}`, + `${item.latitude}`, + `${item.longitude}` + ]) || [] + ); break; case SurveySpatialDataSet.MARKED_ANIMALS: setMapPoints([]); + setTableHeaders(['Alias', 'Event', 'Date', 'Time', 'Lat', 'Long']); + setTableRows([]); break; default: setMapPoints([]); + setTableHeaders([]); + setTableRows([]); break; } }; @@ -112,7 +136,7 @@ const SurveySpatialData = () => {
- + {/* */} diff --git a/app/src/features/surveys/view/SurveySpatialDataTable.tsx b/app/src/features/surveys/view/SurveySpatialDataTable.tsx index e48ea35312..f2dda14fe0 100644 --- a/app/src/features/surveys/view/SurveySpatialDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialDataTable.tsx @@ -8,27 +8,18 @@ import TableRow from '@mui/material/TableRow'; import { v4 as uuid } from 'uuid'; interface ISurveySpatialDataTableProps { - tableData: any[]; + tableHeaders: string[]; + tableRows: string[][]; } const SurveySpatialDataTable = (props: ISurveySpatialDataTableProps) => { - const buildTablesHeaders = (data: any) => { - return Object.keys(data); - }; - - const buildTableRows = (data: any[]) => { - return data.map((item) => { - return Object.values(item); - }); - }; - return ( <> - {buildTablesHeaders(props.tableData[0] || []).map((header) => ( + {props.tableHeaders.map((header) => ( {header} @@ -36,9 +27,9 @@ const SurveySpatialDataTable = (props: ISurveySpatialDataTableProps) => { - {buildTableRows(props.tableData).map((items) => ( + {props.tableRows.map((items: string[]) => ( - {items.map((value: any) => ( + {items.map((value: string) => ( {value} ))} From 0206c1e44c5a96110e733062e0796dd3a2d38e18 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 19 Jan 2024 15:38:29 -0800 Subject: [PATCH 12/85] cleaned up telemetry data fetch --- .../features/surveys/view/SurveySpatialData.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index b29cf4a6e7..cfcfb3912c 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -14,16 +14,12 @@ const SurveySpatialData = () => { const observationsContext = useContext(ObservationsContext); const telemetryContext = useContext(TelemetryDataContext); const surveyContext = useContext(SurveyContext); - - //TODO: is this the cleanest way to do this? because this feels gross useEffect(() => { - surveyContext.deploymentDataLoader.load(surveyContext.projectId, surveyContext.surveyId).then(() => { - if (surveyContext.deploymentDataLoader.data) { - const deploymentIds = surveyContext.deploymentDataLoader.data.map((item) => item.deployment_id); - telemetryContext.telemetryDataLoader.load(deploymentIds); - } - }); - }, []); + if (surveyContext.deploymentDataLoader.data) { + const deploymentIds = surveyContext.deploymentDataLoader.data.map((item) => item.deployment_id); + telemetryContext.telemetryDataLoader.load(deploymentIds); + } + }, [surveyContext.deploymentDataLoader.data]); const telemetryPoints: INonEditableGeometries[] = useMemo(() => { const telemetryData = telemetryContext.telemetryDataLoader.data; From 9c09ce731c133f6f3750127310b96b5a46ff2742 Mon Sep 17 00:00:00 2001 From: jeznorth Date: Mon, 22 Jan 2024 11:13:15 -0800 Subject: [PATCH 13/85] UI Updates --- app/src/features/surveys/view/SurveyPage.tsx | 9 ++- .../surveys/view/SurveySpatialData.tsx | 2 +- .../surveys/view/SurveySpatialDataTable.tsx | 3 +- .../view/components/SurveyMapToolBar.tsx | 71 +++++++++++++++---- 4 files changed, 64 insertions(+), 21 deletions(-) diff --git a/app/src/features/surveys/view/SurveyPage.tsx b/app/src/features/surveys/view/SurveyPage.tsx index 00a5f5a99f..f02cb9503b 100644 --- a/app/src/features/surveys/view/SurveyPage.tsx +++ b/app/src/features/surveys/view/SurveyPage.tsx @@ -44,11 +44,10 @@ const SurveyPage: React.FC = () => { <> - - - - - + + + + {/* diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index cfcfb3912c..9c3e6d380e 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -131,7 +131,7 @@ const SurveySpatialData = () => { - + {/* */} diff --git a/app/src/features/surveys/view/SurveySpatialDataTable.tsx b/app/src/features/surveys/view/SurveySpatialDataTable.tsx index f2dda14fe0..ac7e111327 100644 --- a/app/src/features/surveys/view/SurveySpatialDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialDataTable.tsx @@ -1,4 +1,3 @@ -import Paper from '@mui/material/Paper'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableCell from '@mui/material/TableCell'; @@ -15,7 +14,7 @@ interface ISurveySpatialDataTableProps { const SurveySpatialDataTable = (props: ISurveySpatialDataTableProps) => { return ( <> - +
diff --git a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx index dc9ccd39cc..9aae17ee07 100644 --- a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx +++ b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx @@ -1,4 +1,6 @@ -import { Box, Menu, MenuItem, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material'; +import { mdiChevronDown } from '@mdi/js'; +import Icon from '@mdi/react'; +import { Box, Menu, MenuItem, ToggleButton, ToggleButtonGroup, Toolbar, Typography } from '@mui/material'; import Button from '@mui/material/Button'; import { useState } from 'react'; @@ -45,7 +47,19 @@ const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { return ( <> - + Observations Telemetry Marked Animals @@ -56,25 +70,56 @@ const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { display: 'flex', flexDirection: 'column' }}> - - - Survey Data + + + SURVEY DATA - - - - - Observations - Telemetry - Marked Animals + + + + Observations + Telemetry + Animal Events - + + MAP TABLE SPLIT + From 1dac43c04fde8afd2bc3d6205fef7c802feaa104 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Mon, 22 Jan 2024 11:23:13 -0800 Subject: [PATCH 14/85] merged ui changes --- app/src/contexts/taxonomyContext.tsx | 1 - app/src/features/surveys/view/SurveyPage.tsx | 6 ++-- .../surveys/view/SurveySpatialData.tsx | 28 +++++++++++++++---- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/app/src/contexts/taxonomyContext.tsx b/app/src/contexts/taxonomyContext.tsx index 235ed49a42..18d9ca8119 100644 --- a/app/src/contexts/taxonomyContext.tsx +++ b/app/src/contexts/taxonomyContext.tsx @@ -41,7 +41,6 @@ export const TaxonomyContextProvider = (props: PropsWithChildren) => { async (ids: number[]) => { setIsLoading(true); ids.forEach((id) => _dispatchedIds.current.add(id)); - await biohubApi.taxonomy .getSpeciesFromIds(ids) .then((result) => { diff --git a/app/src/features/surveys/view/SurveyPage.tsx b/app/src/features/surveys/view/SurveyPage.tsx index f02cb9503b..eead5e446f 100644 --- a/app/src/features/surveys/view/SurveyPage.tsx +++ b/app/src/features/surveys/view/SurveyPage.tsx @@ -8,6 +8,7 @@ import { SYSTEM_ROLE } from 'constants/roles'; import { CodesContext } from 'contexts/codesContext'; import { ObservationsContext } from 'contexts/observationsContext'; import { SurveyContext } from 'contexts/surveyContext'; +import { TaxonomyContextProvider } from 'contexts/taxonomyContext'; import SurveyDetails from 'features/surveys/view/SurveyDetails'; import React, { useContext, useEffect } from 'react'; import SurveyStudyArea from './components/SurveyStudyArea'; @@ -44,9 +45,10 @@ const SurveyPage: React.FC = () => { <> - - + + + {/* diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 9c3e6d380e..1dc67d3205 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -1,6 +1,7 @@ import { Box, Paper } from '@mui/material'; import { ObservationsContext } from 'contexts/observationsContext'; import { SurveyContext } from 'contexts/surveyContext'; +import { TaxonomyContext } from 'contexts/taxonomyContext'; import { TelemetryDataContext } from 'contexts/telemetryDataContext'; import dayjs from 'dayjs'; import { Position } from 'geojson'; @@ -13,7 +14,10 @@ import SurveySpatialDataTable from './SurveySpatialDataTable'; const SurveySpatialData = () => { const observationsContext = useContext(ObservationsContext); const telemetryContext = useContext(TelemetryDataContext); + const taxonomyContext = useContext(TaxonomyContext); const surveyContext = useContext(SurveyContext); + + // useEffect(() => { if (surveyContext.deploymentDataLoader.data) { const deploymentIds = surveyContext.deploymentDataLoader.data.map((item) => item.deployment_id); @@ -21,6 +25,21 @@ const SurveySpatialData = () => { } }, [surveyContext.deploymentDataLoader.data]); + // Fetch/ Cache all taxonomic data for the observations + useEffect(() => { + const cacheTaxonomicData = async () => { + if (observationsContext.observationsDataLoader.data) { + const taxonomicIds = observationsContext.observationsDataLoader.data.surveyObservations.map( + (item) => item.wldtaxonomic_units_id + ); + console.log(`Taxonomic IDs: ${taxonomicIds}`); + await taxonomyContext.cacheSpeciesTaxonomyByIds(taxonomicIds); + } + }; + + cacheTaxonomicData(); + }, [observationsContext.observationsDataLoader.data]); + const telemetryPoints: INonEditableGeometries[] = useMemo(() => { const telemetryData = telemetryContext.telemetryDataLoader.data; if (!telemetryData) { @@ -44,7 +63,7 @@ const SurveySpatialData = () => { }); }, [telemetryContext.telemetryDataLoader.data]); - const surveyObservations: INonEditableGeometries[] = useMemo(() => { + const observationPoints: INonEditableGeometries[] = useMemo(() => { const observations = observationsContext.observationsDataLoader.data?.surveyObservations; if (!observations) { @@ -68,7 +87,7 @@ const SurveySpatialData = () => { }); }, [observationsContext.observationsDataLoader.data]); - const [mapPoints, setMapPoints] = useState(surveyObservations); + const [mapPoints, setMapPoints] = useState(observationPoints); const [tableHeaders, setTableHeaders] = useState([]); const [tableRows, setTableRows] = useState([]); @@ -76,14 +95,13 @@ const SurveySpatialData = () => { // const [layout, setLayout] = useState(SurveySpatialDataLayout.MAP); const updateDataSet = (data: SurveySpatialDataSet) => { - console.log(`DataSet: ${data}`); switch (data) { case SurveySpatialDataSet.OBSERVATIONS: - setMapPoints(surveyObservations); + setMapPoints(observationPoints); setTableHeaders(['Species', 'Count', 'Date', 'Time', 'Lat', 'Long']); setTableRows( observationsContext.observationsDataLoader.data?.surveyObservations.map((item) => [ - `Moose...`, + `${taxonomyContext.getCachedSpeciesTaxonomyById(item.wldtaxonomic_units_id)?.label}`, `${item.count}`, `${dayjs(item.observation_date).format('YYYY-MM-DD')}`, `${dayjs(item.observation_date).format('HH:mm:ss')}`, From 72bfe66a5d1495b4ee2c1332b137c445b1991760 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Mon, 22 Jan 2024 11:27:29 -0800 Subject: [PATCH 15/85] clean up --- app/src/features/surveys/view/SurveySpatialData.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 1dc67d3205..6b37e5a732 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -32,7 +32,6 @@ const SurveySpatialData = () => { const taxonomicIds = observationsContext.observationsDataLoader.data.surveyObservations.map( (item) => item.wldtaxonomic_units_id ); - console.log(`Taxonomic IDs: ${taxonomicIds}`); await taxonomyContext.cacheSpeciesTaxonomyByIds(taxonomicIds); } }; From 72a62ca9efcfa6d9b0bbe2ef862b29fbe48e5351 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Mon, 22 Jan 2024 11:44:33 -0800 Subject: [PATCH 16/85] renamed header --- app/src/features/surveys/view/SurveySpatialData.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 6b37e5a732..533d0b6f6f 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -111,7 +111,7 @@ const SurveySpatialData = () => { break; case SurveySpatialDataSet.TELEMETRY: setMapPoints(telemetryPoints); - setTableHeaders(['Alias', 'Device ID', 'Date', 'Time', 'Lat', 'Long']); + setTableHeaders(['Deployment ID', 'Device ID', 'Date', 'Time', 'Lat', 'Long']); setTableRows( telemetryContext.telemetryDataLoader.data?.map((item) => [ `${item.deployment_id}`, From 07123ab6b733a99d06e492569ef83e050c195879 Mon Sep 17 00:00:00 2001 From: jeznorth Date: Mon, 22 Jan 2024 11:48:41 -0800 Subject: [PATCH 17/85] UI Updates --- .../features/surveys/view/SurveyAnimals.tsx | 9 +++-- app/src/features/surveys/view/SurveyPage.tsx | 35 ++++++++--------- .../surveys/view/SurveySpatialData.tsx | 4 +- .../view/components/SurveyMapToolBar.tsx | 39 ++++++++++--------- 4 files changed, 45 insertions(+), 42 deletions(-) diff --git a/app/src/features/surveys/view/SurveyAnimals.tsx b/app/src/features/surveys/view/SurveyAnimals.tsx index ecd6ec082e..3c1c8755aa 100644 --- a/app/src/features/surveys/view/SurveyAnimals.tsx +++ b/app/src/features/surveys/view/SurveyAnimals.tsx @@ -122,12 +122,15 @@ const SurveyAnimals: React.FC = () => { title="Manage Marked and Known Animals" color="primary" variant="contained" - startIcon={}> + startIcon={} + sx={{ + m: -1 + }}> Manage Animals - - + + {critterData?.length ? ( @@ -45,19 +47,26 @@ const SurveyPage: React.FC = () => { - - - + + + + - {/* - - + + - */} + + + + + + + + {/* - + */} @@ -67,16 +76,6 @@ const SurveyPage: React.FC = () => { */} - - - - - - - - - - diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 9c3e6d380e..f5b5de1aed 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -125,13 +125,13 @@ const SurveySpatialData = () => { }; return ( - + - + {/* */} diff --git a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx index 9aae17ee07..bcf233ddae 100644 --- a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx +++ b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx @@ -1,6 +1,6 @@ -import { mdiChevronDown } from '@mdi/js'; +import { mdiBroadcast, mdiChevronDown, mdiEye } from '@mdi/js'; import Icon from '@mdi/react'; -import { Box, Menu, MenuItem, ToggleButton, ToggleButtonGroup, Toolbar, Typography } from '@mui/material'; +import { Box, Divider, ListItemIcon, ListItemText, Menu, MenuItem, ToggleButton, ToggleButtonGroup, Toolbar, Typography } from '@mui/material'; import Button from '@mui/material/Button'; import { useState } from 'react'; @@ -60,9 +60,14 @@ const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { horizontal: 'right', }} > - Observations - Telemetry - Marked Animals + + + Observations + + + + Telemetry + { }}> - SURVEY DATA + Survey Data - + + { px: 1.25, border: 'none', borderRadius: '4px !important', + fontSize: '0.875rem', fontWeight: 700, - letterSpacing: '0.01rem', - '&.Mui-selected': { - color: '#fff', - backgroundColor: 'info.main' - }, - '&.Mui-selected:hover': { - color: '#fff', - backgroundColor: 'info.main' - } + letterSpacing: '0.02rem' } }} > - Observations - Telemetry - Animal Events + } value={SurveySpatialDataSet.OBSERVATIONS}>Observations + } value={SurveySpatialDataSet.TELEMETRY}>Telemetry From d245310fe1146365cb237212bf9d86cc0f33e6de Mon Sep 17 00:00:00 2001 From: jeznorth Date: Mon, 22 Jan 2024 11:50:51 -0800 Subject: [PATCH 18/85] UI Updates --- app/src/features/surveys/view/SurveyPage.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/features/surveys/view/SurveyPage.tsx b/app/src/features/surveys/view/SurveyPage.tsx index abeb9f6a19..883eef6ae9 100644 --- a/app/src/features/surveys/view/SurveyPage.tsx +++ b/app/src/features/surveys/view/SurveyPage.tsx @@ -47,11 +47,14 @@ const SurveyPage: React.FC = () => { <> - - - - - + + + + + + + + From dffa8b6a782e2a33eff6cdd05d59b6f17c883ea0 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Mon, 22 Jan 2024 11:52:30 -0800 Subject: [PATCH 19/85] merged in changes --- app/src/features/surveys/view/SurveySpatialData.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 2e2e6cb5b7..4cd63ffced 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -7,6 +7,7 @@ import dayjs from 'dayjs'; import { Position } from 'geojson'; import { useContext, useEffect, useMemo, useState } from 'react'; import { INonEditableGeometries } from 'utils/mapUtils'; +import NoSurveySectionData from '../components/NoSurveySectionData'; import SurveyMapToolBar, { SurveySpatialDataLayout, SurveySpatialDataSet } from './components/SurveyMapToolBar'; import SurveyMap from './SurveyMap'; import SurveySpatialDataTable from './SurveySpatialDataTable'; @@ -149,8 +150,11 @@ const SurveySpatialData = () => { - - {/* */} + {tableRows.length > 0 ? ( + + ) : ( + + )} {/* {layout === SurveySpatialDataLayout.MAP && ( From e89c1dbf7a83d170a0e2dd1eb646fc8b029836f2 Mon Sep 17 00:00:00 2001 From: jeznorth Date: Mon, 22 Jan 2024 13:55:12 -0800 Subject: [PATCH 20/85] UI Updates --- app/src/features/surveys/view/SurveyMap.tsx | 36 +++++++++++++++++++ app/src/features/surveys/view/SurveyPage.tsx | 8 ++--- .../view/components/SurveyBaseHeader.tsx | 2 +- .../view/components/SurveyMapToolBar.tsx | 7 ++-- 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/app/src/features/surveys/view/SurveyMap.tsx b/app/src/features/surveys/view/SurveyMap.tsx index 31fe5d9bbb..0da2eedbb6 100644 --- a/app/src/features/surveys/view/SurveyMap.tsx +++ b/app/src/features/surveys/view/SurveyMap.tsx @@ -1,3 +1,7 @@ +import { mdiMapSearchOutline } from '@mdi/js'; +import Icon from '@mdi/react'; +import { Box, Skeleton } from '@mui/material'; +import grey from '@mui/material/colors/grey'; import BaseLayerControls from 'components/map/components/BaseLayerControls'; import { SetMapBounds } from 'components/map/components/Bounds'; import FullScreenScrollingEventHandler from 'components/map/components/FullScreenScrollingEventHandler'; @@ -18,6 +22,38 @@ const SurveyMap = (props: ISurveyMapProps) => { const [bounds] = useState(calculateUpdatedMapBounds([ALL_OF_BC_BOUNDARY])); return ( <> + + + + + + { - - - - - + + + diff --git a/app/src/features/surveys/view/components/SurveyBaseHeader.tsx b/app/src/features/surveys/view/components/SurveyBaseHeader.tsx index 9d01889074..08882510e5 100644 --- a/app/src/features/surveys/view/components/SurveyBaseHeader.tsx +++ b/app/src/features/surveys/view/components/SurveyBaseHeader.tsx @@ -25,7 +25,7 @@ const SurveyBaseHeader = (props: ISurveyHeader) => { square={true} id="pageTitle" sx={{ - position: 'sticky', + position: {sm: 'relative', xl: 'sticky'}, top: 0, zIndex: 1002, borderBottom: '1px solid' + grey[300] diff --git a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx index bcf233ddae..e8b62dc6f6 100644 --- a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx +++ b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx @@ -1,4 +1,4 @@ -import { mdiBroadcast, mdiChevronDown, mdiEye } from '@mdi/js'; +import { mdiBroadcast, mdiChevronDown, mdiCog, mdiEye } from '@mdi/js'; import Icon from '@mdi/react'; import { Box, Divider, ListItemIcon, ListItemText, Menu, MenuItem, ToggleButton, ToggleButtonGroup, Toolbar, Typography } from '@mui/material'; import Button from '@mui/material/Button'; @@ -84,6 +84,9 @@ const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { color="primary" aria-label="Manage Survey Data" onClick={handleMenuClick} + startIcon={ + + } endIcon={ } @@ -102,7 +105,7 @@ const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { gap: 1, '& Button': { py: 0.25, - px: 1.25, + px: 1.5, border: 'none', borderRadius: '4px !important', fontSize: '0.875rem', From 45e46f7aecfa9c4d496369a188e5c228a5bd9b61 Mon Sep 17 00:00:00 2001 From: jeznorth Date: Mon, 22 Jan 2024 13:57:27 -0800 Subject: [PATCH 21/85] Fix positioning --- app/src/features/surveys/view/SurveySpatialData.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 4cd63ffced..37ead64861 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -146,7 +146,7 @@ const SurveySpatialData = () => { - + From 0abc3f6b558007a2f045041da68fd6b25d4d3e98 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Mon, 22 Jan 2024 14:15:00 -0800 Subject: [PATCH 22/85] telemetry data sorted --- .../telemetry/ManualTelemetryTable.tsx | 1 + .../surveys/view/SurveySpatialData.tsx | 50 +++++++++++++------ 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/app/src/features/surveys/telemetry/ManualTelemetryTable.tsx b/app/src/features/surveys/telemetry/ManualTelemetryTable.tsx index 36b3507d1e..ff1e60283b 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryTable.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryTable.tsx @@ -41,6 +41,7 @@ const ManualTelemetryTable = (props: IManualTelemetryTableProps) => { }); return data; }, [surveyContext.critterDataLoader.data, surveyContext.deploymentDataLoader.data]); + const { _muiDataGridApiRef } = telemetryTableContext; const hasError = useCallback( (params: GridCellParams): boolean => { diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 37ead64861..6b09049850 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -8,6 +8,7 @@ import { Position } from 'geojson'; import { useContext, useEffect, useMemo, useState } from 'react'; import { INonEditableGeometries } from 'utils/mapUtils'; import NoSurveySectionData from '../components/NoSurveySectionData'; +import { ICritterDeployment } from '../telemetry/ManualTelemetryList'; import SurveyMapToolBar, { SurveySpatialDataLayout, SurveySpatialDataSet } from './components/SurveyMapToolBar'; import SurveyMap from './SurveyMap'; import SurveySpatialDataTable from './SurveySpatialDataTable'; @@ -18,7 +19,11 @@ const SurveySpatialData = () => { const taxonomyContext = useContext(TaxonomyContext); const surveyContext = useContext(SurveyContext); - // + useEffect(() => { + surveyContext.deploymentDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); + surveyContext.critterDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); + }, []); + useEffect(() => { if (surveyContext.deploymentDataLoader.data) { const deploymentIds = surveyContext.deploymentDataLoader.data.map((item) => item.deployment_id); @@ -30,9 +35,12 @@ const SurveySpatialData = () => { useEffect(() => { const cacheTaxonomicData = async () => { if (observationsContext.observationsDataLoader.data) { - const taxonomicIds = observationsContext.observationsDataLoader.data.surveyObservations.map( - (item) => item.wldtaxonomic_units_id - ); + // fetch all unique wldtaxonomic_units_id's from observations to find taxonomic names + const taxonomicIds = [ + ...new Set( + observationsContext.observationsDataLoader.data.surveyObservations.map((item) => item.wldtaxonomic_units_id) + ) + ]; await taxonomyContext.cacheSpeciesTaxonomyByIds(taxonomicIds); } }; @@ -40,6 +48,20 @@ const SurveySpatialData = () => { cacheTaxonomicData(); }, [observationsContext.observationsDataLoader.data]); + const flattenedCritterDeployments: ICritterDeployment[] = useMemo(() => { + const data: ICritterDeployment[] = []; + // combine all critter and deployments into a flat list + surveyContext.deploymentDataLoader.data?.forEach((deployment) => { + const critter = surveyContext.critterDataLoader.data?.find( + (critter) => critter.critter_id === deployment.critter_id + ); + if (critter) { + data.push({ critter, deployment }); + } + }); + return data; + }, [surveyContext.critterDataLoader.data, surveyContext.deploymentDataLoader.data]); + const telemetryPoints: INonEditableGeometries[] = useMemo(() => { const telemetryData = telemetryContext.telemetryDataLoader.data; if (!telemetryData) { @@ -112,21 +134,21 @@ const SurveySpatialData = () => { break; case SurveySpatialDataSet.TELEMETRY: setMapPoints(telemetryPoints); - setTableHeaders(['Deployment ID', 'Device ID', 'Date', 'Time', 'Lat', 'Long']); + setTableHeaders(['Alias', 'Device ID', 'Start', 'End']); setTableRows( - telemetryContext.telemetryDataLoader.data?.map((item) => [ - `${item.deployment_id}`, - `${item.deployment_id}`, - `${dayjs(item.acquisition_date).format('YYYY-MM-DD')}`, - `${dayjs(item.acquisition_date).format('HH:mm:ss')}`, - `${item.latitude}`, - `${item.longitude}` - ]) || [] + flattenedCritterDeployments.map((item) => { + return [ + `${item.critter.animal_id}`, + `${item.deployment.device_id}`, + `${dayjs(item.deployment.attachment_start).format('YYYY-MM-DD')}`, + `${dayjs(item.deployment.attachment_end).format('YYYY-MM-DD')}` + ]; + }) ); break; case SurveySpatialDataSet.MARKED_ANIMALS: setMapPoints([]); - setTableHeaders(['Alias', 'Event', 'Date', 'Time', 'Lat', 'Long']); + setTableHeaders(['Deployment ID', 'Device ID', 'Date', 'Time', 'Lat', 'Long']); setTableRows([]); break; default: From 2a2dbf082dfd6859a0bb42adee8c8870c8c5acfd Mon Sep 17 00:00:00 2001 From: Kjartan Date: Mon, 22 Jan 2024 14:23:38 -0800 Subject: [PATCH 23/85] fix observation species display --- app/src/features/surveys/observations/ObservationsMap.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/features/surveys/observations/ObservationsMap.tsx b/app/src/features/surveys/observations/ObservationsMap.tsx index fe791cdfb6..c450e29f7f 100644 --- a/app/src/features/surveys/observations/ObservationsMap.tsx +++ b/app/src/features/surveys/observations/ObservationsMap.tsx @@ -49,7 +49,7 @@ const ObservationsMap = () => { handleGetSpecies(speciesIds); }, [handleGetSpecies, speciesIds]); - const handleCheckSpeciesName = useMemo( + const handleCheckSpeciesName = useCallback( (id: number) => { const speciesName = speciesNames.find((item) => Number(item.id) === id); @@ -67,7 +67,7 @@ const ObservationsMap = () => { return observations .filter((observation) => observation.latitude !== undefined && observation.longitude !== undefined) - .map((observation, index) => { + .map((observation) => { const link = observation.survey_observation_id ? `observations/#view-${observation.survey_observation_id}` : 'observations'; From bb94383de4e408001eae51b40d19100768042220 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Mon, 22 Jan 2024 15:46:01 -0800 Subject: [PATCH 24/85] observation data displaying --- .../surveys/view/SurveySpatialData.tsx | 64 ++++++++++++++++--- 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 6b09049850..0dd1ee53ee 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -1,4 +1,5 @@ import { Box, Paper } from '@mui/material'; +import { CodesContext } from 'contexts/codesContext'; import { ObservationsContext } from 'contexts/observationsContext'; import { SurveyContext } from 'contexts/surveyContext'; import { TaxonomyContext } from 'contexts/taxonomyContext'; @@ -7,6 +8,7 @@ import dayjs from 'dayjs'; import { Position } from 'geojson'; import { useContext, useEffect, useMemo, useState } from 'react'; import { INonEditableGeometries } from 'utils/mapUtils'; +import { getCodesName } from 'utils/Utils'; import NoSurveySectionData from '../components/NoSurveySectionData'; import { ICritterDeployment } from '../telemetry/ManualTelemetryList'; import SurveyMapToolBar, { SurveySpatialDataLayout, SurveySpatialDataSet } from './components/SurveyMapToolBar'; @@ -18,10 +20,13 @@ const SurveySpatialData = () => { const telemetryContext = useContext(TelemetryDataContext); const taxonomyContext = useContext(TaxonomyContext); const surveyContext = useContext(SurveyContext); + const codesContext = useContext(CodesContext); useEffect(() => { + codesContext.codesDataLoader.load(); surveyContext.deploymentDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); surveyContext.critterDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); + surveyContext.sampleSiteDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); }, []); useEffect(() => { @@ -62,6 +67,22 @@ const SurveySpatialData = () => { return data; }, [surveyContext.critterDataLoader.data, surveyContext.deploymentDataLoader.data]); + const sampleSites = useMemo(() => { + return surveyContext.sampleSiteDataLoader.data?.sampleSites ?? []; + }, [surveyContext.sampleSiteDataLoader.data]); + + const sampleMethods = useMemo(() => { + return surveyContext.sampleSiteDataLoader.data?.sampleSites.flatMap((item) => item.sample_methods) || []; + }, [surveyContext.sampleSiteDataLoader.data]); + + const samplePeriods = useMemo(() => { + return ( + surveyContext.sampleSiteDataLoader.data?.sampleSites.flatMap((item) => + item.sample_methods?.flatMap((method) => method.sample_periods) + ) || [] + ); + }, [surveyContext.sampleSiteDataLoader.data]); + const telemetryPoints: INonEditableGeometries[] = useMemo(() => { const telemetryData = telemetryContext.telemetryDataLoader.data; if (!telemetryData) { @@ -120,16 +141,41 @@ const SurveySpatialData = () => { switch (data) { case SurveySpatialDataSet.OBSERVATIONS: setMapPoints(observationPoints); - setTableHeaders(['Species', 'Count', 'Date', 'Time', 'Lat', 'Long']); + setTableHeaders([ + 'Species', + 'Count', + 'Sample Site', + 'Sample Method', + 'Sample Period', + 'Date', + 'Time', + 'Lat', + 'Long' + ]); setTableRows( - observationsContext.observationsDataLoader.data?.surveyObservations.map((item) => [ - `${taxonomyContext.getCachedSpeciesTaxonomyById(item.wldtaxonomic_units_id)?.label}`, - `${item.count}`, - `${dayjs(item.observation_date).format('YYYY-MM-DD')}`, - `${dayjs(item.observation_date).format('HH:mm:ss')}`, - `${item.latitude}`, - `${item.longitude}` - ]) || [] + observationsContext.observationsDataLoader.data?.surveyObservations.map((item) => { + const siteName = sampleSites.find( + (site) => site.survey_sample_site_id === item.survey_sample_site_id + )?.name; + const method_id = sampleMethods.find( + (method) => method?.survey_sample_method_id === item.survey_sample_method_id + )?.method_lookup_id; + const period = samplePeriods.find( + (period) => period?.survey_sample_period_id === item.survey_sample_period_id + ); + + return [ + `${taxonomyContext.getCachedSpeciesTaxonomyById(item.wldtaxonomic_units_id)?.label}`, + `${item.count}`, + `${siteName}`, + `${method_id ? getCodesName(codesContext.codesDataLoader.data, 'sample_methods', method_id) : ''}`, + `${period?.start_date} ${period?.end_date}`, + `${dayjs(item.observation_date).format('YYYY-MM-DD')}`, + `${dayjs(item.observation_date).format('HH:mm:ss')}`, + `${item.latitude}`, + `${item.longitude}` + ]; + }) || [] ); break; case SurveySpatialDataSet.TELEMETRY: From d32503fdcf1ce1c4b94a5f7dfe31a29e6d3500fb Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Mon, 22 Jan 2024 16:26:20 -0800 Subject: [PATCH 25/85] moved some code into new components --- .../surveys/view/SurveySpatialData.tsx | 107 +++--------------- .../SurveySpatialObservationDataTable.tsx | 85 ++++++++++++++ .../view/SurveySpatialTelemetryDataTable.tsx | 45 ++++++++ 3 files changed, 143 insertions(+), 94 deletions(-) create mode 100644 app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx create mode 100644 app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 0dd1ee53ee..2f71d82d27 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -4,16 +4,13 @@ import { ObservationsContext } from 'contexts/observationsContext'; import { SurveyContext } from 'contexts/surveyContext'; import { TaxonomyContext } from 'contexts/taxonomyContext'; import { TelemetryDataContext } from 'contexts/telemetryDataContext'; -import dayjs from 'dayjs'; import { Position } from 'geojson'; import { useContext, useEffect, useMemo, useState } from 'react'; import { INonEditableGeometries } from 'utils/mapUtils'; -import { getCodesName } from 'utils/Utils'; -import NoSurveySectionData from '../components/NoSurveySectionData'; -import { ICritterDeployment } from '../telemetry/ManualTelemetryList'; import SurveyMapToolBar, { SurveySpatialDataLayout, SurveySpatialDataSet } from './components/SurveyMapToolBar'; import SurveyMap from './SurveyMap'; -import SurveySpatialDataTable from './SurveySpatialDataTable'; +import SurveySpatialObservationDataTable from './SurveySpatialObservationDataTable'; +import SurveySpatialTelemetryDataTable from './SurveySpatialTelemetryDataTable'; const SurveySpatialData = () => { const observationsContext = useContext(ObservationsContext); @@ -22,6 +19,9 @@ const SurveySpatialData = () => { const surveyContext = useContext(SurveyContext); const codesContext = useContext(CodesContext); + const [mapPoints, setMapPoints] = useState([]); + const [currentTab, setCurrentTab] = useState(SurveySpatialDataSet.OBSERVATIONS); + useEffect(() => { codesContext.codesDataLoader.load(); surveyContext.deploymentDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); @@ -53,36 +53,6 @@ const SurveySpatialData = () => { cacheTaxonomicData(); }, [observationsContext.observationsDataLoader.data]); - const flattenedCritterDeployments: ICritterDeployment[] = useMemo(() => { - const data: ICritterDeployment[] = []; - // combine all critter and deployments into a flat list - surveyContext.deploymentDataLoader.data?.forEach((deployment) => { - const critter = surveyContext.critterDataLoader.data?.find( - (critter) => critter.critter_id === deployment.critter_id - ); - if (critter) { - data.push({ critter, deployment }); - } - }); - return data; - }, [surveyContext.critterDataLoader.data, surveyContext.deploymentDataLoader.data]); - - const sampleSites = useMemo(() => { - return surveyContext.sampleSiteDataLoader.data?.sampleSites ?? []; - }, [surveyContext.sampleSiteDataLoader.data]); - - const sampleMethods = useMemo(() => { - return surveyContext.sampleSiteDataLoader.data?.sampleSites.flatMap((item) => item.sample_methods) || []; - }, [surveyContext.sampleSiteDataLoader.data]); - - const samplePeriods = useMemo(() => { - return ( - surveyContext.sampleSiteDataLoader.data?.sampleSites.flatMap((item) => - item.sample_methods?.flatMap((method) => method.sample_periods) - ) || [] - ); - }, [surveyContext.sampleSiteDataLoader.data]); - const telemetryPoints: INonEditableGeometries[] = useMemo(() => { const telemetryData = telemetryContext.telemetryDataLoader.data; if (!telemetryData) { @@ -130,77 +100,23 @@ const SurveySpatialData = () => { }); }, [observationsContext.observationsDataLoader.data]); - const [mapPoints, setMapPoints] = useState(observationPoints); - const [tableHeaders, setTableHeaders] = useState([]); - const [tableRows, setTableRows] = useState([]); - // TODO: this needs to be saved between page visits // const [layout, setLayout] = useState(SurveySpatialDataLayout.MAP); const updateDataSet = (data: SurveySpatialDataSet) => { + setCurrentTab(data); switch (data) { case SurveySpatialDataSet.OBSERVATIONS: setMapPoints(observationPoints); - setTableHeaders([ - 'Species', - 'Count', - 'Sample Site', - 'Sample Method', - 'Sample Period', - 'Date', - 'Time', - 'Lat', - 'Long' - ]); - setTableRows( - observationsContext.observationsDataLoader.data?.surveyObservations.map((item) => { - const siteName = sampleSites.find( - (site) => site.survey_sample_site_id === item.survey_sample_site_id - )?.name; - const method_id = sampleMethods.find( - (method) => method?.survey_sample_method_id === item.survey_sample_method_id - )?.method_lookup_id; - const period = samplePeriods.find( - (period) => period?.survey_sample_period_id === item.survey_sample_period_id - ); - - return [ - `${taxonomyContext.getCachedSpeciesTaxonomyById(item.wldtaxonomic_units_id)?.label}`, - `${item.count}`, - `${siteName}`, - `${method_id ? getCodesName(codesContext.codesDataLoader.data, 'sample_methods', method_id) : ''}`, - `${period?.start_date} ${period?.end_date}`, - `${dayjs(item.observation_date).format('YYYY-MM-DD')}`, - `${dayjs(item.observation_date).format('HH:mm:ss')}`, - `${item.latitude}`, - `${item.longitude}` - ]; - }) || [] - ); break; case SurveySpatialDataSet.TELEMETRY: setMapPoints(telemetryPoints); - setTableHeaders(['Alias', 'Device ID', 'Start', 'End']); - setTableRows( - flattenedCritterDeployments.map((item) => { - return [ - `${item.critter.animal_id}`, - `${item.deployment.device_id}`, - `${dayjs(item.deployment.attachment_start).format('YYYY-MM-DD')}`, - `${dayjs(item.deployment.attachment_end).format('YYYY-MM-DD')}` - ]; - }) - ); break; case SurveySpatialDataSet.MARKED_ANIMALS: setMapPoints([]); - setTableHeaders(['Deployment ID', 'Device ID', 'Date', 'Time', 'Lat', 'Long']); - setTableRows([]); break; default: setMapPoints([]); - setTableHeaders([]); - setTableRows([]); break; } }; @@ -218,11 +134,14 @@ const SurveySpatialData = () => { - {tableRows.length > 0 ? ( - - ) : ( - + {currentTab === SurveySpatialDataSet.OBSERVATIONS && ( + )} + + {currentTab === SurveySpatialDataSet.TELEMETRY && } {/* {layout === SurveySpatialDataLayout.MAP && ( diff --git a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx new file mode 100644 index 0000000000..40fcd0f632 --- /dev/null +++ b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx @@ -0,0 +1,85 @@ +import { CodesContext } from 'contexts/codesContext'; +import { IObservationRecord } from 'contexts/observationsTableContext'; +import { SurveyContext } from 'contexts/surveyContext'; +import { TaxonomyContext } from 'contexts/taxonomyContext'; +import dayjs from 'dayjs'; +import { IGetSampleLocationRecord } from 'interfaces/useSurveyApi.interface'; +import { useContext, useMemo } from 'react'; +import { getCodesName } from 'utils/Utils'; +import NoSurveySectionData from '../components/NoSurveySectionData'; +import SurveySpatialDataTable from './SurveySpatialDataTable'; + +interface ISurveySpatialObservationDataTableProps { + data: IObservationRecord[]; + sample_sites: IGetSampleLocationRecord[]; +} +const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataTableProps) => { + const surveyContext = useContext(SurveyContext); + const codesContext = useContext(CodesContext); + const taxonomyContext = useContext(TaxonomyContext); + + const sampleSites = useMemo(() => { + return surveyContext.sampleSiteDataLoader.data?.sampleSites ?? []; + }, [surveyContext.sampleSiteDataLoader.data]); + + const sampleMethods = useMemo(() => { + return surveyContext.sampleSiteDataLoader.data?.sampleSites.flatMap((item) => item.sample_methods) || []; + }, [surveyContext.sampleSiteDataLoader.data]); + + const samplePeriods = useMemo(() => { + return ( + surveyContext.sampleSiteDataLoader.data?.sampleSites.flatMap((item) => + item.sample_methods?.flatMap((method) => method.sample_periods) + ) || [] + ); + }, [surveyContext.sampleSiteDataLoader.data]); + + const mapData = (): string[][] => { + return ( + props.data.map((item) => { + const siteName = sampleSites.find((site) => site.survey_sample_site_id === item.survey_sample_site_id)?.name; + const method_id = sampleMethods.find( + (method) => method?.survey_sample_method_id === item.survey_sample_method_id + )?.method_lookup_id; + const period = samplePeriods.find((period) => period?.survey_sample_period_id === item.survey_sample_period_id); + + return [ + `${taxonomyContext.getCachedSpeciesTaxonomyById(item.wldtaxonomic_units_id)?.label}`, + `${item.count}`, + `${siteName}`, + `${method_id ? getCodesName(codesContext.codesDataLoader.data, 'sample_methods', method_id) : ''}`, + `${period?.start_date} ${period?.end_date}`, + `${dayjs(item.observation_date).format('YYYY-MM-DD')}`, + `${dayjs(item.observation_date).format('HH:mm:ss')}`, + `${item.latitude}`, + `${item.longitude}` + ]; + }) || [] + ); + }; + + return ( + <> + {props.data.length > 0 ? ( + + ) : ( + + )} + + ); +}; + +export default SurveySpatialObservationDataTable; diff --git a/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx b/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx new file mode 100644 index 0000000000..5a11f6741f --- /dev/null +++ b/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx @@ -0,0 +1,45 @@ +import { SurveyContext } from 'contexts/surveyContext'; +import dayjs from 'dayjs'; +import { useContext, useMemo } from 'react'; +import NoSurveySectionData from '../components/NoSurveySectionData'; +import { ICritterDeployment } from '../telemetry/ManualTelemetryList'; +import SurveySpatialDataTable from './SurveySpatialDataTable'; + +const SurveySpatialTelemetryDataTable = () => { + const surveyContext = useContext(SurveyContext); + const flattenedCritterDeployments: ICritterDeployment[] = useMemo(() => { + const data: ICritterDeployment[] = []; + // combine all critter and deployments into a flat list + surveyContext.deploymentDataLoader.data?.forEach((deployment) => { + const critter = surveyContext.critterDataLoader.data?.find( + (critter) => critter.critter_id === deployment.critter_id + ); + if (critter) { + data.push({ critter, deployment }); + } + }); + return data; + }, [surveyContext.critterDataLoader.data, surveyContext.deploymentDataLoader.data]); + + const mapData = () => { + return flattenedCritterDeployments.map((item) => { + return [ + `${item.critter.animal_id}`, + `${item.deployment.device_id}`, + `${dayjs(item.deployment.attachment_start).format('YYYY-MM-DD')}`, + `${dayjs(item.deployment.attachment_end).format('YYYY-MM-DD')}` + ]; + }); + }; + return ( + <> + {flattenedCritterDeployments.length > 0 ? ( + + ) : ( + + )} + + ); +}; + +export default SurveySpatialTelemetryDataTable; From dca98f4ee3b1d131d5ab7554588f9ab1e21b0b87 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Mon, 22 Jan 2024 16:47:00 -0800 Subject: [PATCH 26/85] added map skeleton loader component --- .../components/loading/SkeletonLoaders.tsx | 37 +++++++- app/src/features/surveys/view/SurveyMap.tsx | 89 +++++++------------ .../surveys/view/SurveySpatialData.tsx | 11 ++- 3 files changed, 76 insertions(+), 61 deletions(-) diff --git a/app/src/components/loading/SkeletonLoaders.tsx b/app/src/components/loading/SkeletonLoaders.tsx index 0ae22f397e..49574a4c4d 100644 --- a/app/src/components/loading/SkeletonLoaders.tsx +++ b/app/src/components/loading/SkeletonLoaders.tsx @@ -1,3 +1,5 @@ +import { mdiMapSearchOutline } from '@mdi/js'; +import Icon from '@mdi/react'; import Box from '@mui/material/Box'; import { grey } from '@mui/material/colors'; import Paper from '@mui/material/Paper'; @@ -114,4 +116,37 @@ const SkeletonRow = () => ( ); -export { SkeletonList, SkeletonListStack, SkeletonRow, SkeletonTable }; +const SkeletonMap = () => ( + + + + +); + +export { SkeletonList, SkeletonListStack, SkeletonRow, SkeletonTable, SkeletonMap }; diff --git a/app/src/features/surveys/view/SurveyMap.tsx b/app/src/features/surveys/view/SurveyMap.tsx index 0da2eedbb6..e12a7edef1 100644 --- a/app/src/features/surveys/view/SurveyMap.tsx +++ b/app/src/features/surveys/view/SurveyMap.tsx @@ -1,7 +1,4 @@ -import { mdiMapSearchOutline } from '@mdi/js'; -import Icon from '@mdi/react'; -import { Box, Skeleton } from '@mui/material'; -import grey from '@mui/material/colors/grey'; +import { SkeletonMap } from 'components/loading/SkeletonLoaders'; import BaseLayerControls from 'components/map/components/BaseLayerControls'; import { SetMapBounds } from 'components/map/components/Bounds'; import FullScreenScrollingEventHandler from 'components/map/components/FullScreenScrollingEventHandler'; @@ -16,69 +13,43 @@ import { v4 as uuidv4 } from 'uuid'; interface ISurveyMapProps { mapPoints: INonEditableGeometries[]; + isLoading: boolean; } // TODO: need a way to pass in the map dimensions depending on the screen size const SurveyMap = (props: ISurveyMapProps) => { + //TODO: This needs to reset bounds when the data is updated const [bounds] = useState(calculateUpdatedMapBounds([ALL_OF_BC_BOUNDARY])); return ( <> - - - - - + {props.isLoading ? ( + + ) : ( + + + + - - - - + {props.mapPoints?.map((nonEditableGeo: INonEditableGeometries) => ( + coloredPoint({ latlng, fillColor: '#1f7dff', borderColor: '#ffffff' })}> + {nonEditableGeo.popupComponent} + + ))} - {props.mapPoints?.map((nonEditableGeo: INonEditableGeometries) => ( - coloredPoint({ latlng, fillColor: '#1f7dff', borderColor: '#ffffff' })}> - {nonEditableGeo.popupComponent} - - ))} - - - - - + + + + + )} ); }; diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 2f71d82d27..813066d3ae 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -103,6 +103,15 @@ const SurveySpatialData = () => { // TODO: this needs to be saved between page visits // const [layout, setLayout] = useState(SurveySpatialDataLayout.MAP); + const isLoading = () => { + return ( + codesContext.codesDataLoader.isLoading || + surveyContext.deploymentDataLoader.isLoading || + surveyContext.critterDataLoader.isLoading || + surveyContext.sampleSiteDataLoader.isLoading + ); + }; + const updateDataSet = (data: SurveySpatialDataSet) => { setCurrentTab(data); switch (data) { @@ -131,7 +140,7 @@ const SurveySpatialData = () => { - + {currentTab === SurveySpatialDataSet.OBSERVATIONS && ( From f52955fbd17088b3f47e9a1e1826fa72160f7858 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Mon, 22 Jan 2024 16:56:08 -0800 Subject: [PATCH 27/85] lint --- .../view/components/SurveyMapToolBar.tsx | 80 ++++++++++++------- 1 file changed, 51 insertions(+), 29 deletions(-) diff --git a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx index e8b62dc6f6..ba5436c57c 100644 --- a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx +++ b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx @@ -1,6 +1,17 @@ import { mdiBroadcast, mdiChevronDown, mdiCog, mdiEye } from '@mdi/js'; import Icon from '@mdi/react'; -import { Box, Divider, ListItemIcon, ListItemText, Menu, MenuItem, ToggleButton, ToggleButtonGroup, Toolbar, Typography } from '@mui/material'; +import { + Box, + Divider, + ListItemIcon, + ListItemText, + Menu, + MenuItem, + ToggleButton, + ToggleButtonGroup, + Toolbar, + Typography +} from '@mui/material'; import Button from '@mui/material/Button'; import { useState } from 'react'; @@ -47,25 +58,28 @@ const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { return ( <> - + horizontal: 'right' + }}> - + + + Observations - + + + Telemetry @@ -79,27 +93,25 @@ const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { Survey Data - - { fontWeight: 700, letterSpacing: '0.02rem' } - }} - > - } value={SurveySpatialDataSet.OBSERVATIONS}>Observations - } value={SurveySpatialDataSet.TELEMETRY}>Telemetry + }}> + } + value={SurveySpatialDataSet.OBSERVATIONS}> + Observations + + } + value={SurveySpatialDataSet.TELEMETRY}> + Telemetry + - + MAP TABLE SPLIT - From 04bdee17fde5e64dabca0a1f86c6b1a89d070da4 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 23 Jan 2024 11:57:54 -0800 Subject: [PATCH 28/85] added labels to toggle buttons --- .../surveys/view/SurveySpatialData.tsx | 26 ++++++-- .../view/components/SurveyMapToolBar.tsx | 59 ++++++++++--------- 2 files changed, 52 insertions(+), 33 deletions(-) diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 813066d3ae..7c9f0eb020 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -1,3 +1,4 @@ +import { mdiBroadcast, mdiEye } from '@mdi/js'; import { Box, Paper } from '@mui/material'; import { CodesContext } from 'contexts/codesContext'; import { ObservationsContext } from 'contexts/observationsContext'; @@ -24,9 +25,9 @@ const SurveySpatialData = () => { useEffect(() => { codesContext.codesDataLoader.load(); - surveyContext.deploymentDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); - surveyContext.critterDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); - surveyContext.sampleSiteDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); + surveyContext.deploymentDataLoader.load(surveyContext.projectId, surveyContext.surveyId); + surveyContext.critterDataLoader.load(surveyContext.projectId, surveyContext.surveyId); + surveyContext.sampleSiteDataLoader.load(surveyContext.projectId, surveyContext.surveyId); }, []); useEffect(() => { @@ -137,7 +138,24 @@ const SurveySpatialData = () => { return ( - + diff --git a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx index ba5436c57c..0f1cae9d7b 100644 --- a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx +++ b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx @@ -1,19 +1,18 @@ import { mdiBroadcast, mdiChevronDown, mdiCog, mdiEye } from '@mdi/js'; import Icon from '@mdi/react'; -import { - Box, - Divider, - ListItemIcon, - ListItemText, - Menu, - MenuItem, - ToggleButton, - ToggleButtonGroup, - Toolbar, - Typography -} from '@mui/material'; +import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; +import Divider from '@mui/material/Divider'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; import { useState } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; export enum SurveySpatialDataSet { OBSERVATIONS = 'Observations', @@ -27,10 +26,16 @@ export enum SurveySpatialDataLayout { SPLIT = 'Split' } +interface IToolBarButtons { + label: string; + icon: string; + value: SurveySpatialDataSet; + isLoading: boolean; +} interface ISurveyMapToolBarProps { - //TODO: I don't want to pull the contexts into this but I will need an array of key value pairs for the options updateDataSet: (data: SurveySpatialDataSet) => void; updateLayout: (data: SurveySpatialDataLayout) => void; + toggleButtons: IToolBarButtons[]; } const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { const [dataset, setDataset] = useState(SurveySpatialDataSet.OBSERVATIONS); @@ -62,6 +67,7 @@ const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { anchorEl={anchorEl} open={open} onClose={handleMenuClose} + disableAutoFocusItem anchorOrigin={{ vertical: 'bottom', horizontal: 'right' @@ -70,13 +76,13 @@ const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { vertical: 'top', horizontal: 'right' }}> - + Observations - + @@ -125,20 +131,15 @@ const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { letterSpacing: '0.02rem' } }}> - } - value={SurveySpatialDataSet.OBSERVATIONS}> - Observations - - } - value={SurveySpatialDataSet.TELEMETRY}> - Telemetry - + {props.toggleButtons.map((item) => ( + } + value={item.value}> + {item.label} + + ))} From 29d49d9b986cfe7f7b0af7db9787835ab83b0ebf Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 23 Jan 2024 13:01:09 -0800 Subject: [PATCH 29/85] fixing data load --- .../surveys/view/SurveySpatialData.tsx | 31 ++++++++++--------- .../view/components/SurveyMapToolBar.tsx | 1 + 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 7c9f0eb020..1e4f4d58b5 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -20,7 +20,6 @@ const SurveySpatialData = () => { const surveyContext = useContext(SurveyContext); const codesContext = useContext(CodesContext); - const [mapPoints, setMapPoints] = useState([]); const [currentTab, setCurrentTab] = useState(SurveySpatialDataSet.OBSERVATIONS); useEffect(() => { @@ -115,20 +114,6 @@ const SurveySpatialData = () => { const updateDataSet = (data: SurveySpatialDataSet) => { setCurrentTab(data); - switch (data) { - case SurveySpatialDataSet.OBSERVATIONS: - setMapPoints(observationPoints); - break; - case SurveySpatialDataSet.TELEMETRY: - setMapPoints(telemetryPoints); - break; - case SurveySpatialDataSet.MARKED_ANIMALS: - setMapPoints([]); - break; - default: - setMapPoints([]); - break; - } }; const updateLayout = (data: SurveySpatialDataLayout) => { @@ -136,6 +121,22 @@ const SurveySpatialData = () => { // setLayout(data); }; + let mapPoints: INonEditableGeometries[] = []; + switch (currentTab) { + case SurveySpatialDataSet.OBSERVATIONS: + mapPoints = observationPoints; + break; + case SurveySpatialDataSet.TELEMETRY: + mapPoints = telemetryPoints; + break; + case SurveySpatialDataSet.MARKED_ANIMALS: + mapPoints = []; + break; + default: + mapPoints = []; + break; + } + return ( { }}> {props.toggleButtons.map((item) => ( } From ffae6e9f5f4645426998b3bba0c90fe7fa034711 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 23 Jan 2024 13:39:41 -0800 Subject: [PATCH 30/85] split loading logic --- .../surveys/view/SurveySpatialData.tsx | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 1e4f4d58b5..b023835420 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -104,12 +104,22 @@ const SurveySpatialData = () => { // const [layout, setLayout] = useState(SurveySpatialDataLayout.MAP); const isLoading = () => { - return ( - codesContext.codesDataLoader.isLoading || - surveyContext.deploymentDataLoader.isLoading || - surveyContext.critterDataLoader.isLoading || - surveyContext.sampleSiteDataLoader.isLoading - ); + let isLoading = false; + if (currentTab === SurveySpatialDataSet.OBSERVATIONS) { + isLoading = + codesContext.codesDataLoader.isLoading || + surveyContext.sampleSiteDataLoader.isLoading || + observationsContext.observationsDataLoader.isLoading; + } + + if (currentTab === SurveySpatialDataSet.TELEMETRY) { + isLoading = + codesContext.codesDataLoader.isLoading || + surveyContext.deploymentDataLoader.isLoading || + surveyContext.critterDataLoader.isLoading; + } + + return isLoading; }; const updateDataSet = (data: SurveySpatialDataSet) => { @@ -162,14 +172,14 @@ const SurveySpatialData = () => { - {currentTab === SurveySpatialDataSet.OBSERVATIONS && ( + {currentTab === SurveySpatialDataSet.OBSERVATIONS && !isLoading() && ( )} - {currentTab === SurveySpatialDataSet.TELEMETRY && } + {currentTab === SurveySpatialDataSet.TELEMETRY && !isLoading() && } {/* {layout === SurveySpatialDataLayout.MAP && ( From a39c87e1f36d17b2dd33a5ba9d395d5b439fe7df Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 23 Jan 2024 14:59:58 -0800 Subject: [PATCH 31/85] added loading skeletons for tables --- .../features/surveys/view/SurveySpatialData.tsx | 9 ++++++--- .../view/SurveySpatialObservationDataTable.tsx | 12 +++++++++--- .../view/SurveySpatialTelemetryDataTable.tsx | 17 +++++++++++++---- .../view/components/SurveyMapToolBar.tsx | 5 ++--- 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index b023835420..c1db8cb460 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -20,7 +20,8 @@ const SurveySpatialData = () => { const surveyContext = useContext(SurveyContext); const codesContext = useContext(CodesContext); - const [currentTab, setCurrentTab] = useState(SurveySpatialDataSet.OBSERVATIONS); + //TODO: look into adding this to the query param + const [currentTab, setCurrentTab] = useState(SurveySpatialDataSet.TELEMETRY); useEffect(() => { codesContext.codesDataLoader.load(); @@ -150,6 +151,7 @@ const SurveySpatialData = () => { return ( { - {currentTab === SurveySpatialDataSet.OBSERVATIONS && !isLoading() && ( + {currentTab === SurveySpatialDataSet.OBSERVATIONS && ( )} - {currentTab === SurveySpatialDataSet.TELEMETRY && !isLoading() && } + {currentTab === SurveySpatialDataSet.TELEMETRY && } {/* {layout === SurveySpatialDataLayout.MAP && ( diff --git a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx index 40fcd0f632..a7d986f5ce 100644 --- a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx @@ -1,3 +1,4 @@ +import { SkeletonList } from 'components/loading/SkeletonLoaders'; import { CodesContext } from 'contexts/codesContext'; import { IObservationRecord } from 'contexts/observationsTableContext'; import { SurveyContext } from 'contexts/surveyContext'; @@ -12,6 +13,7 @@ import SurveySpatialDataTable from './SurveySpatialDataTable'; interface ISurveySpatialObservationDataTableProps { data: IObservationRecord[]; sample_sites: IGetSampleLocationRecord[]; + isLoading: boolean; } const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataTableProps) => { const surveyContext = useContext(SurveyContext); @@ -60,7 +62,13 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT return ( <> - {props.data.length > 0 ? ( + {props.isLoading && } + + {!props.isLoading && props.data.length === 0 && ( + + )} + + {!props.isLoading && props.data.length > 0 && ( - ) : ( - )} ); diff --git a/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx b/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx index 5a11f6741f..0c6e77b01a 100644 --- a/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx @@ -1,3 +1,4 @@ +import { SkeletonList } from 'components/loading/SkeletonLoaders'; import { SurveyContext } from 'contexts/surveyContext'; import dayjs from 'dayjs'; import { useContext, useMemo } from 'react'; @@ -5,7 +6,10 @@ import NoSurveySectionData from '../components/NoSurveySectionData'; import { ICritterDeployment } from '../telemetry/ManualTelemetryList'; import SurveySpatialDataTable from './SurveySpatialDataTable'; -const SurveySpatialTelemetryDataTable = () => { +interface ISurveySpatialTelemetryDataTableProps { + isLoading: boolean; +} +const SurveySpatialTelemetryDataTable = (props: ISurveySpatialTelemetryDataTableProps) => { const surveyContext = useContext(SurveyContext); const flattenedCritterDeployments: ICritterDeployment[] = useMemo(() => { const data: ICritterDeployment[] = []; @@ -31,12 +35,17 @@ const SurveySpatialTelemetryDataTable = () => { ]; }); }; + return ( <> - {flattenedCritterDeployments.length > 0 ? ( + {flattenedCritterDeployments.length > 0 && !props.isLoading && ( - ) : ( - + )} + + {props.isLoading && } + + {!props.isLoading && flattenedCritterDeployments.length === 0 && ( + )} ); diff --git a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx index d911d2cb16..0b2a61c329 100644 --- a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx +++ b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx @@ -36,16 +36,15 @@ interface ISurveyMapToolBarProps { updateDataSet: (data: SurveySpatialDataSet) => void; updateLayout: (data: SurveySpatialDataLayout) => void; toggleButtons: IToolBarButtons[]; + currentTab: SurveySpatialDataSet; } const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { - const [dataset, setDataset] = useState(SurveySpatialDataSet.OBSERVATIONS); const [layout, setLayout] = useState(SurveySpatialDataLayout.MAP); const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); const updateDataSet = (event: React.MouseEvent, newAlignment: SurveySpatialDataSet) => { - setDataset(newAlignment); props.updateDataSet(newAlignment); }; const updateLayout = (event: React.MouseEvent, newAlignment: SurveySpatialDataLayout) => { @@ -115,7 +114,7 @@ const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { Date: Tue, 23 Jan 2024 16:13:38 -0800 Subject: [PATCH 32/85] renamed old grid, updated observation table --- ...{CustomDataGrid.tsx => StyledDataGrid.tsx} | 2 +- .../list/FundingSourcesTable.tsx | 4 +- .../surveys/view/SurveySpatialDataTable.tsx | 1 - .../SurveySpatialObservationDataTable.tsx | 136 +++++++++++++----- .../survey-animals/SurveyAnimalsTable.tsx | 4 +- 5 files changed, 104 insertions(+), 43 deletions(-) rename app/src/components/data-grid/{CustomDataGrid.tsx => StyledDataGrid.tsx} (95%) diff --git a/app/src/components/data-grid/CustomDataGrid.tsx b/app/src/components/data-grid/StyledDataGrid.tsx similarity index 95% rename from app/src/components/data-grid/CustomDataGrid.tsx rename to app/src/components/data-grid/StyledDataGrid.tsx index 0036f56b6d..f90bfc8a63 100644 --- a/app/src/components/data-grid/CustomDataGrid.tsx +++ b/app/src/components/data-grid/StyledDataGrid.tsx @@ -34,7 +34,7 @@ const useStyles = makeStyles(() => ({ } } })); -export const CustomDataGrid = (props: DataGridProps) => { +export const StyledDataGrid = (props: DataGridProps) => { const classes = useStyles(); const NoRowsOverlayStyled = useCallback(() => , [classes.noDataText]); return ; diff --git a/app/src/features/funding-sources/list/FundingSourcesTable.tsx b/app/src/features/funding-sources/list/FundingSourcesTable.tsx index 8771d35efc..8a0ade4357 100644 --- a/app/src/features/funding-sources/list/FundingSourcesTable.tsx +++ b/app/src/features/funding-sources/list/FundingSourcesTable.tsx @@ -1,5 +1,5 @@ import { GridColDef } from '@mui/x-data-grid'; -import { CustomDataGrid } from 'components/data-grid/CustomDataGrid'; +import { StyledDataGrid } from 'components/data-grid/StyledDataGrid'; import { IGetFundingSourcesResponse } from 'interfaces/useFundingSourceApi.interface'; import { getFormattedAmount } from 'utils/Utils'; import TableActionsMenu from './FundingSourcesTableActionsMenu'; @@ -62,7 +62,7 @@ const FundingSourcesTable = (props: IFundingSourcesTableTableProps) => { ]; return ( - `funding-source-${row.funding_source_id}`} diff --git a/app/src/features/surveys/view/SurveySpatialDataTable.tsx b/app/src/features/surveys/view/SurveySpatialDataTable.tsx index ac7e111327..00d4212642 100644 --- a/app/src/features/surveys/view/SurveySpatialDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialDataTable.tsx @@ -10,7 +10,6 @@ interface ISurveySpatialDataTableProps { tableHeaders: string[]; tableRows: string[][]; } - const SurveySpatialDataTable = (props: ISurveySpatialDataTableProps) => { return ( <> diff --git a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx index a7d986f5ce..80a5220d69 100644 --- a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx @@ -1,3 +1,5 @@ +import { GridColDef } from '@mui/x-data-grid'; +import { StyledDataGrid } from 'components/data-grid/StyledDataGrid'; import { SkeletonList } from 'components/loading/SkeletonLoaders'; import { CodesContext } from 'contexts/codesContext'; import { IObservationRecord } from 'contexts/observationsTableContext'; @@ -8,8 +10,19 @@ import { IGetSampleLocationRecord } from 'interfaces/useSurveyApi.interface'; import { useContext, useMemo } from 'react'; import { getCodesName } from 'utils/Utils'; import NoSurveySectionData from '../components/NoSurveySectionData'; -import SurveySpatialDataTable from './SurveySpatialDataTable'; +interface IObservationTableRow { + id: number; + taxon: string | undefined; + count: number | null; + site: string | undefined; + method: string | undefined; + period: string | undefined; + date: string | undefined; + time: string | undefined; + lat: number | null; + long: number | null; +} interface ISurveySpatialObservationDataTableProps { data: IObservationRecord[]; sample_sites: IGetSampleLocationRecord[]; @@ -36,29 +49,73 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT ); }, [surveyContext.sampleSiteDataLoader.data]); - const mapData = (): string[][] => { - return ( - props.data.map((item) => { - const siteName = sampleSites.find((site) => site.survey_sample_site_id === item.survey_sample_site_id)?.name; - const method_id = sampleMethods.find( - (method) => method?.survey_sample_method_id === item.survey_sample_method_id - )?.method_lookup_id; - const period = samplePeriods.find((period) => period?.survey_sample_period_id === item.survey_sample_period_id); + const tableData: IObservationTableRow[] = props.data.map((item) => { + const siteName = sampleSites.find((site) => site.survey_sample_site_id === item.survey_sample_site_id)?.name; + const method_id = sampleMethods.find( + (method) => method?.survey_sample_method_id === item.survey_sample_method_id + )?.method_lookup_id; + const period = samplePeriods.find((period) => period?.survey_sample_period_id === item.survey_sample_period_id); - return [ - `${taxonomyContext.getCachedSpeciesTaxonomyById(item.wldtaxonomic_units_id)?.label}`, - `${item.count}`, - `${siteName}`, - `${method_id ? getCodesName(codesContext.codesDataLoader.data, 'sample_methods', method_id) : ''}`, - `${period?.start_date} ${period?.end_date}`, - `${dayjs(item.observation_date).format('YYYY-MM-DD')}`, - `${dayjs(item.observation_date).format('HH:mm:ss')}`, - `${item.latitude}`, - `${item.longitude}` - ]; - }) || [] - ); - }; + return { + id: item.survey_observation_id, + taxon: taxonomyContext.getCachedSpeciesTaxonomyById(item.wldtaxonomic_units_id)?.label, + count: item.count, + site: siteName, + method: method_id ? getCodesName(codesContext.codesDataLoader.data, 'sample_methods', method_id) : '', + period: `${period?.start_date} ${period?.end_date}`, + date: dayjs(item.observation_date).format('YYYY-MM-DD'), + time: dayjs(item.observation_date).format('HH:mm:ss'), + lat: item.latitude, + long: item.longitude + }; + }); + const columns: GridColDef[] = [ + { + field: 'taxon', + headerName: 'Species', + flex: 1 + }, + { + field: 'count', + headerName: 'Count', + flex: 1 + }, + { + field: 'site', + headerName: 'Sample Site', + flex: 1 + }, + { + field: 'method', + headerName: 'Sample Method', + flex: 1 + }, + { + field: 'period', + headerName: 'Sample Period', + flex: 1 + }, + { + field: 'date', + headerName: 'Date', + flex: 1 + }, + { + field: 'time', + headerName: 'Time', + flex: 1 + }, + { + field: 'lat', + headerName: 'Lat', + flex: 1 + }, + { + field: 'long', + headerName: 'Long', + flex: 1 + } + ]; return ( <> @@ -69,20 +126,25 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT )} {!props.isLoading && props.data.length > 0 && ( - + <> + row.id} + columns={columns} + pageSizeOptions={[5]} + rowSelection={false} + checkboxSelection={false} + hideFooter + disableRowSelectionOnClick + disableColumnSelector + disableColumnFilter + disableColumnMenu + disableVirtualization + sortingOrder={['asc', 'desc']} + data-testid="survey-spatial-observation-data-table" + /> + )} ); diff --git a/app/src/features/surveys/view/survey-animals/SurveyAnimalsTable.tsx b/app/src/features/surveys/view/survey-animals/SurveyAnimalsTable.tsx index c8d7781fae..513f63c829 100644 --- a/app/src/features/surveys/view/survey-animals/SurveyAnimalsTable.tsx +++ b/app/src/features/surveys/view/survey-animals/SurveyAnimalsTable.tsx @@ -1,5 +1,5 @@ import { GridColDef } from '@mui/x-data-grid'; -import { CustomDataGrid } from 'components/data-grid/CustomDataGrid'; +import { StyledDataGrid } from 'components/data-grid/StyledDataGrid'; import { default as dayjs } from 'dayjs'; import { IDetailedCritterWithInternalId } from 'interfaces/useSurveyApi.interface'; import SurveyAnimalsTableActions from './SurveyAnimalsTableActions'; @@ -106,7 +106,7 @@ export const SurveyAnimalsTable = ({ ]; return ( - row.critter_id} From b3c8cdfa628bf179d89cc1ff7eeeeb08a65757e7 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 23 Jan 2024 16:27:12 -0800 Subject: [PATCH 33/85] swapped out telemetry table with data grid --- .../surveys/view/SurveySpatialData.tsx | 6 +- .../view/SurveySpatialTelemetryDataTable.tsx | 69 +++++++++++++++---- 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index c1db8cb460..449872a50b 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -25,9 +25,9 @@ const SurveySpatialData = () => { useEffect(() => { codesContext.codesDataLoader.load(); - surveyContext.deploymentDataLoader.load(surveyContext.projectId, surveyContext.surveyId); - surveyContext.critterDataLoader.load(surveyContext.projectId, surveyContext.surveyId); - surveyContext.sampleSiteDataLoader.load(surveyContext.projectId, surveyContext.surveyId); + surveyContext.deploymentDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); + surveyContext.critterDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); + surveyContext.sampleSiteDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); }, []); useEffect(() => { diff --git a/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx b/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx index 0c6e77b01a..b5b6a016ff 100644 --- a/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx @@ -1,11 +1,18 @@ +import { GridColDef } from '@mui/x-data-grid'; +import { StyledDataGrid } from 'components/data-grid/StyledDataGrid'; import { SkeletonList } from 'components/loading/SkeletonLoaders'; import { SurveyContext } from 'contexts/surveyContext'; import dayjs from 'dayjs'; import { useContext, useMemo } from 'react'; import NoSurveySectionData from '../components/NoSurveySectionData'; import { ICritterDeployment } from '../telemetry/ManualTelemetryList'; -import SurveySpatialDataTable from './SurveySpatialDataTable'; - +interface ITelemetryData { + id: number; + critter_id: string | null; + device_id: number; + start: string; + end: string; +} interface ISurveySpatialTelemetryDataTableProps { isLoading: boolean; } @@ -25,21 +32,55 @@ const SurveySpatialTelemetryDataTable = (props: ISurveySpatialTelemetryDataTable return data; }, [surveyContext.critterDataLoader.data, surveyContext.deploymentDataLoader.data]); - const mapData = () => { - return flattenedCritterDeployments.map((item) => { - return [ - `${item.critter.animal_id}`, - `${item.deployment.device_id}`, - `${dayjs(item.deployment.attachment_start).format('YYYY-MM-DD')}`, - `${dayjs(item.deployment.attachment_end).format('YYYY-MM-DD')}` - ]; - }); - }; - + const tableData: ITelemetryData[] = flattenedCritterDeployments.map((item) => ({ + id: item.critter.survey_critter_id, + critter_id: item.critter.animal_id, + device_id: item.deployment.device_id, + start: dayjs(item.deployment.attachment_start).format('YYYY-MM-DD'), + end: item.deployment.attachment_end ? dayjs(item.deployment.attachment_end).format('YYYY-MM-DD') : 'Still Active' + })); + const columns: GridColDef[] = [ + { + field: 'critter_id', + headerName: 'Alias', + flex: 1 + }, + { + field: 'device_id', + headerName: 'Device ID', + flex: 1 + }, + { + field: 'start', + headerName: 'Start', + flex: 1 + }, + { + field: 'end', + headerName: 'End', + flex: 1 + } + ]; return ( <> {flattenedCritterDeployments.length > 0 && !props.isLoading && ( - + row.id} + columns={columns} + pageSizeOptions={[5]} + rowSelection={false} + checkboxSelection={false} + hideFooter + disableRowSelectionOnClick + disableColumnSelector + disableColumnFilter + disableColumnMenu + disableVirtualization + sortingOrder={['asc', 'desc']} + data-testid="survey-spatial-telemetry-data-table" + /> )} {props.isLoading && } From d99f68c4787a0e1302edd6dfa2663eb826678f4c Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Tue, 23 Jan 2024 17:02:43 -0800 Subject: [PATCH 34/85] SIMSBIOHUB-445: Added observations pagination on the API --- .../survey/{surveyId}/observations/index.ts | 53 +++++++++++++++++-- .../repositories/observation-repository.ts | 14 +++-- api/src/services/observation-service.ts | 21 ++++++-- api/src/zod-schema/pagination.ts | 23 ++++++++ app/src/hooks/api/useObservationApi.ts | 14 ++++- .../interfaces/useObservationApi.interface.ts | 10 +++- 6 files changed, 121 insertions(+), 14 deletions(-) create mode 100644 api/src/zod-schema/pagination.ts diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts index f0cec31397..e13e6b471f 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts @@ -7,6 +7,7 @@ import { InsertObservation, UpdateObservation } from '../../../../../../reposito import { authorizeRequestHandler } from '../../../../../../request-handlers/security/authorization'; import { ObservationService } from '../../../../../../services/observation-service'; import { getLogger } from '../../../../../../utils/logger'; +import { ApiPaginationOptions } from '../../../../../../zod-schema/pagination'; const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/observation'); @@ -136,6 +137,28 @@ export const surveyObservationsResponseSchema: SchemaObject = { } }; +const paginationSchema: SchemaObject = { + type: 'object', + required: ['total', 'per_page', 'current_page', 'last_page'], + properties: { + total: { + type: 'integer' + }, + per_page: { + type: 'integer', + minimum: 1 + }, + current_page: { + type: 'integer', + minimum: 1, + }, + last_page: { + type: 'integer', + minimum: 1, + } + } +} + GET.apiDoc = { description: 'Get all observations for the survey.', tags: ['observation'], @@ -174,7 +197,8 @@ GET.apiDoc = { required: ['surveyObservations', 'supplementaryObservationData'], properties: { ...surveyObservationsResponseSchema.properties, - supplementaryObservationData: { ...surveyObservationsSupplementaryData } + supplementaryObservationData: { ...surveyObservationsSupplementaryData }, + pagination: { ...paginationSchema } }, title: 'Survey get response object, for view purposes' } @@ -217,6 +241,16 @@ PUT.apiDoc = { in: 'path', name: 'surveyId', required: true + }, + { + in: 'query', + name: 'page', + required: false + }, + { + in: 'query', + name: 'limit', + required: false } ], requestBody: { @@ -302,6 +336,8 @@ PUT.apiDoc = { export function getSurveyObservations(): RequestHandler { return async (req, res) => { const surveyId = Number(req.params.surveyId); + const page: number | undefined = req.query.page ? Number(req.query.page) : undefined; + const limit: number | undefined = req.query.limit ? Number(req.query.limit) : undefined; defaultLog.debug({ label: 'getSurveyObservations', surveyId }); @@ -312,8 +348,19 @@ export function getSurveyObservations(): RequestHandler { const observationService = new ObservationService(connection); - const observationData = await observationService.getSurveyObservationsWithSupplementaryData(surveyId); - return res.status(200).json(observationData); + const paginationOptions: ApiPaginationOptions | undefined = (limit && page) ? { limit, page } : undefined + const observationData = await observationService.getSurveyObservationsWithSupplementaryData(surveyId, paginationOptions); + const { observationCount } = observationData.supplementaryObservationData + + return res.status(200).json({ + ...observationData, + pagination: { + total: observationCount, + per_page: limit ?? observationCount, + current_page: page ?? 1, + last_page: limit ? Math.ceil(observationCount / limit) : 1 + } + }); } catch (error) { defaultLog.error({ label: 'getSurveyObservations', message: 'error', error }); await connection.rollback(); diff --git a/api/src/repositories/observation-repository.ts b/api/src/repositories/observation-repository.ts index 1de84412de..674fe85a28 100644 --- a/api/src/repositories/observation-repository.ts +++ b/api/src/repositories/observation-repository.ts @@ -4,6 +4,7 @@ import { getKnex } from '../database/db'; import { ApiExecuteSQLError } from '../errors/api-error'; import { getLogger } from '../utils/logger'; import { BaseRepository } from './base-repository'; +import { ApiPaginationOptions } from '../zod-schema/pagination'; /** * Interface reflecting survey observations retrieved from the database @@ -200,14 +201,19 @@ export class ObservationRepository extends BaseRepository { * Retrieves all observation records for the given survey * * @param {number} surveyId + * @param {ApiPaginationOptions} [pagination] * @return {*} {Promise} * @memberof ObservationRepository */ - async getSurveyObservations(surveyId: number): Promise { + async getSurveyObservations(surveyId: number, pagination?: ApiPaginationOptions): Promise { const knex = getKnex(); - const sqlStatement = knex.queryBuilder().select('*').from('survey_observation').where('survey_id', surveyId); + const allRowsQuery = knex.queryBuilder().select('*').from('survey_observation').where('survey_id', surveyId) - const response = await this.connection.knex(sqlStatement, ObservationRecord); + const query = pagination + ? allRowsQuery.limit(pagination.limit).offset((pagination.page - 1) * pagination.limit) + : allRowsQuery + + const response = await this.connection.knex(query, ObservationRecord); return response.rows; } @@ -215,7 +221,7 @@ export class ObservationRepository extends BaseRepository { * Retrieves the count of survey observations for the given survey * * @param {number} surveyId - * @return {*} {Promise<{ observationCount: number }>} + * @return {*} {Promise} * @memberof ObservationRepository */ async getSurveyObservationCount(surveyId: number): Promise { diff --git a/api/src/services/observation-service.ts b/api/src/services/observation-service.ts index 4c11a811f7..ffdd0858f8 100644 --- a/api/src/services/observation-service.ts +++ b/api/src/services/observation-service.ts @@ -21,6 +21,7 @@ import { validateWorksheetHeaders } from '../utils/xlsx-utils/worksheet-utils'; import { DBService } from './db-service'; +import { ApiPaginationOptions } from '../zod-schema/pagination'; const defaultLog = getLogger('services/observation-service'); @@ -114,13 +115,15 @@ export class ObservationService extends DBService { * Retrieves all observation records for the given survey along with supplementary data * * @param {number} surveyId + * @param {ApiPaginationOptions} [pagination] * @return {*} {Promise<{ surveyObservations: ObservationRecord[]; supplementaryObservationData: ObservationSupplementaryData }>} * @memberof ObservationService */ async getSurveyObservationsWithSupplementaryData( - surveyId: number + surveyId: number, + pagination?: ApiPaginationOptions ): Promise<{ surveyObservations: ObservationRecord[]; supplementaryObservationData: ObservationSupplementaryData }> { - const surveyObservations = await this.observationRepository.getSurveyObservations(surveyId); + const surveyObservations = await this.observationRepository.getSurveyObservations(surveyId, pagination); const supplementaryObservationData = await this.getSurveyObservationsSupplementaryData(surveyId); return { surveyObservations, supplementaryObservationData }; @@ -130,7 +133,7 @@ export class ObservationService extends DBService { * Retrieves all supplementary data for the given survey's observations * * @param {number} surveyId - * @return {*} {Promise<{ surveyObservations: ObservationRecord[]; supplementaryObservationData: ObservationSupplementaryData }>} + * @return {*} {Promise} * @memberof ObservationService */ async getSurveyObservationsSupplementaryData(surveyId: number): Promise { @@ -139,6 +142,18 @@ export class ObservationService extends DBService { return { observationCount }; } + /** + * Retrieves the count of survey observations for the given survey + * + * @param {number} surveyId + * @return {*} {Promise} + * @memberof ObservationRepository + */ + async getSurveyObservationCount(surveyId: number): Promise { + return this.observationRepository.getSurveyObservationCount(surveyId); + } + + /** * Inserts a survey observation submission record into the database and returns the key * diff --git a/api/src/zod-schema/pagination.ts b/api/src/zod-schema/pagination.ts new file mode 100644 index 0000000000..d378a45c0a --- /dev/null +++ b/api/src/zod-schema/pagination.ts @@ -0,0 +1,23 @@ +import { z } from "zod"; + +/** + * Object used to make paginated requests + */ +export const ApiPaginationOptions = z.object({ + limit: z.number(), + page: z.number(), +}); + +export type ApiPaginationOptions = z.infer; + +/** + * Object used to represent results from paginated queries + */ +export const ApiPaginationResults = z.object({ + total: z.number(), + per_page: z.number(), + current_page: z.number(), + last_page: z.number() +}); + +export type ApiPaginationResults = z.infer; \ No newline at end of file diff --git a/app/src/hooks/api/useObservationApi.ts b/app/src/hooks/api/useObservationApi.ts index aec547a2ce..a32a19d941 100644 --- a/app/src/hooks/api/useObservationApi.ts +++ b/app/src/hooks/api/useObservationApi.ts @@ -43,10 +43,20 @@ const useObservationApi = (axios: AxiosInstance) => { */ const getObservationRecords = async ( projectId: number, - surveyId: number + surveyId: number, + pagination?: { page: number, limit: number } ): Promise => { + let urlParamsString = ''; + + if (pagination) { + const params = new URLSearchParams(); + params.append('page', pagination.page.toString()); + params.append('limit', pagination.limit.toString()); + urlParamsString = `?${params.toString()}`; + } + const { data } = await axios.get( - `/api/project/${projectId}/survey/${surveyId}/observations` + `/api/project/${projectId}/survey/${surveyId}/observations${urlParamsString}` ); return data; diff --git a/app/src/interfaces/useObservationApi.interface.ts b/app/src/interfaces/useObservationApi.interface.ts index bfda8e953c..7c5a124e91 100644 --- a/app/src/interfaces/useObservationApi.interface.ts +++ b/app/src/interfaces/useObservationApi.interface.ts @@ -1,6 +1,12 @@ -import { IObservationRecord } from 'contexts/observationsTableContext'; +import { IObservationRecord, ISupplementaryObservationData } from 'contexts/observationsTableContext'; export interface IGetSurveyObservationsResponse { surveyObservations: IObservationRecord[]; - supplementaryObservationData: { observationCount: number }; + supplementaryObservationData: ISupplementaryObservationData; + pagination: { + total: number; + per_page: number; + current_page: number; + last_page: number; + }; } From bab422ff53f2f8f5c827a2675b7b6a775f884b05 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 24 Jan 2024 09:22:41 -0800 Subject: [PATCH 35/85] swapped default tab --- app/src/features/surveys/view/SurveySpatialData.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 449872a50b..5b32faf47b 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -21,7 +21,7 @@ const SurveySpatialData = () => { const codesContext = useContext(CodesContext); //TODO: look into adding this to the query param - const [currentTab, setCurrentTab] = useState(SurveySpatialDataSet.TELEMETRY); + const [currentTab, setCurrentTab] = useState(SurveySpatialDataSet.OBSERVATIONS); useEffect(() => { codesContext.codesDataLoader.load(); From 9b7e87a8601a001975f3690c68d0a2fac815450c Mon Sep 17 00:00:00 2001 From: jeznorth Date: Wed, 24 Jan 2024 10:40:36 -0800 Subject: [PATCH 36/85] Updating Spatial Tables --- .../surveys/view/SurveySpatialData.tsx | 2 +- .../SurveySpatialObservationDataTable.tsx | 76 ++++++++++++++++--- .../view/SurveySpatialTelemetryDataTable.tsx | 59 ++++++++++++-- 3 files changed, 119 insertions(+), 18 deletions(-) diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 449872a50b..d9340f2111 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -173,7 +173,7 @@ const SurveySpatialData = () => { - + {currentTab === SurveySpatialDataSet.OBSERVATIONS && ( [] = [ { field: 'taxon', headerName: 'Species', flex: 1 }, - { - field: 'count', - headerName: 'Count', - flex: 1 - }, { field: 'site', headerName: 'Sample Site', @@ -95,31 +93,86 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT headerName: 'Sample Period', flex: 1 }, + { + field: 'count', + headerName: 'Count', + headerAlign: 'right', + align: 'right', + width: 100 + }, { field: 'date', headerName: 'Date', - flex: 1 + width: 100 }, { field: 'time', headerName: 'Time', - flex: 1 + headerAlign: 'right', + align: 'right', + width: 100 }, { field: 'lat', headerName: 'Lat', - flex: 1 + headerAlign: 'right', + align: 'right', + width: 100 }, { field: 'long', headerName: 'Long', - flex: 1 + headerAlign: 'right', + align: 'right', + width: 100 } ]; + // Set height so we the skeleton loader will match table rows + const RowHeight = 52; + + // Skeleton Loader template + const SkeletonRow = () => ( + + + + + + + + + + + + ) + return ( <> - {props.isLoading && } + {!props.isLoading && ( + + + + + + )} {!props.isLoading && props.data.length === 0 && ( @@ -128,6 +181,7 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT {!props.isLoading && props.data.length > 0 && ( <> row.id} diff --git a/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx b/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx index b5b6a016ff..3c108e2bc5 100644 --- a/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx @@ -6,6 +6,9 @@ import dayjs from 'dayjs'; import { useContext, useMemo } from 'react'; import NoSurveySectionData from '../components/NoSurveySectionData'; import { ICritterDeployment } from '../telemetry/ManualTelemetryList'; +import Stack from '@mui/material/Stack'; +import Skeleton from '@mui/material/Skeleton'; +import grey from '@mui/material/colors/grey'; interface ITelemetryData { id: number; critter_id: string | null; @@ -39,6 +42,7 @@ const SurveySpatialTelemetryDataTable = (props: ISurveySpatialTelemetryDataTable start: dayjs(item.deployment.attachment_start).format('YYYY-MM-DD'), end: item.deployment.attachment_end ? dayjs(item.deployment.attachment_end).format('YYYY-MM-DD') : 'Still Active' })); + const columns: GridColDef[] = [ { field: 'critter_id', @@ -61,8 +65,57 @@ const SurveySpatialTelemetryDataTable = (props: ISurveySpatialTelemetryDataTable flex: 1 } ]; + + // Set height so we the skeleton loader will match table rows + const RowHeight = 52; + + // Skeleton Loader template + const SkeletonRow = () => ( + + + + + + + + + + + + ) + return ( <> + {!props.isLoading && ( + + + + + + )} + + {!props.isLoading && flattenedCritterDeployments.length === 0 && ( + + )} + {flattenedCritterDeployments.length > 0 && !props.isLoading && ( )} - - {props.isLoading && } - - {!props.isLoading && flattenedCritterDeployments.length === 0 && ( - - )} ); }; From f3892a587f001fc7b312009b342b4f281a1fe639 Mon Sep 17 00:00:00 2001 From: jeznorth Date: Wed, 24 Jan 2024 10:41:31 -0800 Subject: [PATCH 37/85] Removing reference --- .../features/surveys/view/SurveySpatialTelemetryDataTable.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx b/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx index 3c108e2bc5..4058553794 100644 --- a/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx @@ -1,6 +1,5 @@ import { GridColDef } from '@mui/x-data-grid'; import { StyledDataGrid } from 'components/data-grid/StyledDataGrid'; -import { SkeletonList } from 'components/loading/SkeletonLoaders'; import { SurveyContext } from 'contexts/surveyContext'; import dayjs from 'dayjs'; import { useContext, useMemo } from 'react'; From b998100455ccbd9524a15be032075b55c728b9b8 Mon Sep 17 00:00:00 2001 From: jeznorth Date: Wed, 24 Jan 2024 10:42:27 -0800 Subject: [PATCH 38/85] Fixing small issue --- .../features/surveys/view/SurveySpatialObservationDataTable.tsx | 2 +- .../features/surveys/view/SurveySpatialTelemetryDataTable.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx index b3a1c850c2..911ff30df5 100644 --- a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx @@ -166,7 +166,7 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT return ( <> - {!props.isLoading && ( + {props.isLoading && ( diff --git a/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx b/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx index 4058553794..c7704b0e1e 100644 --- a/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx @@ -103,7 +103,7 @@ const SurveySpatialTelemetryDataTable = (props: ISurveySpatialTelemetryDataTable return ( <> - {!props.isLoading && ( + {props.isLoading && ( From 0a21adb2ef05c656f2202a4ed24aa6d63cd02366 Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Wed, 24 Jan 2024 11:04:31 -0800 Subject: [PATCH 39/85] SIMSBIOHUB-445: Added column sorting to API pagination --- .../survey/{surveyId}/observations/index.ts | 11 ++++++++++- api/src/repositories/observation-repository.ts | 11 +++++++++-- api/src/zod-schema/pagination.ts | 13 ++++++++++--- app/src/hooks/api/useObservationApi.ts | 10 +++++++++- app/src/types/misc.ts | 7 +++++++ 5 files changed, 45 insertions(+), 7 deletions(-) diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts index e13e6b471f..9c9e9766b7 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts @@ -155,6 +155,13 @@ const paginationSchema: SchemaObject = { last_page: { type: 'integer', minimum: 1, + }, + sort: { + type: 'string', + }, + order: { + type: 'string', + enum: ['ASC', 'DESC'] } } } @@ -338,6 +345,8 @@ export function getSurveyObservations(): RequestHandler { const surveyId = Number(req.params.surveyId); const page: number | undefined = req.query.page ? Number(req.query.page) : undefined; const limit: number | undefined = req.query.limit ? Number(req.query.limit) : undefined; + const sort: string | undefined = req.query.sort ? String(req.query.sort) : undefined; + const order: 'ASC' | 'DESC' | undefined = req.query.order ? String(req.query.order) as 'ASC' | 'DESC' : undefined; defaultLog.debug({ label: 'getSurveyObservations', surveyId }); @@ -348,7 +357,7 @@ export function getSurveyObservations(): RequestHandler { const observationService = new ObservationService(connection); - const paginationOptions: ApiPaginationOptions | undefined = (limit && page) ? { limit, page } : undefined + const paginationOptions: ApiPaginationOptions | undefined = (limit && page) ? { limit, page, sort, order } : undefined const observationData = await observationService.getSurveyObservationsWithSupplementaryData(surveyId, paginationOptions); const { observationCount } = observationData.supplementaryObservationData diff --git a/api/src/repositories/observation-repository.ts b/api/src/repositories/observation-repository.ts index 674fe85a28..d7781cdd93 100644 --- a/api/src/repositories/observation-repository.ts +++ b/api/src/repositories/observation-repository.ts @@ -210,10 +210,17 @@ export class ObservationRepository extends BaseRepository { const allRowsQuery = knex.queryBuilder().select('*').from('survey_observation').where('survey_id', surveyId) const query = pagination - ? allRowsQuery.limit(pagination.limit).offset((pagination.page - 1) * pagination.limit) + ? allRowsQuery + .limit(pagination.limit) + .offset((pagination.page - 1) * pagination.limit) : allRowsQuery - const response = await this.connection.knex(query, ObservationRecord); + // TODO possible to conditionally chain these methods together, rather than redeclare the query builder? + const query2 = pagination?.sort && pagination.order + ? query.orderBy(pagination.sort, pagination.order) + : query; + + const response = await this.connection.knex(query2, ObservationRecord); return response.rows; } diff --git a/api/src/zod-schema/pagination.ts b/api/src/zod-schema/pagination.ts index d378a45c0a..287e3dd28d 100644 --- a/api/src/zod-schema/pagination.ts +++ b/api/src/zod-schema/pagination.ts @@ -1,9 +1,16 @@ import { z } from "zod"; +export const ApiPaginationSorting = z.object({ + sort: z.string().optional(), + order: z.enum(['ASC', 'DESC']).optional() +}); + +export type ApiPaginationSorting = z.infer; + /** * Object used to make paginated requests */ -export const ApiPaginationOptions = z.object({ +export const ApiPaginationOptions = ApiPaginationSorting.extend({ limit: z.number(), page: z.number(), }); @@ -13,11 +20,11 @@ export type ApiPaginationOptions = z.infer; /** * Object used to represent results from paginated queries */ -export const ApiPaginationResults = z.object({ +export const ApiPaginationResults = ApiPaginationSorting.extend({ total: z.number(), per_page: z.number(), current_page: z.number(), last_page: z.number() }); -export type ApiPaginationResults = z.infer; \ No newline at end of file +export type ApiPaginationResults = z.infer; \ No newline at end of file diff --git a/app/src/hooks/api/useObservationApi.ts b/app/src/hooks/api/useObservationApi.ts index a32a19d941..0f5a4c1321 100644 --- a/app/src/hooks/api/useObservationApi.ts +++ b/app/src/hooks/api/useObservationApi.ts @@ -5,6 +5,7 @@ import { ISupplementaryObservationData } from 'contexts/observationsTableContext'; import { IGetSurveyObservationsResponse } from 'interfaces/useObservationApi.interface'; +import { ApiPaginationOptions } from 'types/misc'; /** * Returns a set of supported api methods for working with observations. @@ -39,12 +40,13 @@ const useObservationApi = (axios: AxiosInstance) => { * * @param {number} projectId * @param {number} surveyId + * @param {ApiPaginationOptions} [pagination] * @return {*} {Promise} */ const getObservationRecords = async ( projectId: number, surveyId: number, - pagination?: { page: number, limit: number } + pagination?: ApiPaginationOptions ): Promise => { let urlParamsString = ''; @@ -52,6 +54,12 @@ const useObservationApi = (axios: AxiosInstance) => { const params = new URLSearchParams(); params.append('page', pagination.page.toString()); params.append('limit', pagination.limit.toString()); + if (pagination.sort) { + params.append('sort', pagination.sort); + } + if (pagination.order) { + params.append('order', pagination.order); + } urlParamsString = `?${params.toString()}`; } diff --git a/app/src/types/misc.ts b/app/src/types/misc.ts index aeb34dee36..1f3259e074 100644 --- a/app/src/types/misc.ts +++ b/app/src/types/misc.ts @@ -1 +1,8 @@ export type StringBoolean = 'true' | 'false'; + +export type ApiPaginationOptions = { + page: number; + limit: number; + sort?: string; + order?: 'ASC' | 'DESC'; +} \ No newline at end of file From d7dffc49546e543ef6ac885ea1295ad9e48e9300 Mon Sep 17 00:00:00 2001 From: jeznorth Date: Wed, 24 Jan 2024 11:07:27 -0800 Subject: [PATCH 40/85] Updated Styling --- app/src/components/map/styles/MapBase.scss | 5 +++++ app/src/features/surveys/view/SurveyMap.tsx | 1 - .../view/components/SurveyMapToolBar.tsx | 18 ++++++++++++++++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/app/src/components/map/styles/MapBase.scss b/app/src/components/map/styles/MapBase.scss index 44deb7377a..7bc93bbcc5 100644 --- a/app/src/components/map/styles/MapBase.scss +++ b/app/src/components/map/styles/MapBase.scss @@ -7,3 +7,8 @@ path.leaflet-interactive:focus { outline: none; } + +.leaflet-left .leaflet-control { + margin-top: 16px; + margin-left: 16px; +} \ No newline at end of file diff --git a/app/src/features/surveys/view/SurveyMap.tsx b/app/src/features/surveys/view/SurveyMap.tsx index e12a7edef1..015866ac7c 100644 --- a/app/src/features/surveys/view/SurveyMap.tsx +++ b/app/src/features/surveys/view/SurveyMap.tsx @@ -30,7 +30,6 @@ const SurveyMap = (props: ISurveyMapProps) => { center={MAP_DEFAULT_CENTER} scrollWheelZoom={false} fullscreenControl={true} - // style={{ height: '100%', width: '800px' }} style={{ height: '100%' }}> diff --git a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx index 0b2a61c329..0a9206eb61 100644 --- a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx +++ b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx @@ -74,7 +74,21 @@ const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { transformOrigin={{ vertical: 'top', horizontal: 'right' - }}> + }} + sx={{ + '& a': { + display: 'flex', + px: 2, + py: '6px', + textDecoration: 'none', + color: 'text.primary', + borderRadius: 0, + '&:focus': { + outline: 'none' + } + } + }} + > @@ -112,7 +126,7 @@ const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { - + Date: Wed, 24 Jan 2024 11:54:22 -0800 Subject: [PATCH 41/85] updated api to account for first page --- .../survey/{surveyId}/observations/index.ts | 21 ++++--- .../repositories/observation-repository.ts | 14 ++--- .../surveys/view/SurveySpatialData.tsx | 1 - .../SurveySpatialObservationDataTable.tsx | 56 +++++++++++++++---- 4 files changed, 61 insertions(+), 31 deletions(-) diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts index 9c9e9766b7..5c9974bcba 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts @@ -149,22 +149,21 @@ const paginationSchema: SchemaObject = { minimum: 1 }, current_page: { - type: 'integer', - minimum: 1, + type: 'integer' }, last_page: { type: 'integer', - minimum: 1, + minimum: 1 }, sort: { - type: 'string', + type: 'string' }, order: { type: 'string', enum: ['ASC', 'DESC'] } } -} +}; GET.apiDoc = { description: 'Get all observations for the survey.', @@ -346,7 +345,7 @@ export function getSurveyObservations(): RequestHandler { const page: number | undefined = req.query.page ? Number(req.query.page) : undefined; const limit: number | undefined = req.query.limit ? Number(req.query.limit) : undefined; const sort: string | undefined = req.query.sort ? String(req.query.sort) : undefined; - const order: 'ASC' | 'DESC' | undefined = req.query.order ? String(req.query.order) as 'ASC' | 'DESC' : undefined; + const order: 'ASC' | 'DESC' | undefined = req.query.order ? (String(req.query.order) as 'ASC' | 'DESC') : undefined; defaultLog.debug({ label: 'getSurveyObservations', surveyId }); @@ -357,9 +356,13 @@ export function getSurveyObservations(): RequestHandler { const observationService = new ObservationService(connection); - const paginationOptions: ApiPaginationOptions | undefined = (limit && page) ? { limit, page, sort, order } : undefined - const observationData = await observationService.getSurveyObservationsWithSupplementaryData(surveyId, paginationOptions); - const { observationCount } = observationData.supplementaryObservationData + const paginationOptions: ApiPaginationOptions | undefined = + limit !== undefined && page !== undefined ? { limit, page, sort, order } : undefined; + const observationData = await observationService.getSurveyObservationsWithSupplementaryData( + surveyId, + paginationOptions + ); + const { observationCount } = observationData.supplementaryObservationData; return res.status(200).json({ ...observationData, diff --git a/api/src/repositories/observation-repository.ts b/api/src/repositories/observation-repository.ts index d7781cdd93..afff43b028 100644 --- a/api/src/repositories/observation-repository.ts +++ b/api/src/repositories/observation-repository.ts @@ -3,8 +3,8 @@ import { z } from 'zod'; import { getKnex } from '../database/db'; import { ApiExecuteSQLError } from '../errors/api-error'; import { getLogger } from '../utils/logger'; -import { BaseRepository } from './base-repository'; import { ApiPaginationOptions } from '../zod-schema/pagination'; +import { BaseRepository } from './base-repository'; /** * Interface reflecting survey observations retrieved from the database @@ -207,18 +207,14 @@ export class ObservationRepository extends BaseRepository { */ async getSurveyObservations(surveyId: number, pagination?: ApiPaginationOptions): Promise { const knex = getKnex(); - const allRowsQuery = knex.queryBuilder().select('*').from('survey_observation').where('survey_id', surveyId) + const allRowsQuery = knex.queryBuilder().select('*').from('survey_observation').where('survey_id', surveyId); const query = pagination - ? allRowsQuery - .limit(pagination.limit) - .offset((pagination.page - 1) * pagination.limit) - : allRowsQuery + ? allRowsQuery.limit(pagination.limit).offset(pagination.page * pagination.limit) + : allRowsQuery; // TODO possible to conditionally chain these methods together, rather than redeclare the query builder? - const query2 = pagination?.sort && pagination.order - ? query.orderBy(pagination.sort, pagination.order) - : query; + const query2 = pagination?.sort && pagination.order ? query.orderBy(pagination.sort, pagination.order) : query; const response = await this.connection.knex(query2, ObservationRecord); return response.rows; diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 48ea9e1930..d292c89304 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -176,7 +176,6 @@ const SurveySpatialData = () => { {currentTab === SurveySpatialDataSet.OBSERVATIONS && ( diff --git a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx index 911ff30df5..b6b00a6968 100644 --- a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx @@ -1,3 +1,6 @@ +import grey from '@mui/material/colors/grey'; +import Skeleton from '@mui/material/Skeleton'; +import Stack from '@mui/material/Stack'; import { GridColDef } from '@mui/x-data-grid'; import { StyledDataGrid } from 'components/data-grid/StyledDataGrid'; import { CodesContext } from 'contexts/codesContext'; @@ -5,13 +8,12 @@ import { IObservationRecord } from 'contexts/observationsTableContext'; import { SurveyContext } from 'contexts/surveyContext'; import { TaxonomyContext } from 'contexts/taxonomyContext'; import dayjs from 'dayjs'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import useDataLoader from 'hooks/useDataLoader'; import { IGetSampleLocationRecord } from 'interfaces/useSurveyApi.interface'; -import { useContext, useMemo } from 'react'; +import { useContext, useEffect, useMemo, useState } from 'react'; import { getCodesName } from 'utils/Utils'; import NoSurveySectionData from '../components/NoSurveySectionData'; -import Stack from '@mui/material/Stack'; -import Skeleton from '@mui/material/Skeleton'; -import grey from '@mui/material/colors/grey'; interface IObservationTableRow { id: number; @@ -26,15 +28,39 @@ interface IObservationTableRow { long: number | null; } interface ISurveySpatialObservationDataTableProps { - data: IObservationRecord[]; sample_sites: IGetSampleLocationRecord[]; isLoading: boolean; } const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataTableProps) => { + const biohubApi = useBiohubApi(); const surveyContext = useContext(SurveyContext); const codesContext = useContext(CodesContext); const taxonomyContext = useContext(TaxonomyContext); + const [data, setData] = useState([]); + const [totalRows, setTotalRows] = useState(0); + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState(5); + + const paginatedDataLoader = useDataLoader((page: number, limit: number) => + biohubApi.observation.getObservationRecords(surveyContext.projectId, surveyContext.surveyId, { + page, + limit + }) + ); + + // page information has changed, fetch more data + useEffect(() => { + paginatedDataLoader.refresh(page, pageSize); + }, [page, pageSize]); + + useEffect(() => { + if (paginatedDataLoader.data) { + setData(paginatedDataLoader.data.surveyObservations); + setTotalRows(paginatedDataLoader.data.pagination.total); + } + }, [paginatedDataLoader.data]); + const sampleSites = useMemo(() => { return surveyContext.sampleSiteDataLoader.data?.sampleSites ?? []; }, [surveyContext.sampleSiteDataLoader.data]); @@ -51,7 +77,7 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT ); }, [surveyContext.sampleSiteDataLoader.data]); - const tableData: IObservationTableRow[] = props.data.map((item) => { + const tableData: IObservationTableRow[] = data.map((item) => { const siteName = sampleSites.find((site) => site.survey_sample_site_id === item.survey_sample_site_id)?.name; const method_id = sampleMethods.find( (method) => method?.survey_sample_method_id === item.survey_sample_method_id @@ -162,7 +188,7 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT - ) + ); return ( <> @@ -174,23 +200,29 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT )} - {!props.isLoading && props.data.length === 0 && ( + {!props.isLoading && data.length === 0 && ( )} - {!props.isLoading && props.data.length > 0 && ( + {!props.isLoading && data.length > 0 && ( <> { + setPage(model.page); + setPageSize(model.pageSize); + }} + pageSizeOptions={[5]} + paginationMode="server" + loading={paginatedDataLoader.isLoading} getRowId={(row) => row.id} columns={columns} - pageSizeOptions={[5]} rowSelection={false} checkboxSelection={false} - hideFooter - disableRowSelectionOnClick disableColumnSelector disableColumnFilter disableColumnMenu From 05af61db84261a95e44c0577279ccc9e80bafd5e Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 24 Jan 2024 13:44:22 -0800 Subject: [PATCH 42/85] modified for server side sorting --- .../SurveySpatialObservationDataTable.tsx | 23 +++++++++++++++---- app/src/types/misc.ts | 10 ++++---- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx index b6b00a6968..a3210d3432 100644 --- a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx @@ -1,7 +1,7 @@ import grey from '@mui/material/colors/grey'; import Skeleton from '@mui/material/Skeleton'; import Stack from '@mui/material/Stack'; -import { GridColDef } from '@mui/x-data-grid'; +import { GridColDef, GridSortModel } from '@mui/x-data-grid'; import { StyledDataGrid } from 'components/data-grid/StyledDataGrid'; import { CodesContext } from 'contexts/codesContext'; import { IObservationRecord } from 'contexts/observationsTableContext'; @@ -41,18 +41,28 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT const [totalRows, setTotalRows] = useState(0); const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState(5); + const [sortModel, setSortModel] = useState([]); - const paginatedDataLoader = useDataLoader((page: number, limit: number) => + const paginatedDataLoader = useDataLoader((page: number, limit: number, sort?: string, order?: 'asc' | 'desc') => biohubApi.observation.getObservationRecords(surveyContext.projectId, surveyContext.surveyId, { page, - limit + limit, + sort, + order }) ); // page information has changed, fetch more data useEffect(() => { - paginatedDataLoader.refresh(page, pageSize); - }, [page, pageSize]); + if (sortModel.length > 0) { + if (sortModel[0].sort) { + console.log(`Table Sort: ${sortModel[0].field} ${sortModel[0].sort}`); + paginatedDataLoader.refresh(page, pageSize, sortModel[0].field, sortModel[0].sort); + } + } else { + paginatedDataLoader.refresh(page, pageSize); + } + }, [page, pageSize, sortModel]); useEffect(() => { if (paginatedDataLoader.data) { @@ -218,6 +228,9 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT }} pageSizeOptions={[5]} paginationMode="server" + sortingMode="server" + sortModel={sortModel} + onSortModelChange={(model) => setSortModel(model)} loading={paginatedDataLoader.isLoading} getRowId={(row) => row.id} columns={columns} diff --git a/app/src/types/misc.ts b/app/src/types/misc.ts index 1f3259e074..43941baba5 100644 --- a/app/src/types/misc.ts +++ b/app/src/types/misc.ts @@ -1,8 +1,8 @@ export type StringBoolean = 'true' | 'false'; export type ApiPaginationOptions = { - page: number; - limit: number; - sort?: string; - order?: 'ASC' | 'DESC'; -} \ No newline at end of file + page: number; + limit: number; + sort?: string; + order?: 'asc' | 'desc'; +}; From d880fe2dadb49331032acbd7a006d0bfd904b478 Mon Sep 17 00:00:00 2001 From: jeznorth Date: Wed, 24 Jan 2024 13:57:22 -0800 Subject: [PATCH 43/85] Update spatial data tables --- .../surveys/view/SurveySpatialData.tsx | 2 +- .../SurveySpatialObservationDataTable.tsx | 84 +++++++++---------- .../view/SurveySpatialTelemetryDataTable.tsx | 21 ++--- 3 files changed, 49 insertions(+), 58 deletions(-) diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index d292c89304..42f8e4e3e0 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -173,7 +173,7 @@ const SurveySpatialData = () => { - + {currentTab === SurveySpatialDataSet.OBSERVATIONS && ( ( @@ -192,45 +195,36 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT return ( <> - {props.isLoading && ( + {props.isLoading ? ( - )} - - {!props.isLoading && data.length === 0 && ( - - )} - - {!props.isLoading && data.length > 0 && ( - <> - { - setPage(model.page); - setPageSize(model.pageSize); - }} - pageSizeOptions={[5]} - paginationMode="server" - loading={paginatedDataLoader.isLoading} - getRowId={(row) => row.id} - columns={columns} - rowSelection={false} - checkboxSelection={false} - disableColumnSelector - disableColumnFilter - disableColumnMenu - disableVirtualization - sortingOrder={['asc', 'desc']} - data-testid="survey-spatial-observation-data-table" - /> - + ) : ( + { + setPage(model.page); + setPageSize(model.pageSize); + }} + pageSizeOptions={[5]} + paginationMode="server" + loading={paginatedDataLoader.isLoading} + getRowId={(row) => row.id} + columns={columns} + rowSelection={false} + checkboxSelection={false} + disableColumnSelector + disableColumnFilter + disableColumnMenu + disableVirtualization + sortingOrder={['asc', 'desc']} + data-testid="survey-spatial-observation-data-table" + /> )} ); diff --git a/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx b/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx index c7704b0e1e..89d02fe074 100644 --- a/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx @@ -3,7 +3,6 @@ import { StyledDataGrid } from 'components/data-grid/StyledDataGrid'; import { SurveyContext } from 'contexts/surveyContext'; import dayjs from 'dayjs'; import { useContext, useMemo } from 'react'; -import NoSurveySectionData from '../components/NoSurveySectionData'; import { ICritterDeployment } from '../telemetry/ManualTelemetryList'; import Stack from '@mui/material/Stack'; import Skeleton from '@mui/material/Skeleton'; @@ -66,7 +65,7 @@ const SurveySpatialTelemetryDataTable = (props: ISurveySpatialTelemetryDataTable ]; // Set height so we the skeleton loader will match table rows - const RowHeight = 52; + const RowHeight = 40; // Skeleton Loader template const SkeletonRow = () => ( @@ -103,28 +102,26 @@ const SurveySpatialTelemetryDataTable = (props: ISurveySpatialTelemetryDataTable return ( <> - {props.isLoading && ( + {props.isLoading ? ( - )} - - {!props.isLoading && flattenedCritterDeployments.length === 0 && ( - - )} - - {flattenedCritterDeployments.length > 0 && !props.isLoading && ( + ) : ( row.id} columns={columns} + initialState={{ + pagination: { + paginationModel: { page: 1, pageSize: 5 }, + }, + }} pageSizeOptions={[5]} rowSelection={false} checkboxSelection={false} - hideFooter disableRowSelectionOnClick disableColumnSelector disableColumnFilter From 7cf9c4cb3cf6c916af9640ef390ed4c46c00ebe9 Mon Sep 17 00:00:00 2001 From: jeznorth Date: Wed, 24 Jan 2024 14:05:16 -0800 Subject: [PATCH 44/85] Fix small code issue --- .../surveys/view/SurveySpatialObservationDataTable.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx index 208bb514c9..9d48fd926c 100644 --- a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx @@ -223,6 +223,9 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT }} pageSizeOptions={[5]} paginationMode="server" + sortingMode="server" + sortModel={sortModel} + onSortModelChange={(model) => setSortModel(model)} loading={paginatedDataLoader.isLoading} getRowId={(row) => row.id} columns={columns} @@ -236,11 +239,8 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT data-testid="survey-spatial-observation-data-table" /> )} - sortingMode="server" - sortModel={sortModel} - onSortModelChange={(model) => setSortModel(model)} - ); + ) }; export default SurveySpatialObservationDataTable; From e02c84d5e1b17b59809b76d96573f619fd66e765 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 24 Jan 2024 14:56:24 -0800 Subject: [PATCH 45/85] added migration --- .../SurveySpatialObservationDataTable.tsx | 16 ++++++++----- ...40124141800_observation_scientific_name.ts | 24 +++++++++++++++++++ 2 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 database/src/migrations/20240124141800_observation_scientific_name.ts diff --git a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx index 9d48fd926c..4015c4cd03 100644 --- a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx @@ -23,8 +23,8 @@ interface IObservationTableRow { period: string | undefined; date: string | undefined; time: string | undefined; - lat: number | null; - long: number | null; + latitude: number | null; + longitude: number | null; } interface ISurveySpatialObservationDataTableProps { sample_sites: IGetSampleLocationRecord[]; @@ -92,6 +92,10 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT (method) => method?.survey_sample_method_id === item.survey_sample_method_id )?.method_lookup_id; const period = samplePeriods.find((period) => period?.survey_sample_period_id === item.survey_sample_period_id); + let periodString = ''; + if (period) { + periodString = `${period.start_date} ${period.start_time ?? ''} - ${period.end_date} ${period.end_time ?? ''}`; + } return { id: item.survey_observation_id, @@ -99,11 +103,11 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT count: item.count, site: siteName, method: method_id ? getCodesName(codesContext.codesDataLoader.data, 'sample_methods', method_id) : '', - period: `${period?.start_date} ${period?.end_date}`, + period: periodString, date: dayjs(item.observation_date).format('YYYY-MM-DD'), time: dayjs(item.observation_date).format('HH:mm:ss'), - lat: item.latitude, - long: item.longitude + latitude: item.latitude, + longitude: item.longitude }; }); @@ -240,7 +244,7 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT /> )} - ) + ); }; export default SurveySpatialObservationDataTable; diff --git a/database/src/migrations/20240124141800_observation_scientific_name.ts b/database/src/migrations/20240124141800_observation_scientific_name.ts new file mode 100644 index 0000000000..4a82f288fc --- /dev/null +++ b/database/src/migrations/20240124141800_observation_scientific_name.ts @@ -0,0 +1,24 @@ +/* +itis_tsn integer NOT NULL, + itis_scientific_name varchar(300) NOT NULL, + */ + +import { Knex } from 'knex'; + +/** + * Adds itis_scientific_name and itis_tsn id to survery_observations table + * + * @export + * @param {Knex} knex + * @return {*} {Promise} + */ +export async function up(knex: Knex): Promise { + await knex.raw(`--sql + ALTER TABLE survey_observation ADD itis_tsn integer; + ALTER TABLE survey_observation ADD itis_scientific_name varchar(300); + `); +} + +export async function down(knex: Knex): Promise { + await knex.raw(``); +} From 28bb6f76bc56f698cad0d8b54872a47b939917dc Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 24 Jan 2024 15:09:13 -0800 Subject: [PATCH 46/85] fixed migration --- .../20240124141800_observation_scientific_name.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/database/src/migrations/20240124141800_observation_scientific_name.ts b/database/src/migrations/20240124141800_observation_scientific_name.ts index 4a82f288fc..5409f555ce 100644 --- a/database/src/migrations/20240124141800_observation_scientific_name.ts +++ b/database/src/migrations/20240124141800_observation_scientific_name.ts @@ -1,8 +1,3 @@ -/* -itis_tsn integer NOT NULL, - itis_scientific_name varchar(300) NOT NULL, - */ - import { Knex } from 'knex'; /** @@ -14,6 +9,8 @@ import { Knex } from 'knex'; */ export async function up(knex: Knex): Promise { await knex.raw(`--sql + SET search_path = 'biohub'; + ALTER TABLE survey_observation ADD itis_tsn integer; ALTER TABLE survey_observation ADD itis_scientific_name varchar(300); `); From 6db11cef6f924169e5f0d899f4a145a1371e3024 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 24 Jan 2024 15:42:27 -0800 Subject: [PATCH 47/85] updated some column names --- .../surveys/view/SurveySpatialData.tsx | 2 +- .../SurveySpatialObservationDataTable.tsx | 28 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 42f8e4e3e0..1639cd752d 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -33,7 +33,7 @@ const SurveySpatialData = () => { useEffect(() => { if (surveyContext.deploymentDataLoader.data) { const deploymentIds = surveyContext.deploymentDataLoader.data.map((item) => item.deployment_id); - telemetryContext.telemetryDataLoader.load(deploymentIds); + telemetryContext.telemetryDataLoader.refresh(deploymentIds); } }, [surveyContext.deploymentDataLoader.data]); diff --git a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx index 4015c4cd03..1f81997e06 100644 --- a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx @@ -16,11 +16,11 @@ import { getCodesName } from 'utils/Utils'; interface IObservationTableRow { id: number; - taxon: string | undefined; + itis_scientific_name: string | undefined; count: number | null; - site: string | undefined; - method: string | undefined; - period: string | undefined; + sample_name: string | undefined; + sample_method: string | undefined; + sample_period: string | undefined; date: string | undefined; time: string | undefined; latitude: number | null; @@ -99,11 +99,11 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT return { id: item.survey_observation_id, - taxon: taxonomyContext.getCachedSpeciesTaxonomyById(item.wldtaxonomic_units_id)?.label, + itis_scientific_name: taxonomyContext.getCachedSpeciesTaxonomyById(item.wldtaxonomic_units_id)?.label, count: item.count, - site: siteName, - method: method_id ? getCodesName(codesContext.codesDataLoader.data, 'sample_methods', method_id) : '', - period: periodString, + sample_name: siteName, + sample_method: method_id ? getCodesName(codesContext.codesDataLoader.data, 'sample_methods', method_id) : '', + sample_period: periodString, date: dayjs(item.observation_date).format('YYYY-MM-DD'), time: dayjs(item.observation_date).format('HH:mm:ss'), latitude: item.latitude, @@ -113,25 +113,25 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT const columns: GridColDef[] = [ { - field: 'taxon', + field: 'itis_scientific_name', headerName: 'Species', flex: 1, minWidth: 200 }, { - field: 'site', + field: 'sample_site', headerName: 'Sample Site', flex: 1, minWidth: 200 }, { - field: 'method', + field: 'sample_method', headerName: 'Sample Method', flex: 1, minWidth: 200 }, { - field: 'period', + field: 'sample_period', headerName: 'Sample Period', flex: 1, minWidth: 200 @@ -156,14 +156,14 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT maxWidth: 100 }, { - field: 'lat', + field: 'latitude', headerName: 'Lat', headerAlign: 'right', align: 'right', maxWidth: 100 }, { - field: 'long', + field: 'longitude', headerName: 'Long', headerAlign: 'right', align: 'right', From a26c86c14f221844a008fb83a06d695680f4a385 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 24 Jan 2024 16:09:00 -0800 Subject: [PATCH 48/85] map auto bounds map on tab select --- app/src/features/surveys/view/SurveyMap.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/features/surveys/view/SurveyMap.tsx b/app/src/features/surveys/view/SurveyMap.tsx index 015866ac7c..27d42599a5 100644 --- a/app/src/features/surveys/view/SurveyMap.tsx +++ b/app/src/features/surveys/view/SurveyMap.tsx @@ -5,7 +5,6 @@ import FullScreenScrollingEventHandler from 'components/map/components/FullScree import { MapBaseCss } from 'components/map/styles/MapBaseCss'; import { ALL_OF_BC_BOUNDARY, MAP_DEFAULT_CENTER } from 'constants/spatial'; import { LatLngBoundsExpression } from 'leaflet'; -import { useState } from 'react'; import { GeoJSON, LayersControl, MapContainer as LeafletMapContainer } from 'react-leaflet'; import { calculateUpdatedMapBounds } from 'utils/mapBoundaryUploadHelpers'; import { coloredPoint, INonEditableGeometries } from 'utils/mapUtils'; @@ -17,8 +16,12 @@ interface ISurveyMapProps { } // TODO: need a way to pass in the map dimensions depending on the screen size const SurveyMap = (props: ISurveyMapProps) => { - //TODO: This needs to reset bounds when the data is updated - const [bounds] = useState(calculateUpdatedMapBounds([ALL_OF_BC_BOUNDARY])); + let bounds: LatLngBoundsExpression | undefined; + if (props.mapPoints.length > 0) { + bounds = calculateUpdatedMapBounds(props.mapPoints.map((item) => item.feature)); + } else { + bounds = calculateUpdatedMapBounds([ALL_OF_BC_BOUNDARY]); + } return ( <> {props.isLoading ? ( From 401a62e802e36fb59132bf5ae8dcf5884378455e Mon Sep 17 00:00:00 2001 From: jeznorth Date: Thu, 25 Jan 2024 10:10:31 -0800 Subject: [PATCH 49/85] UI Updates --- .../components/data-grid/StyledDataGrid.tsx | 72 +++++++++---------- .../data-grid/StyledDataGridOverlay.tsx | 12 ++++ .../surveys/view/SurveySpatialData.tsx | 4 +- .../SurveySpatialObservationDataTable.tsx | 6 +- .../view/SurveySpatialTelemetryDataTable.tsx | 3 +- 5 files changed, 55 insertions(+), 42 deletions(-) create mode 100644 app/src/components/data-grid/StyledDataGridOverlay.tsx diff --git a/app/src/components/data-grid/StyledDataGrid.tsx b/app/src/components/data-grid/StyledDataGrid.tsx index f90bfc8a63..2aab998e19 100644 --- a/app/src/components/data-grid/StyledDataGrid.tsx +++ b/app/src/components/data-grid/StyledDataGrid.tsx @@ -1,41 +1,39 @@ import { grey } from '@mui/material/colors'; -import { makeStyles } from '@mui/styles'; import { DataGrid, DataGridProps } from '@mui/x-data-grid'; -import NoRowsOverlay from 'features/funding-sources/list/FundingSourcesTableNoRowsOverlay'; -import { useCallback } from 'react'; -const useStyles = makeStyles(() => ({ - projectsTable: { - tableLayout: 'fixed' - }, - linkButton: { - textAlign: 'left', - fontWeight: 700 - }, - noDataText: { - fontFamily: 'inherit !important', - fontSize: '0.875rem', - fontWeight: 700 - }, - dataGrid: { - border: 'none !important', - fontFamily: 'inherit !important', - '& .MuiDataGrid-columnHeaderTitle': { - textTransform: 'uppercase', - fontSize: '0.875rem', - fontWeight: 700, - color: grey[600] - }, - '& .MuiDataGrid-cell:focus-within, & .MuiDataGrid-cellCheckbox:focus-within, & .MuiDataGrid-columnHeader:focus-within': - { - outline: 'none !important' - }, - '& .MuiDataGrid-row:hover': { - backgroundColor: 'transparent !important' - } - } -})); +import StyledDataGridOverlay from './StyledDataGridOverlay'; +import Box from '@mui/material/Box'; + +const StyledLoadingOverlay = () => ( + +); + export const StyledDataGrid = (props: DataGridProps) => { - const classes = useStyles(); - const NoRowsOverlayStyled = useCallback(() => , [classes.noDataText]); - return ; + return ( + + ); }; diff --git a/app/src/components/data-grid/StyledDataGridOverlay.tsx b/app/src/components/data-grid/StyledDataGridOverlay.tsx new file mode 100644 index 0000000000..5731351f01 --- /dev/null +++ b/app/src/components/data-grid/StyledDataGridOverlay.tsx @@ -0,0 +1,12 @@ +import Typography from '@mui/material/Typography'; +import { GridOverlay } from '@mui/x-data-grid'; + +const StyledDataGridOverlay = () => ( + + + No records found + + +); + +export default StyledDataGridOverlay; \ No newline at end of file diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 1639cd752d..9080cf9f7f 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -170,10 +170,10 @@ const SurveySpatialData = () => { updateLayout={updateLayout} /> - + - + {currentTab === SurveySpatialDataSet.OBSERVATIONS && ( ( @@ -180,7 +180,8 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT flexDirection="row" alignItems="center" gap={2} - p={2} + py={2} + px={1} height={RowHeight} overflow="hidden" sx={{ @@ -218,6 +219,7 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT ) : ( ( @@ -111,6 +111,7 @@ const SurveySpatialTelemetryDataTable = (props: ISurveySpatialTelemetryDataTable ) : ( row.id} columns={columns} From 542fcfdab8ad316cf156017e57478c64b0ce5dbe Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 25 Jan 2024 10:29:50 -0800 Subject: [PATCH 50/85] added custom message to no rows overlay for styled data grid --- app/src/components/data-grid/StyledDataGrid.tsx | 14 +++++++++----- .../components/data-grid/StyledDataGridOverlay.tsx | 6 +++--- .../funding-sources/list/FundingSourcesTable.tsx | 1 + .../view/SurveySpatialObservationDataTable.tsx | 1 + .../view/SurveySpatialTelemetryDataTable.tsx | 13 +++++++------ 5 files changed, 21 insertions(+), 14 deletions(-) diff --git a/app/src/components/data-grid/StyledDataGrid.tsx b/app/src/components/data-grid/StyledDataGrid.tsx index 2aab998e19..95b378a4e1 100644 --- a/app/src/components/data-grid/StyledDataGrid.tsx +++ b/app/src/components/data-grid/StyledDataGrid.tsx @@ -1,20 +1,24 @@ +import Box from '@mui/material/Box'; import { grey } from '@mui/material/colors'; import { DataGrid, DataGridProps } from '@mui/x-data-grid'; +import { useCallback } from 'react'; import StyledDataGridOverlay from './StyledDataGridOverlay'; -import Box from '@mui/material/Box'; const StyledLoadingOverlay = () => ( ); - -export const StyledDataGrid = (props: DataGridProps) => { +export type StyledDataGridProps = DataGridProps & { + noRowsMessage?: string; +}; +export const StyledDataGrid = (props: StyledDataGridProps) => { + const noRowsOverlay = useCallback(() => , []); return ( { '& .MuiDataGrid-cell': { borderBottom: 'none' } - }, + } }} /> ); diff --git a/app/src/components/data-grid/StyledDataGridOverlay.tsx b/app/src/components/data-grid/StyledDataGridOverlay.tsx index 5731351f01..deb197ddbf 100644 --- a/app/src/components/data-grid/StyledDataGridOverlay.tsx +++ b/app/src/components/data-grid/StyledDataGridOverlay.tsx @@ -1,12 +1,12 @@ import Typography from '@mui/material/Typography'; import { GridOverlay } from '@mui/x-data-grid'; -const StyledDataGridOverlay = () => ( +const StyledDataGridOverlay = (props: { message?: string }) => ( - No records found + {props.message || 'No records found'} ); -export default StyledDataGridOverlay; \ No newline at end of file +export default StyledDataGridOverlay; diff --git a/app/src/features/funding-sources/list/FundingSourcesTable.tsx b/app/src/features/funding-sources/list/FundingSourcesTable.tsx index 8a0ade4357..3f87dc5520 100644 --- a/app/src/features/funding-sources/list/FundingSourcesTable.tsx +++ b/app/src/features/funding-sources/list/FundingSourcesTable.tsx @@ -63,6 +63,7 @@ const FundingSourcesTable = (props: IFundingSourcesTableTableProps) => { return ( `funding-source-${row.funding_source_id}`} diff --git a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx index a417f2c353..d825d84405 100644 --- a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx @@ -218,6 +218,7 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT ) : ( - ) + ); return ( <> @@ -110,6 +110,7 @@ const SurveySpatialTelemetryDataTable = (props: ISurveySpatialTelemetryDataTable ) : ( Date: Thu, 25 Jan 2024 11:12:30 -0800 Subject: [PATCH 51/85] SIMSBIOHUB-445: Refactored observations fetching to include supplementary sampling information --- .../survey/{surveyId}/observations/index.ts | 34 ++++++-- .../survey/{surveyId}/sample-site/delete.ts | 4 +- .../repositories/observation-repository.ts | 81 ++++++++++++++++--- api/src/services/observation-service.ts | 17 +++- api/src/services/platform-service.ts | 2 +- app/src/contexts/observationsTableContext.tsx | 16 ++++ .../interfaces/useObservationApi.interface.ts | 4 +- 7 files changed, 135 insertions(+), 23 deletions(-) diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts index 5c9974bcba..13dc6373da 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts @@ -149,7 +149,8 @@ const paginationSchema: SchemaObject = { minimum: 1 }, current_page: { - type: 'integer' + type: 'integer', + minimum: 1 }, last_page: { type: 'integer', @@ -251,11 +252,28 @@ PUT.apiDoc = { { in: 'query', name: 'page', - required: false + required: true, + // TODO how to enforce this? + // type: 'integer', + // minimum: 1, }, { in: 'query', name: 'limit', + // TODO how to enforce this? + // type: 'integer', + // minimum: 1, + // maximum: 100, + required: true + }, + { + in: 'query', + name: 'sort', + required: false + }, + { + in: 'query', + name: 'order', required: false } ], @@ -356,9 +374,11 @@ export function getSurveyObservations(): RequestHandler { const observationService = new ObservationService(connection); - const paginationOptions: ApiPaginationOptions | undefined = - limit !== undefined && page !== undefined ? { limit, page, sort, order } : undefined; - const observationData = await observationService.getSurveyObservationsWithSupplementaryData( + const paginationOptions: ApiPaginationOptions | undefined = limit !== undefined && page !== undefined + ? { limit, page, sort, order } + : { limit: 10, page: 1 } + + const observationData = await observationService.getSurveyObservationsWithSupplementaryAndSamplingData( surveyId, paginationOptions ); @@ -370,7 +390,9 @@ export function getSurveyObservations(): RequestHandler { total: observationCount, per_page: limit ?? observationCount, current_page: page ?? 1, - last_page: limit ? Math.ceil(observationCount / limit) : 1 + last_page: limit ? Math.ceil(observationCount / limit) : 1, + sort, + order } }); } catch (error) { diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/delete.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/delete.ts index 00e56087da..c66d263905 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/delete.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/delete.ts @@ -115,12 +115,12 @@ export function deleteSurveySampleSiteRecords(): RequestHandler { const observationService = new ObservationService(connection); const sampleLocationService = new SampleLocationService(connection); - const observationCount = await observationService.getObservationsCountBySampleSiteIds( + const response = await observationService.getObservationsCountBySampleSiteIds( surveyId, surveySampleSiteIds ); - if (observationCount.observationCount > 0) { + if (response.observationCount > 0) { throw new HTTP500(`Cannot delete a sample sites that is associated with an observation`); } diff --git a/api/src/repositories/observation-repository.ts b/api/src/repositories/observation-repository.ts index afff43b028..60cc0d4e32 100644 --- a/api/src/repositories/observation-repository.ts +++ b/api/src/repositories/observation-repository.ts @@ -30,6 +30,14 @@ export const ObservationRecord = z.object({ export type ObservationRecord = z.infer; +export const ObservationRecordWithSamplingData = ObservationRecord.extend({ + survey_sample_site_name: z.string(), + survey_sample_method_name: z.string(), + survey_sample_period_start_datetime: z.date() +}); + +export type ObservationRecordWithSamplingData = z.infer; + /** * Interface reflecting survey observations that are being inserted into the database */ @@ -198,25 +206,80 @@ export class ObservationRepository extends BaseRepository { } /** - * Retrieves all observation records for the given survey + * Retrieves a paginated set of observation records for the given survey, including data for + * associated sampling records. * * @param {number} surveyId * @param {ApiPaginationOptions} [pagination] * @return {*} {Promise} * @memberof ObservationRepository */ - async getSurveyObservations(surveyId: number, pagination?: ApiPaginationOptions): Promise { + async getSurveyObservationsWithSamplingData(surveyId: number, pagination: ApiPaginationOptions): Promise { const knex = getKnex(); - const allRowsQuery = knex.queryBuilder().select('*').from('survey_observation').where('survey_id', surveyId); - const query = pagination - ? allRowsQuery.limit(pagination.limit).offset(pagination.page * pagination.limit) - : allRowsQuery; + const paginatedQuery = knex + .with('survey_sample_method_with_name', knex + .select([ + 'ml.name as survey_sample_method_name', + 'ssm.survey_sample_method_id' + ]) + .from({ ssm: 'survey_sample_method '}) + .leftJoin({ ml: 'method_lookup' }, 'ml.method_lookup_id', 'ssm.method_lookup_id') + ) + .select([ + 'so.*', // Select all columns from survey_observation + + // Select columns from the joined survey_sample_site table + 'sss.survey_sample_site_id', + 'sss.name as survey_sample_site_name', + + // Select columns from the joined survey_sample_method table + 'ssmwn.survey_sample_method_id', + 'ssmwn.survey_sample_method_name', + + // Select columns from the joined survey_sample_period table + 'ssp.survey_sample_period_id', + knex.raw('(??::date + ??::time)::timestamp as survey_sample_period_start_datetime', ['ssp.start_date', 'ssp.start_time']) + // 'ssp.name as survey_sample_period_name', + ]) + .from({ so: 'survey_observation' }) // Alias survey_observation as so + + // Join sample site onto observation + .leftJoin({ sss: 'survey_sample_site' }, 'so.survey_sample_site_id', 'sss.survey_sample_site_id') // Join survey_sample_site + + // Join sample method onto observation + .leftJoin({ ssmwn: 'survey_sample_method_with_name' }, 'so.survey_sample_method_id', 'ssmwn.survey_sample_method_id') // Join survey_sample_method + + // Join sample period onto observation + .leftJoin({ ssp: 'survey_sample_period' }, 'so.survey_sample_period_id', 'ssp.survey_sample_period_id') // Join survey_sample_period + .where('so.survey_id', surveyId) + .limit(pagination.limit) + .offset((pagination.page - 1) * pagination.limit); + + // const { bindings, sql } = paginatedQuery.toSQL().toNative(); + // console.log( { sql, bindings }) + + const query = pagination?.sort && pagination.order + ? paginatedQuery.orderBy(pagination.sort, pagination.order) + : paginatedQuery; + + const response = await this.connection.knex(query, ObservationRecordWithSamplingData); + + return response.rows; + } - // TODO possible to conditionally chain these methods together, rather than redeclare the query builder? - const query2 = pagination?.sort && pagination.order ? query.orderBy(pagination.sort, pagination.order) : query; + /** + * Retrieves all observation records for the given survey + * + * @param {number} surveyId + * @return {*} {Promise} + * @memberof ObservationRepository + */ + async getAllSurveyObservations(surveyId: number): Promise { + const knex = getKnex(); + const allRowsQuery = knex.queryBuilder().select('*').from('survey_observation').where('survey_id', surveyId); - const response = await this.connection.knex(query2, ObservationRecord); + const response = await this.connection.knex(allRowsQuery, ObservationRecord); return response.rows; } diff --git a/api/src/services/observation-service.ts b/api/src/services/observation-service.ts index ffdd0858f8..d2b5d53351 100644 --- a/api/src/services/observation-service.ts +++ b/api/src/services/observation-service.ts @@ -111,6 +111,17 @@ export class ObservationService extends DBService { return this.observationRepository.insertUpdateSurveyObservations(surveyId, observations); } + /** + * Retrieves all observation records for the given survey + * + * @param {number} surveyId + * @return {*} {Promise} + * @memberof ObservationRepository + */ + async getAllSurveyObservations(surveyId: number): Promise { + return this.observationRepository.getAllSurveyObservations(surveyId); + } + /** * Retrieves all observation records for the given survey along with supplementary data * @@ -119,11 +130,11 @@ export class ObservationService extends DBService { * @return {*} {Promise<{ surveyObservations: ObservationRecord[]; supplementaryObservationData: ObservationSupplementaryData }>} * @memberof ObservationService */ - async getSurveyObservationsWithSupplementaryData( + async getSurveyObservationsWithSupplementaryAndSamplingData( surveyId: number, - pagination?: ApiPaginationOptions + pagination: ApiPaginationOptions ): Promise<{ surveyObservations: ObservationRecord[]; supplementaryObservationData: ObservationSupplementaryData }> { - const surveyObservations = await this.observationRepository.getSurveyObservations(surveyId, pagination); + const surveyObservations = await this.observationRepository.getSurveyObservationsWithSamplingData(surveyId, pagination); const supplementaryObservationData = await this.getSurveyObservationsSupplementaryData(surveyId); return { surveyObservations, supplementaryObservationData }; diff --git a/api/src/services/platform-service.ts b/api/src/services/platform-service.ts index cd9569213a..706edb2995 100644 --- a/api/src/services/platform-service.ts +++ b/api/src/services/platform-service.ts @@ -326,7 +326,7 @@ export class PlatformService extends DBService { const surveyService = new SurveyService(this.connection); const survey = await surveyService.getSurveyData(surveyId); - const { surveyObservations } = await observationService.getSurveyObservationsWithSupplementaryData(surveyId); + const surveyObservations = await observationService.getAllSurveyObservations(surveyId); const surveyLocation = await surveyService.getSurveyLocationsData(surveyId); const geometryFeatureCollection: FeatureCollection = { diff --git a/app/src/contexts/observationsTableContext.tsx b/app/src/contexts/observationsTableContext.tsx index 1b11721736..6f91c06060 100644 --- a/app/src/contexts/observationsTableContext.tsx +++ b/app/src/contexts/observationsTableContext.tsx @@ -26,6 +26,22 @@ export interface IObservationRecord { longitude: number | null; } +export interface IObservationRecordWithSamplingData { + survey_observation_id: number; + wldtaxonomic_units_id: number; + survey_sample_site_id: number | null; + survey_sample_site_name: string | null; + survey_sample_method_id: number | null; + survey_sample_method_name: string | null; + survey_sample_period_id: number | null; + survey_sample_period_start_datetime: string | null; + count: number | null; + observation_date: Date; + observation_time: string; + latitude: number | null; + longitude: number | null; +} + export interface ISupplementaryObservationData { observationCount: number; } diff --git a/app/src/interfaces/useObservationApi.interface.ts b/app/src/interfaces/useObservationApi.interface.ts index 7c5a124e91..9173f76f49 100644 --- a/app/src/interfaces/useObservationApi.interface.ts +++ b/app/src/interfaces/useObservationApi.interface.ts @@ -1,7 +1,7 @@ -import { IObservationRecord, ISupplementaryObservationData } from 'contexts/observationsTableContext'; +import { IObservationRecordWithSamplingData, ISupplementaryObservationData } from 'contexts/observationsTableContext'; export interface IGetSurveyObservationsResponse { - surveyObservations: IObservationRecord[]; + surveyObservations: IObservationRecordWithSamplingData[]; supplementaryObservationData: ISupplementaryObservationData; pagination: { total: number; From d19efecc827554e0cd27d900f57ff9bcdf8e36e1 Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Thu, 25 Jan 2024 11:17:55 -0800 Subject: [PATCH 52/85] SIMSBIOHUB-445: Amended zod schema --- api/src/repositories/observation-repository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/repositories/observation-repository.ts b/api/src/repositories/observation-repository.ts index 60cc0d4e32..16b361b592 100644 --- a/api/src/repositories/observation-repository.ts +++ b/api/src/repositories/observation-repository.ts @@ -33,7 +33,7 @@ export type ObservationRecord = z.infer; export const ObservationRecordWithSamplingData = ObservationRecord.extend({ survey_sample_site_name: z.string(), survey_sample_method_name: z.string(), - survey_sample_period_start_datetime: z.date() + survey_sample_period_start_datetime: z.string() }); export type ObservationRecordWithSamplingData = z.infer; From 0b1cf06dcd843eaf35c0afd83f7b2c5a01a4b42c Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 25 Jan 2024 13:16:16 -0800 Subject: [PATCH 53/85] updated pagination flow --- .../survey/{surveyId}/observations/index.ts | 14 ++-- .../repositories/observation-repository.ts | 46 +++++++----- api/src/zod-schema/pagination.ts | 20 ++--- .../surveys/view/SurveySpatialData.tsx | 5 +- .../SurveySpatialObservationDataTable.tsx | 75 ++++++------------- 5 files changed, 67 insertions(+), 93 deletions(-) diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts index 13dc6373da..f3d2c17a14 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts @@ -149,8 +149,7 @@ const paginationSchema: SchemaObject = { minimum: 1 }, current_page: { - type: 'integer', - minimum: 1 + type: 'integer' }, last_page: { type: 'integer', @@ -161,7 +160,7 @@ const paginationSchema: SchemaObject = { }, order: { type: 'string', - enum: ['ASC', 'DESC'] + enum: ['asc', 'desc'] } } }; @@ -252,7 +251,7 @@ PUT.apiDoc = { { in: 'query', name: 'page', - required: true, + required: true // TODO how to enforce this? // type: 'integer', // minimum: 1, @@ -363,7 +362,7 @@ export function getSurveyObservations(): RequestHandler { const page: number | undefined = req.query.page ? Number(req.query.page) : undefined; const limit: number | undefined = req.query.limit ? Number(req.query.limit) : undefined; const sort: string | undefined = req.query.sort ? String(req.query.sort) : undefined; - const order: 'ASC' | 'DESC' | undefined = req.query.order ? (String(req.query.order) as 'ASC' | 'DESC') : undefined; + const order: 'asc' | 'desc' | undefined = req.query.order ? (String(req.query.order) as 'asc' | 'desc') : undefined; defaultLog.debug({ label: 'getSurveyObservations', surveyId }); @@ -374,9 +373,8 @@ export function getSurveyObservations(): RequestHandler { const observationService = new ObservationService(connection); - const paginationOptions: ApiPaginationOptions | undefined = limit !== undefined && page !== undefined - ? { limit, page, sort, order } - : { limit: 10, page: 1 } + const paginationOptions: ApiPaginationOptions | undefined = + limit !== undefined && page !== undefined ? { limit, page, sort, order } : { limit: 10, page: 0 }; const observationData = await observationService.getSurveyObservationsWithSupplementaryAndSamplingData( surveyId, diff --git a/api/src/repositories/observation-repository.ts b/api/src/repositories/observation-repository.ts index 16b361b592..046bd391e5 100644 --- a/api/src/repositories/observation-repository.ts +++ b/api/src/repositories/observation-repository.ts @@ -31,9 +31,9 @@ export const ObservationRecord = z.object({ export type ObservationRecord = z.infer; export const ObservationRecordWithSamplingData = ObservationRecord.extend({ - survey_sample_site_name: z.string(), - survey_sample_method_name: z.string(), - survey_sample_period_start_datetime: z.string() + survey_sample_site_name: z.string().nullable(), + survey_sample_method_name: z.string().nullable(), + survey_sample_period_start_datetime: z.string().nullable() }); export type ObservationRecordWithSamplingData = z.infer; @@ -214,17 +214,19 @@ export class ObservationRepository extends BaseRepository { * @return {*} {Promise} * @memberof ObservationRepository */ - async getSurveyObservationsWithSamplingData(surveyId: number, pagination: ApiPaginationOptions): Promise { + async getSurveyObservationsWithSamplingData( + surveyId: number, + pagination: ApiPaginationOptions + ): Promise { const knex = getKnex(); const paginatedQuery = knex - .with('survey_sample_method_with_name', knex - .select([ - 'ml.name as survey_sample_method_name', - 'ssm.survey_sample_method_id' - ]) - .from({ ssm: 'survey_sample_method '}) - .leftJoin({ ml: 'method_lookup' }, 'ml.method_lookup_id', 'ssm.method_lookup_id') + .with( + 'survey_sample_method_with_name', + knex + .select(['ml.name as survey_sample_method_name', 'ssm.survey_sample_method_id']) + .from({ ssm: 'survey_sample_method ' }) + .leftJoin({ ml: 'method_lookup' }, 'ml.method_lookup_id', 'ssm.method_lookup_id') ) .select([ 'so.*', // Select all columns from survey_observation @@ -232,36 +234,42 @@ export class ObservationRepository extends BaseRepository { // Select columns from the joined survey_sample_site table 'sss.survey_sample_site_id', 'sss.name as survey_sample_site_name', - + // Select columns from the joined survey_sample_method table 'ssmwn.survey_sample_method_id', 'ssmwn.survey_sample_method_name', // Select columns from the joined survey_sample_period table 'ssp.survey_sample_period_id', - knex.raw('(??::date + ??::time)::timestamp as survey_sample_period_start_datetime', ['ssp.start_date', 'ssp.start_time']) + knex.raw('(??::date + ??::time)::timestamp as survey_sample_period_start_datetime', [ + 'ssp.start_date', + 'ssp.start_time' + ]) // 'ssp.name as survey_sample_period_name', ]) .from({ so: 'survey_observation' }) // Alias survey_observation as so - + // Join sample site onto observation .leftJoin({ sss: 'survey_sample_site' }, 'so.survey_sample_site_id', 'sss.survey_sample_site_id') // Join survey_sample_site // Join sample method onto observation - .leftJoin({ ssmwn: 'survey_sample_method_with_name' }, 'so.survey_sample_method_id', 'ssmwn.survey_sample_method_id') // Join survey_sample_method + .leftJoin( + { ssmwn: 'survey_sample_method_with_name' }, + 'so.survey_sample_method_id', + 'ssmwn.survey_sample_method_id' + ) // Join survey_sample_method // Join sample period onto observation .leftJoin({ ssp: 'survey_sample_period' }, 'so.survey_sample_period_id', 'ssp.survey_sample_period_id') // Join survey_sample_period .where('so.survey_id', surveyId) .limit(pagination.limit) - .offset((pagination.page - 1) * pagination.limit); + .offset(pagination.page * pagination.limit); // const { bindings, sql } = paginatedQuery.toSQL().toNative(); // console.log( { sql, bindings }) - const query = pagination?.sort && pagination.order - ? paginatedQuery.orderBy(pagination.sort, pagination.order) - : paginatedQuery; + const query = + pagination?.sort && pagination.order ? paginatedQuery.orderBy(pagination.sort, pagination.order) : paginatedQuery; const response = await this.connection.knex(query, ObservationRecordWithSamplingData); diff --git a/api/src/zod-schema/pagination.ts b/api/src/zod-schema/pagination.ts index 287e3dd28d..ea88e79ac5 100644 --- a/api/src/zod-schema/pagination.ts +++ b/api/src/zod-schema/pagination.ts @@ -1,8 +1,8 @@ -import { z } from "zod"; +import { z } from 'zod'; export const ApiPaginationSorting = z.object({ - sort: z.string().optional(), - order: z.enum(['ASC', 'DESC']).optional() + sort: z.string().optional(), + order: z.enum(['asc', 'desc']).optional() }); export type ApiPaginationSorting = z.infer; @@ -11,8 +11,8 @@ export type ApiPaginationSorting = z.infer; * Object used to make paginated requests */ export const ApiPaginationOptions = ApiPaginationSorting.extend({ - limit: z.number(), - page: z.number(), + limit: z.number(), + page: z.number() }); export type ApiPaginationOptions = z.infer; @@ -21,10 +21,10 @@ export type ApiPaginationOptions = z.infer; * Object used to represent results from paginated queries */ export const ApiPaginationResults = ApiPaginationSorting.extend({ - total: z.number(), - per_page: z.number(), - current_page: z.number(), - last_page: z.number() + total: z.number(), + per_page: z.number(), + current_page: z.number(), + last_page: z.number() }); -export type ApiPaginationResults = z.infer; \ No newline at end of file +export type ApiPaginationResults = z.infer; diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 9080cf9f7f..b26764ce2a 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -175,10 +175,7 @@ const SurveySpatialData = () => { {currentTab === SurveySpatialDataSet.OBSERVATIONS && ( - + )} {currentTab === SurveySpatialDataSet.TELEMETRY && } diff --git a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx index d825d84405..04be981702 100644 --- a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx @@ -3,40 +3,36 @@ import Skeleton from '@mui/material/Skeleton'; import Stack from '@mui/material/Stack'; import { GridColDef, GridSortModel } from '@mui/x-data-grid'; import { StyledDataGrid } from 'components/data-grid/StyledDataGrid'; -import { CodesContext } from 'contexts/codesContext'; -import { IObservationRecord } from 'contexts/observationsTableContext'; +import { IObservationRecordWithSamplingData } from 'contexts/observationsTableContext'; import { SurveyContext } from 'contexts/surveyContext'; import { TaxonomyContext } from 'contexts/taxonomyContext'; import dayjs from 'dayjs'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; -import { IGetSampleLocationRecord } from 'interfaces/useSurveyApi.interface'; -import { useContext, useEffect, useMemo, useState } from 'react'; -import { getCodesName } from 'utils/Utils'; +import { useContext, useEffect, useState } from 'react'; interface IObservationTableRow { - id: number; + survey_observation_id: number; itis_scientific_name: string | undefined; + wldtaxonomic_units_id: number; count: number | null; - sample_name: string | undefined; - sample_method: string | undefined; - sample_period: string | undefined; - date: string | undefined; - time: string | undefined; + survey_sample_site_name: string | null; + survey_sample_method_name: string | null; + survey_sample_period_start_datetime: string | null; + observation_date: Date; + observation_time: string; latitude: number | null; longitude: number | null; } interface ISurveySpatialObservationDataTableProps { - sample_sites: IGetSampleLocationRecord[]; isLoading: boolean; } const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataTableProps) => { const biohubApi = useBiohubApi(); const surveyContext = useContext(SurveyContext); - const codesContext = useContext(CodesContext); const taxonomyContext = useContext(TaxonomyContext); - const [data, setData] = useState([]); + const [data, setData] = useState([]); const [totalRows, setTotalRows] = useState(0); const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState(5); @@ -70,42 +66,17 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT } }, [paginatedDataLoader.data]); - const sampleSites = useMemo(() => { - return surveyContext.sampleSiteDataLoader.data?.sampleSites ?? []; - }, [surveyContext.sampleSiteDataLoader.data]); - - const sampleMethods = useMemo(() => { - return surveyContext.sampleSiteDataLoader.data?.sampleSites.flatMap((item) => item.sample_methods) || []; - }, [surveyContext.sampleSiteDataLoader.data]); - - const samplePeriods = useMemo(() => { - return ( - surveyContext.sampleSiteDataLoader.data?.sampleSites.flatMap((item) => - item.sample_methods?.flatMap((method) => method.sample_periods) - ) || [] - ); - }, [surveyContext.sampleSiteDataLoader.data]); - const tableData: IObservationTableRow[] = data.map((item) => { - const siteName = sampleSites.find((site) => site.survey_sample_site_id === item.survey_sample_site_id)?.name; - const method_id = sampleMethods.find( - (method) => method?.survey_sample_method_id === item.survey_sample_method_id - )?.method_lookup_id; - const period = samplePeriods.find((period) => period?.survey_sample_period_id === item.survey_sample_period_id); - let periodString = ''; - if (period) { - periodString = `${period.start_date} ${period.start_time ?? ''} - ${period.end_date} ${period.end_time ?? ''}`; - } - return { - id: item.survey_observation_id, + survey_observation_id: item.survey_observation_id, itis_scientific_name: taxonomyContext.getCachedSpeciesTaxonomyById(item.wldtaxonomic_units_id)?.label, + wldtaxonomic_units_id: item.wldtaxonomic_units_id, count: item.count, - sample_name: siteName, - sample_method: method_id ? getCodesName(codesContext.codesDataLoader.data, 'sample_methods', method_id) : '', - sample_period: periodString, - date: dayjs(item.observation_date).format('YYYY-MM-DD'), - time: dayjs(item.observation_date).format('HH:mm:ss'), + survey_sample_site_name: item.survey_sample_site_name, + survey_sample_method_name: item.survey_sample_method_name, + survey_sample_period_start_datetime: item.survey_sample_period_start_datetime, + observation_date: dayjs(item.observation_date).toDate(), + observation_time: dayjs(item.observation_date).format('HH:mm:ss'), latitude: item.latitude, longitude: item.longitude }; @@ -119,19 +90,19 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT minWidth: 200 }, { - field: 'sample_site', + field: 'survey_sample_site_name', headerName: 'Sample Site', flex: 1, minWidth: 200 }, { - field: 'sample_method', + field: 'survey_sample_method_name', headerName: 'Sample Method', flex: 1, minWidth: 200 }, { - field: 'sample_period', + field: 'survey_sample_period_start_datetime', headerName: 'Sample Period', flex: 1, minWidth: 200 @@ -144,12 +115,12 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT maxWidth: 100 }, { - field: 'date', + field: 'observation_date', headerName: 'Date', maxWidth: 120 }, { - field: 'time', + field: 'observation_time', headerName: 'Time', headerAlign: 'right', align: 'right', @@ -234,7 +205,7 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT sortModel={sortModel} onSortModelChange={(model) => setSortModel(model)} loading={paginatedDataLoader.isLoading} - getRowId={(row) => row.id} + getRowId={(row) => row.survey_observation_id} columns={columns} rowSelection={false} checkboxSelection={false} From 2b4b3b6b0292da048919156fe7c72e3b4e16e1f7 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 25 Jan 2024 14:13:36 -0800 Subject: [PATCH 54/85] fixed dates --- .../surveys/view/SurveySpatialObservationDataTable.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx index 04be981702..e056538906 100644 --- a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx @@ -19,7 +19,7 @@ interface IObservationTableRow { survey_sample_site_name: string | null; survey_sample_method_name: string | null; survey_sample_period_start_datetime: string | null; - observation_date: Date; + observation_date: string; observation_time: string; latitude: number | null; longitude: number | null; @@ -51,7 +51,6 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT useEffect(() => { if (sortModel.length > 0) { if (sortModel[0].sort) { - console.log(`Table Sort: ${sortModel[0].field} ${sortModel[0].sort}`); paginatedDataLoader.refresh(page, pageSize, sortModel[0].field, sortModel[0].sort); } } else { @@ -75,7 +74,7 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT survey_sample_site_name: item.survey_sample_site_name, survey_sample_method_name: item.survey_sample_method_name, survey_sample_period_start_datetime: item.survey_sample_period_start_datetime, - observation_date: dayjs(item.observation_date).toDate(), + observation_date: dayjs(item.observation_date).format('YYYY-MM-DD'), observation_time: dayjs(item.observation_date).format('HH:mm:ss'), latitude: item.latitude, longitude: item.longitude From bec95a7c4185352eccfe4a843b1f87059bf0cb41 Mon Sep 17 00:00:00 2001 From: jeznorth Date: Thu, 25 Jan 2024 14:31:14 -0800 Subject: [PATCH 55/85] UI Updates --- app/src/components/map/styles/MapBase.scss | 9 ++++++++- app/src/features/surveys/view/SurveyMap.tsx | 2 +- .../surveys/view/SurveySpatialObservationDataTable.tsx | 2 +- .../surveys/view/SurveySpatialTelemetryDataTable.tsx | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/components/map/styles/MapBase.scss b/app/src/components/map/styles/MapBase.scss index 7bc93bbcc5..629beb5c9e 100644 --- a/app/src/components/map/styles/MapBase.scss +++ b/app/src/components/map/styles/MapBase.scss @@ -8,7 +8,14 @@ path.leaflet-interactive:focus { outline: none; } -.leaflet-left .leaflet-control { +.leaflet-top .leaflet-control { margin-top: 16px; +} + +.leaflet-left .leaflet-control { margin-left: 16px; +} + +.leaflet-right .leaflet-control { + margin-right: 16px; } \ No newline at end of file diff --git a/app/src/features/surveys/view/SurveyMap.tsx b/app/src/features/surveys/view/SurveyMap.tsx index 27d42599a5..f41fc7c5df 100644 --- a/app/src/features/surveys/view/SurveyMap.tsx +++ b/app/src/features/surveys/view/SurveyMap.tsx @@ -47,7 +47,7 @@ const SurveyMap = (props: ISurveyMapProps) => { ))} - + diff --git a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx index e056538906..77576d49a9 100644 --- a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx @@ -188,7 +188,7 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT ) : ( ) : ( Date: Thu, 25 Jan 2024 14:42:00 -0800 Subject: [PATCH 56/85] fixed issue with observation reload --- app/src/features/surveys/view/SurveyPage.tsx | 14 ++------------ .../features/surveys/view/SurveySpatialData.tsx | 1 + 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/app/src/features/surveys/view/SurveyPage.tsx b/app/src/features/surveys/view/SurveyPage.tsx index 3022fe0c44..ccf0a563cb 100644 --- a/app/src/features/surveys/view/SurveyPage.tsx +++ b/app/src/features/surveys/view/SurveyPage.tsx @@ -1,3 +1,4 @@ +import { Stack } from '@mui/material'; import Box from '@mui/material/Box'; import CircularProgress from '@mui/material/CircularProgress'; import Container from '@mui/material/Container'; @@ -6,17 +7,15 @@ import SurveySubmissionAlertBar from 'components/publish/SurveySubmissionAlertBa import { SystemRoleGuard } from 'components/security/Guards'; import { SYSTEM_ROLE } from 'constants/roles'; import { CodesContext } from 'contexts/codesContext'; -import { ObservationsContext } from 'contexts/observationsContext'; import { SurveyContext } from 'contexts/surveyContext'; import { TaxonomyContextProvider } from 'contexts/taxonomyContext'; import SurveyDetails from 'features/surveys/view/SurveyDetails'; import React, { useContext, useEffect } from 'react'; import SurveyStudyArea from './components/SurveyStudyArea'; +import SurveyAnimals from './SurveyAnimals'; import SurveyAttachments from './SurveyAttachments'; import SurveyHeader from './SurveyHeader'; import SurveySpatialData from './SurveySpatialData'; -import SurveyAnimals from './SurveyAnimals'; -import { Stack } from '@mui/material'; //TODO: PRODUCTION_BANDAGE: Remove @@ -28,17 +27,11 @@ import { Stack } from '@mui/material'; const SurveyPage: React.FC = () => { const codesContext = useContext(CodesContext); const surveyContext = useContext(SurveyContext); - const observationsContext = useContext(ObservationsContext); useEffect(() => { codesContext.codesDataLoader.load(); }, [codesContext.codesDataLoader]); - useEffect(() => { - observationsContext.observationsDataLoader.refresh(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - if (!codesContext.codesDataLoader.data || !surveyContext.surveyDataLoader.data) { return ; } @@ -47,9 +40,7 @@ const SurveyPage: React.FC = () => { <> - - @@ -63,7 +54,6 @@ const SurveyPage: React.FC = () => { - {/* diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index b26764ce2a..0e4aa2de53 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -28,6 +28,7 @@ const SurveySpatialData = () => { surveyContext.deploymentDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); surveyContext.critterDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); surveyContext.sampleSiteDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); + observationsContext.observationsDataLoader.refresh(); }, []); useEffect(() => { From 23752faf28bcb41850e51b02f50a1fe8abfb9e1e Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Thu, 25 Jan 2024 15:33:24 -0800 Subject: [PATCH 57/85] SIMSBIOHUB-445: Fix some pagination bugs --- .../survey/{surveyId}/observations/index.ts | 56 ++++++++++--------- app/src/contexts/observationsContext.tsx | 2 +- .../observations-table/ObservationsTable.tsx | 3 + .../SurveySpatialObservationDataTable.tsx | 2 +- 4 files changed, 34 insertions(+), 29 deletions(-) diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts index f3d2c17a14..febfe9278a 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts @@ -191,6 +191,35 @@ GET.apiDoc = { minimum: 1 }, required: true + }, + { + in: 'query', + name: 'page', + required: true, + schema: { + type: 'integer', + minimum: 1 + } + }, + { + in: 'query', + name: 'limit', + required: true, + schema: { + type: 'integer', + minimum: 1, + maximum: 100, + } + }, + { + in: 'query', + name: 'sort', + required: false + }, + { + in: 'query', + name: 'order', + required: false } ], responses: { @@ -247,33 +276,6 @@ PUT.apiDoc = { in: 'path', name: 'surveyId', required: true - }, - { - in: 'query', - name: 'page', - required: true - // TODO how to enforce this? - // type: 'integer', - // minimum: 1, - }, - { - in: 'query', - name: 'limit', - // TODO how to enforce this? - // type: 'integer', - // minimum: 1, - // maximum: 100, - required: true - }, - { - in: 'query', - name: 'sort', - required: false - }, - { - in: 'query', - name: 'order', - required: false } ], requestBody: { diff --git a/app/src/contexts/observationsContext.tsx b/app/src/contexts/observationsContext.tsx index dd15ccc976..0053ca0545 100644 --- a/app/src/contexts/observationsContext.tsx +++ b/app/src/contexts/observationsContext.tsx @@ -26,7 +26,7 @@ export const ObservationsContextProvider = (props: PropsWithChildren biohubApi.observation.getObservationRecords(projectId, surveyId)); + const observationsDataLoader = useDataLoader(() => biohubApi.observation.getObservationRecords(projectId, surveyId, { page: 1, limit: 10 })); observationsDataLoader.load(); diff --git a/app/src/features/surveys/observations/observations-table/ObservationsTable.tsx b/app/src/features/surveys/observations/observations-table/ObservationsTable.tsx index 39568da874..67fe240e0f 100644 --- a/app/src/features/surveys/observations/observations-table/ObservationsTable.tsx +++ b/app/src/features/surveys/observations/observations-table/ObservationsTable.tsx @@ -536,6 +536,9 @@ const ObservationsTable = (props: ISpeciesObservationTableProps) => { localeText={{ noRowsLabel: 'No Records' }} + initialState={{ + pagination: { paginationModel: { pageSize: 10 } }, + }} onRowSelectionModelChange={observationsTableContext.onRowSelectionModelChange} rowSelectionModel={observationsTableContext.rowSelectionModel} getRowHeight={() => 'auto'} diff --git a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx index 77576d49a9..17c867c2be 100644 --- a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx @@ -34,7 +34,7 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT const [data, setData] = useState([]); const [totalRows, setTotalRows] = useState(0); - const [page, setPage] = useState(0); + const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(5); const [sortModel, setSortModel] = useState([]); From 072d5d60cac3106cbd84ee66f4a432a55e9ce856 Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Thu, 25 Jan 2024 15:42:27 -0800 Subject: [PATCH 58/85] SIMSBIOHUB-445: Support optional pagination --- .../survey/{surveyId}/observations/index.ts | 6 +++--- api/src/repositories/observation-repository.ts | 12 +++++++----- api/src/services/observation-service.ts | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts index febfe9278a..b15dcc300c 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts @@ -195,7 +195,7 @@ GET.apiDoc = { { in: 'query', name: 'page', - required: true, + required: false, schema: { type: 'integer', minimum: 1 @@ -204,7 +204,7 @@ GET.apiDoc = { { in: 'query', name: 'limit', - required: true, + required: false, schema: { type: 'integer', minimum: 1, @@ -376,7 +376,7 @@ export function getSurveyObservations(): RequestHandler { const observationService = new ObservationService(connection); const paginationOptions: ApiPaginationOptions | undefined = - limit !== undefined && page !== undefined ? { limit, page, sort, order } : { limit: 10, page: 0 }; + limit !== undefined && page !== undefined ? { limit, page, sort, order } : undefined const observationData = await observationService.getSurveyObservationsWithSupplementaryAndSamplingData( surveyId, diff --git a/api/src/repositories/observation-repository.ts b/api/src/repositories/observation-repository.ts index 046bd391e5..4e610c4080 100644 --- a/api/src/repositories/observation-repository.ts +++ b/api/src/repositories/observation-repository.ts @@ -216,11 +216,11 @@ export class ObservationRepository extends BaseRepository { */ async getSurveyObservationsWithSamplingData( surveyId: number, - pagination: ApiPaginationOptions + pagination?: ApiPaginationOptions ): Promise { const knex = getKnex(); - const paginatedQuery = knex + const allRowsQuery = knex .with( 'survey_sample_method_with_name', knex @@ -262,12 +262,14 @@ export class ObservationRepository extends BaseRepository { // Join sample period onto observation .leftJoin({ ssp: 'survey_sample_period' }, 'so.survey_sample_period_id', 'ssp.survey_sample_period_id') // Join survey_sample_period .where('so.survey_id', surveyId) + + + const paginatedQuery = !pagination + ? allRowsQuery + : allRowsQuery .limit(pagination.limit) .offset(pagination.page * pagination.limit); - // const { bindings, sql } = paginatedQuery.toSQL().toNative(); - // console.log( { sql, bindings }) - const query = pagination?.sort && pagination.order ? paginatedQuery.orderBy(pagination.sort, pagination.order) : paginatedQuery; diff --git a/api/src/services/observation-service.ts b/api/src/services/observation-service.ts index d2b5d53351..647bf672b7 100644 --- a/api/src/services/observation-service.ts +++ b/api/src/services/observation-service.ts @@ -132,7 +132,7 @@ export class ObservationService extends DBService { */ async getSurveyObservationsWithSupplementaryAndSamplingData( surveyId: number, - pagination: ApiPaginationOptions + pagination?: ApiPaginationOptions ): Promise<{ surveyObservations: ObservationRecord[]; supplementaryObservationData: ObservationSupplementaryData }> { const surveyObservations = await this.observationRepository.getSurveyObservationsWithSamplingData(surveyId, pagination); const supplementaryObservationData = await this.getSurveyObservationsSupplementaryData(surveyId); From 09dc980c3adfebaa76774e966701fcfb0690b9d5 Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Thu, 25 Jan 2024 15:48:09 -0800 Subject: [PATCH 59/85] SIMSBIOHUB-445: Update toggle button bug --- .../surveys/view/components/SurveyMapToolBar.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx index 0a9206eb61..fd8fafa855 100644 --- a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx +++ b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx @@ -44,9 +44,14 @@ const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); - const updateDataSet = (event: React.MouseEvent, newAlignment: SurveySpatialDataSet) => { - props.updateDataSet(newAlignment); + const updateDataSet = (event: React.MouseEvent, dataset: SurveySpatialDataSet) => { + if (!dataset) { + return; + } + + props.updateDataSet(dataset); }; + const updateLayout = (event: React.MouseEvent, newAlignment: SurveySpatialDataLayout) => { setLayout(newAlignment); props.updateLayout(newAlignment); From 319b7fc807c674b856ea9aee5f0342a28c091405 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 25 Jan 2024 16:17:53 -0800 Subject: [PATCH 60/85] updated page starts --- .../surveys/view/SurveySpatialObservationDataTable.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx index 17c867c2be..246d652890 100644 --- a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx @@ -34,13 +34,13 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT const [data, setData] = useState([]); const [totalRows, setTotalRows] = useState(0); - const [page, setPage] = useState(1); + const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState(5); const [sortModel, setSortModel] = useState([]); const paginatedDataLoader = useDataLoader((page: number, limit: number, sort?: string, order?: 'asc' | 'desc') => biohubApi.observation.getObservationRecords(surveyContext.projectId, surveyContext.surveyId, { - page, + page: page + 1, // this fixes an off by one error between the front end and the back end limit, sort, order @@ -178,6 +178,7 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT ); + console.log(`Page: ${page} Page Size: ${pageSize}`); return ( <> {props.isLoading ? ( From 00acc5ed6bef2b3e653f41a0dd3c581356b3f428 Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Thu, 25 Jan 2024 16:52:10 -0800 Subject: [PATCH 61/85] SIMSBIOHUB-445: Added observations spatial query endpoint --- .../survey/{surveyId}/observations/index.ts | 4 +- .../survey/{surveyId}/observations/spatial.ts | 152 ++++++++++++++++++ .../survey/{surveyId}/sample-site/delete.ts | 5 +- .../repositories/observation-repository.ts | 33 +++- api/src/services/observation-service.ts | 22 ++- app/src/components/map/styles/MapBase.scss | 2 +- app/src/contexts/observationsContext.tsx | 4 +- .../observations-table/ObservationsTable.tsx | 2 +- .../surveys/view/SurveySpatialData.tsx | 10 ++ .../view/components/SurveyBaseHeader.tsx | 2 +- .../view/components/SurveyMapToolBar.tsx | 5 +- app/src/hooks/api/useObservationApi.ts | 8 + 12 files changed, 226 insertions(+), 23 deletions(-) create mode 100644 api/src/paths/project/{projectId}/survey/{surveyId}/observations/spatial.ts diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts index b15dcc300c..9cada160bf 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts @@ -208,7 +208,7 @@ GET.apiDoc = { schema: { type: 'integer', minimum: 1, - maximum: 100, + maximum: 100 } }, { @@ -376,7 +376,7 @@ export function getSurveyObservations(): RequestHandler { const observationService = new ObservationService(connection); const paginationOptions: ApiPaginationOptions | undefined = - limit !== undefined && page !== undefined ? { limit, page, sort, order } : undefined + limit !== undefined && page !== undefined ? { limit, page, sort, order } : undefined; const observationData = await observationService.getSurveyObservationsWithSupplementaryAndSamplingData( surveyId, diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/spatial.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/spatial.ts new file mode 100644 index 0000000000..7107a580a0 --- /dev/null +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/spatial.ts @@ -0,0 +1,152 @@ +import { SchemaObject } from 'ajv'; +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { PROJECT_PERMISSION, SYSTEM_ROLE } from '../../../../../../constants/roles'; +import { getDBConnection } from '../../../../../../database/db'; +import { authorizeRequestHandler } from '../../../../../../request-handlers/security/authorization'; +import { ObservationService } from '../../../../../../services/observation-service'; +import { getLogger } from '../../../../../../utils/logger'; + +const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/observation'); + +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + or: [ + { + validProjectPermissions: [ + PROJECT_PERMISSION.COORDINATOR, + PROJECT_PERMISSION.COLLABORATOR, + PROJECT_PERMISSION.OBSERVER + ], + surveyId: Number(req.params.surveyId), + discriminator: 'ProjectPermission' + }, + { + validSystemRoles: [SYSTEM_ROLE.DATA_ADMINISTRATOR], + discriminator: 'SystemRole' + } + ] + }; + }), + getSurveyObservationsGeometry() +]; + +export const surveyObservationsSupplementaryData: SchemaObject = { + type: 'object', + required: ['observationCount'], + properties: { + observationCount: { + type: 'integer', + minimum: 0 + } + } +}; +GET.apiDoc = { + description: 'Get all observations for the survey.', + tags: ['observation'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'path', + name: 'projectId', + schema: { + type: 'number', + minimum: 1 + }, + required: true + }, + { + in: 'path', + name: 'surveyId', + schema: { + type: 'number', + minimum: 1 + }, + required: true + } + ], + responses: { + 200: { + description: 'Survey Observations spatial get response.', + content: { + 'application/json': { + schema: { + type: 'object', + nullable: true, + required: ['supplementaryObservationData', 'surveyObservationsGeometry'], + properties: { + supplementaryObservationData: { ...surveyObservationsSupplementaryData }, + surveyObservationsGeometry: { + type: 'array', + items: { + type: 'object', + required: ['survey_observation_id', 'geojson'], + properties: { + survey_observation_id: { + type: 'integer' + }, + geojson: { + type: 'object' + } + } + } + } + } + } + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +/** + * Fetch all observations for a survey. + * + * @export + * @return {*} {RequestHandler} + */ +export function getSurveyObservationsGeometry(): RequestHandler { + return async (req, res) => { + const surveyId = Number(req.params.surveyId); + + defaultLog.debug({ label: 'getSurveyObservationsGeometry', surveyId }); + + const connection = getDBConnection(req['keycloak_token']); + + try { + await connection.open(); + + const observationService = new ObservationService(connection); + + const observationData = await observationService.getSurveyObservationsGeometryWithSupplementaryData(surveyId); + + return res.status(200).json(observationData); + } catch (error) { + defaultLog.error({ label: 'getSurveyObservations', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/delete.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/delete.ts index c66d263905..4198781309 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/delete.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/delete.ts @@ -115,10 +115,7 @@ export function deleteSurveySampleSiteRecords(): RequestHandler { const observationService = new ObservationService(connection); const sampleLocationService = new SampleLocationService(connection); - const response = await observationService.getObservationsCountBySampleSiteIds( - surveyId, - surveySampleSiteIds - ); + const response = await observationService.getObservationsCountBySampleSiteIds(surveyId, surveySampleSiteIds); if (response.observationCount > 0) { throw new HTTP500(`Cannot delete a sample sites that is associated with an observation`); diff --git a/api/src/repositories/observation-repository.ts b/api/src/repositories/observation-repository.ts index 4e610c4080..5a2c1ff5ff 100644 --- a/api/src/repositories/observation-repository.ts +++ b/api/src/repositories/observation-repository.ts @@ -38,6 +38,13 @@ export const ObservationRecordWithSamplingData = ObservationRecord.extend({ export type ObservationRecordWithSamplingData = z.infer; +export const ObservationGeometryRecord = z.object({ + survey_observation_id: z.number(), + geojson: z.string().transform((jsonString) => JSON.parse(jsonString)) +}); + +export type ObservationGeometryRecord = z.infer; + /** * Interface reflecting survey observations that are being inserted into the database */ @@ -261,14 +268,11 @@ export class ObservationRepository extends BaseRepository { // Join sample period onto observation .leftJoin({ ssp: 'survey_sample_period' }, 'so.survey_sample_period_id', 'ssp.survey_sample_period_id') // Join survey_sample_period - .where('so.survey_id', surveyId) + .where('so.survey_id', surveyId); - - const paginatedQuery = !pagination - ? allRowsQuery - : allRowsQuery - .limit(pagination.limit) - .offset(pagination.page * pagination.limit); + const paginatedQuery = !pagination + ? allRowsQuery + : allRowsQuery.limit(pagination.limit).offset(pagination.page * pagination.limit); const query = pagination?.sort && pagination.order ? paginatedQuery.orderBy(pagination.sort, pagination.order) : paginatedQuery; @@ -278,6 +282,21 @@ export class ObservationRepository extends BaseRepository { return response.rows; } + /** + * TODO jsdoc + */ + async getSurveyObservationsGeometry(surveyId: number): Promise { + const knex = getKnex(); + + const query = knex + .select('survey_observation_id', knex.raw('ST_AsGeoJSON(ST_MakePoint(longitude, latitude)) as geojson')) + .from('survey_observation'); + + const response = await this.connection.knex(query, ObservationGeometryRecord); + + return response.rows; + } + /** * Retrieves all observation records for the given survey * diff --git a/api/src/services/observation-service.ts b/api/src/services/observation-service.ts index 647bf672b7..f398498e89 100644 --- a/api/src/services/observation-service.ts +++ b/api/src/services/observation-service.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import { IDBConnection } from '../database/db'; import { InsertObservation, + ObservationGeometryRecord, ObservationRecord, ObservationRepository, ObservationSubmissionRecord, @@ -20,8 +21,8 @@ import { validateWorksheetColumnTypes, validateWorksheetHeaders } from '../utils/xlsx-utils/worksheet-utils'; -import { DBService } from './db-service'; import { ApiPaginationOptions } from '../zod-schema/pagination'; +import { DBService } from './db-service'; const defaultLog = getLogger('services/observation-service'); @@ -134,12 +135,28 @@ export class ObservationService extends DBService { surveyId: number, pagination?: ApiPaginationOptions ): Promise<{ surveyObservations: ObservationRecord[]; supplementaryObservationData: ObservationSupplementaryData }> { - const surveyObservations = await this.observationRepository.getSurveyObservationsWithSamplingData(surveyId, pagination); + const surveyObservations = await this.observationRepository.getSurveyObservationsWithSamplingData( + surveyId, + pagination + ); const supplementaryObservationData = await this.getSurveyObservationsSupplementaryData(surveyId); return { surveyObservations, supplementaryObservationData }; } + // TODO jsdoc + async getSurveyObservationsGeometryWithSupplementaryData( + surveyId: number + ): Promise<{ + surveyObservationsGeometry: ObservationGeometryRecord[]; + supplementaryObservationData: ObservationSupplementaryData; + }> { + const surveyObservationsGeometry = await this.observationRepository.getSurveyObservationsGeometry(surveyId); + const supplementaryObservationData = await this.getSurveyObservationsSupplementaryData(surveyId); + + return { surveyObservationsGeometry, supplementaryObservationData }; + } + /** * Retrieves all supplementary data for the given survey's observations * @@ -164,7 +181,6 @@ export class ObservationService extends DBService { return this.observationRepository.getSurveyObservationCount(surveyId); } - /** * Inserts a survey observation submission record into the database and returns the key * diff --git a/app/src/components/map/styles/MapBase.scss b/app/src/components/map/styles/MapBase.scss index 629beb5c9e..e4ff96b2e7 100644 --- a/app/src/components/map/styles/MapBase.scss +++ b/app/src/components/map/styles/MapBase.scss @@ -18,4 +18,4 @@ path.leaflet-interactive:focus { .leaflet-right .leaflet-control { margin-right: 16px; -} \ No newline at end of file +} diff --git a/app/src/contexts/observationsContext.tsx b/app/src/contexts/observationsContext.tsx index 0053ca0545..01d9ee58a4 100644 --- a/app/src/contexts/observationsContext.tsx +++ b/app/src/contexts/observationsContext.tsx @@ -26,7 +26,9 @@ export const ObservationsContextProvider = (props: PropsWithChildren biohubApi.observation.getObservationRecords(projectId, surveyId, { page: 1, limit: 10 })); + const observationsDataLoader = useDataLoader(() => + biohubApi.observation.getObservationRecords(projectId, surveyId, { page: 1, limit: 10 }) + ); observationsDataLoader.load(); diff --git a/app/src/features/surveys/observations/observations-table/ObservationsTable.tsx b/app/src/features/surveys/observations/observations-table/ObservationsTable.tsx index 67fe240e0f..e16b80b84b 100644 --- a/app/src/features/surveys/observations/observations-table/ObservationsTable.tsx +++ b/app/src/features/surveys/observations/observations-table/ObservationsTable.tsx @@ -537,7 +537,7 @@ const ObservationsTable = (props: ISpeciesObservationTableProps) => { noRowsLabel: 'No Records' }} initialState={{ - pagination: { paginationModel: { pageSize: 10 } }, + pagination: { paginationModel: { pageSize: 10 } } }} onRowSelectionModelChange={observationsTableContext.onRowSelectionModelChange} rowSelectionModel={observationsTableContext.rowSelectionModel} diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 0e4aa2de53..e24462bfd0 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -6,6 +6,8 @@ import { SurveyContext } from 'contexts/surveyContext'; import { TaxonomyContext } from 'contexts/taxonomyContext'; import { TelemetryDataContext } from 'contexts/telemetryDataContext'; import { Position } from 'geojson'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import useDataLoader from 'hooks/useDataLoader'; import { useContext, useEffect, useMemo, useState } from 'react'; import { INonEditableGeometries } from 'utils/mapUtils'; import SurveyMapToolBar, { SurveySpatialDataLayout, SurveySpatialDataSet } from './components/SurveyMapToolBar'; @@ -20,6 +22,14 @@ const SurveySpatialData = () => { const surveyContext = useContext(SurveyContext); const codesContext = useContext(CodesContext); + const biohubApi = useBiohubApi(); + const { projectId, surveyId } = useContext(SurveyContext); + + const _test__observationsGeometry = useDataLoader(() => + biohubApi.observation.getObservationsGeometry(projectId, surveyId) + ); + _test__observationsGeometry.load(); + //TODO: look into adding this to the query param const [currentTab, setCurrentTab] = useState(SurveySpatialDataSet.OBSERVATIONS); diff --git a/app/src/features/surveys/view/components/SurveyBaseHeader.tsx b/app/src/features/surveys/view/components/SurveyBaseHeader.tsx index 08882510e5..1b6a46dd34 100644 --- a/app/src/features/surveys/view/components/SurveyBaseHeader.tsx +++ b/app/src/features/surveys/view/components/SurveyBaseHeader.tsx @@ -25,7 +25,7 @@ const SurveyBaseHeader = (props: ISurveyHeader) => { square={true} id="pageTitle" sx={{ - position: {sm: 'relative', xl: 'sticky'}, + position: { sm: 'relative', xl: 'sticky' }, top: 0, zIndex: 1002, borderBottom: '1px solid' + grey[300] diff --git a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx index fd8fafa855..084b80505c 100644 --- a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx +++ b/app/src/features/surveys/view/components/SurveyMapToolBar.tsx @@ -48,7 +48,7 @@ const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { if (!dataset) { return; } - + props.updateDataSet(dataset); }; @@ -92,8 +92,7 @@ const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { outline: 'none' } } - }} - > + }}> diff --git a/app/src/hooks/api/useObservationApi.ts b/app/src/hooks/api/useObservationApi.ts index 0f5a4c1321..626f2fae29 100644 --- a/app/src/hooks/api/useObservationApi.ts +++ b/app/src/hooks/api/useObservationApi.ts @@ -70,6 +70,13 @@ const useObservationApi = (axios: AxiosInstance) => { return data; }; + // TODO promise type; jsdoc. + const getObservationsGeometry = async (projectId: number, surveyId: number): Promise => { + const { data } = await axios.get(`/api/project/${projectId}/survey/${surveyId}/observations/spatial`); + + return data; + }; + /** * Uploads an observation CSV for import. * @@ -143,6 +150,7 @@ const useObservationApi = (axios: AxiosInstance) => { return { insertUpdateObservationRecords, getObservationRecords, + getObservationsGeometry, deleteObservationRecords, uploadCsvForImport, processCsvSubmission From a25c925b646c2e1f0b0d5c95c04f5bf84ee7986e Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Thu, 25 Jan 2024 17:02:25 -0800 Subject: [PATCH 62/85] SIMSBIOHUB-442: Updated observationPoints to use observation geometry dateloader --- .../surveys/view/SurveySpatialData.tsx | 44 ++++++++----------- app/src/hooks/api/useObservationApi.ts | 14 ++++-- .../interfaces/useObservationApi.interface.ts | 8 ++++ 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index e24462bfd0..3238a2c9a7 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -25,14 +25,16 @@ const SurveySpatialData = () => { const biohubApi = useBiohubApi(); const { projectId, surveyId } = useContext(SurveyContext); - const _test__observationsGeometry = useDataLoader(() => + const observationsGeometryDataLoader = useDataLoader(() => biohubApi.observation.getObservationsGeometry(projectId, surveyId) ); - _test__observationsGeometry.load(); + + observationsGeometryDataLoader.load(); //TODO: look into adding this to the query param const [currentTab, setCurrentTab] = useState(SurveySpatialDataSet.OBSERVATIONS); + // TODO is this actually needed? useEffect(() => { codesContext.codesDataLoader.load(); surveyContext.deploymentDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); @@ -89,28 +91,18 @@ const SurveySpatialData = () => { }, [telemetryContext.telemetryDataLoader.data]); const observationPoints: INonEditableGeometries[] = useMemo(() => { - const observations = observationsContext.observationsDataLoader.data?.surveyObservations; - - if (!observations) { - return []; - } - - return observations - .filter((observation) => observation.latitude !== undefined && observation.longitude !== undefined) - .map((observation) => { - return { - feature: { - type: 'Feature', - properties: {}, - geometry: { - type: 'Point', - coordinates: [observation.longitude, observation.latitude] as Position - } - }, - popupComponent: undefined - }; - }); - }, [observationsContext.observationsDataLoader.data]); + // TODO type change from any + return (observationsGeometryDataLoader.data?.surveyObservationsGeometry ?? []).map((observation: any) => { + return { + feature: { + type: 'Feature', + properties: {}, + geometry: observation.geojson + }, + popupComponent: undefined + }; + }); + }, [observationsGeometryDataLoader.data]); // TODO: this needs to be saved between page visits // const [layout, setLayout] = useState(SurveySpatialDataLayout.MAP); @@ -165,7 +157,9 @@ const SurveySpatialData = () => { currentTab={currentTab} toggleButtons={[ { - label: `Observations (${observationPoints.length})`, + label: `Observations (${ + observationsGeometryDataLoader.data?.supplementaryObservationData?.observationCount ?? 0 + })`, value: SurveySpatialDataSet.OBSERVATIONS, icon: mdiEye, isLoading: false diff --git a/app/src/hooks/api/useObservationApi.ts b/app/src/hooks/api/useObservationApi.ts index 626f2fae29..4325489f57 100644 --- a/app/src/hooks/api/useObservationApi.ts +++ b/app/src/hooks/api/useObservationApi.ts @@ -4,7 +4,10 @@ import { IObservationTableRow, ISupplementaryObservationData } from 'contexts/observationsTableContext'; -import { IGetSurveyObservationsResponse } from 'interfaces/useObservationApi.interface'; +import { + IGetSurveyObservationsGeometryResponse, + IGetSurveyObservationsResponse +} from 'interfaces/useObservationApi.interface'; import { ApiPaginationOptions } from 'types/misc'; /** @@ -71,8 +74,13 @@ const useObservationApi = (axios: AxiosInstance) => { }; // TODO promise type; jsdoc. - const getObservationsGeometry = async (projectId: number, surveyId: number): Promise => { - const { data } = await axios.get(`/api/project/${projectId}/survey/${surveyId}/observations/spatial`); + const getObservationsGeometry = async ( + projectId: number, + surveyId: number + ): Promise => { + const { data } = await axios.get( + `/api/project/${projectId}/survey/${surveyId}/observations/spatial` + ); return data; }; diff --git a/app/src/interfaces/useObservationApi.interface.ts b/app/src/interfaces/useObservationApi.interface.ts index 9173f76f49..ce807057f4 100644 --- a/app/src/interfaces/useObservationApi.interface.ts +++ b/app/src/interfaces/useObservationApi.interface.ts @@ -10,3 +10,11 @@ export interface IGetSurveyObservationsResponse { last_page: number; }; } + +export interface IGetSurveyObservationsGeometryResponse { + surveyObservationsGeometry: { + survey_observation_id: number; + geojson: any; // TODO actually type `{ type: "Point", coordinates: [number, number] }`. Does this type exist in our app already? + }[]; + supplementaryObservationData: ISupplementaryObservationData; +} From 4ed8a5856d274133f1cebf69ce59256b1982c68b Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 26 Jan 2024 13:07:55 -0800 Subject: [PATCH 63/85] got initial load working with pagination --- app/src/contexts/observationsContext.tsx | 9 ++-- app/src/contexts/observationsTableContext.tsx | 49 ++++++++++++++++--- .../observations-table/ObservationsTable.tsx | 19 +++++-- 3 files changed, 62 insertions(+), 15 deletions(-) diff --git a/app/src/contexts/observationsContext.tsx b/app/src/contexts/observationsContext.tsx index 01d9ee58a4..f2d0e0304d 100644 --- a/app/src/contexts/observationsContext.tsx +++ b/app/src/contexts/observationsContext.tsx @@ -2,6 +2,7 @@ import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader, { DataLoader } from 'hooks/useDataLoader'; import { IGetSurveyObservationsResponse } from 'interfaces/useObservationApi.interface'; import { createContext, PropsWithChildren, useContext } from 'react'; +import { ApiPaginationOptions } from 'types/misc'; import { SurveyContext } from './surveyContext'; /** @@ -14,11 +15,11 @@ export type IObservationsContext = { /** * Data Loader used for retrieving survey observations */ - observationsDataLoader: DataLoader<[], IGetSurveyObservationsResponse, unknown>; + observationsDataLoader: DataLoader<[pagination?: ApiPaginationOptions], IGetSurveyObservationsResponse, unknown>; }; export const ObservationsContext = createContext({ - observationsDataLoader: {} as DataLoader + observationsDataLoader: {} as DataLoader<[pagination?: ApiPaginationOptions], IGetSurveyObservationsResponse, unknown> }); export const ObservationsContextProvider = (props: PropsWithChildren>) => { @@ -26,8 +27,8 @@ export const ObservationsContextProvider = (props: PropsWithChildren - biohubApi.observation.getObservationRecords(projectId, surveyId, { page: 1, limit: 10 }) + const observationsDataLoader = useDataLoader((pagination?: ApiPaginationOptions) => + biohubApi.observation.getObservationRecords(projectId, surveyId, pagination) ); observationsDataLoader.load(); diff --git a/app/src/contexts/observationsTableContext.tsx b/app/src/contexts/observationsTableContext.tsx index 6f91c06060..0144dc4895 100644 --- a/app/src/contexts/observationsTableContext.tsx +++ b/app/src/contexts/observationsTableContext.tsx @@ -1,5 +1,11 @@ import Typography from '@mui/material/Typography'; -import { GridRowId, GridRowSelectionModel, GridValidRowModel, useGridApiRef } from '@mui/x-data-grid'; +import { + GridPaginationModel, + GridRowId, + GridRowSelectionModel, + GridValidRowModel, + useGridApiRef +} from '@mui/x-data-grid'; import { GridApiCommunity } from '@mui/x-data-grid/internals'; import { ObservationsTableI18N } from 'constants/i18n'; import { DialogContext } from 'contexts/dialogContext'; @@ -136,6 +142,9 @@ export type IObservationsTableContext = { * Updates the total observation count for the survey */ setObservationCount: (observationCount: number) => void; + + updatePaginationModel: (model: GridPaginationModel) => void; + paginationModel: GridPaginationModel; }; export const ObservationsTableContext = createContext({ @@ -157,7 +166,9 @@ export const ObservationsTableContext = createContext isLoading: false, validationModel: {}, observationCount: 0, - setObservationCount: () => undefined + setObservationCount: () => undefined, + updatePaginationModel: () => undefined, + paginationModel: { page: 0, pageSize: 0 } }); export const ObservationsTableContextProvider = (props: PropsWithChildren>) => { @@ -190,6 +201,23 @@ export const ObservationsTableContextProvider = (props: PropsWithChildren({}); + // Pagination States + + // const [totalRows, setTotalRows] = useState(0); + const [paginationModel, setPaginationModel] = useState({ + page: 0, + pageSize: 10 + }); + // const [sortModel, setSortModel] = useState([]); + + const updatePaginationModel = (model: GridPaginationModel) => { + setPaginationModel(model); + }; + + useEffect(() => { + observationsContext.observationsDataLoader.refresh({ page: 1, limit: 10 }); + }, []); + /** * Gets all rows from the table, including values that have been edited in the table. */ @@ -504,8 +532,13 @@ export const ObservationsTableContextProvider = (props: PropsWithChildren { - return observationsContext.observationsDataLoader.refresh(); - }, [observationsContext.observationsDataLoader]); + console.log('REFRESH OBSERVATION TRIGGERED'); + console.log(paginationModel); + return observationsContext.observationsDataLoader.refresh({ + page: paginationModel.page, + limit: paginationModel.pageSize + }); + }, [observationsContext.observationsDataLoader, paginationModel]); // True if the data grid contains at least 1 unsaved record const hasUnsavedChanges = modifiedRowIds.length > 0 || addedRowIds.length > 0; @@ -684,7 +717,9 @@ export const ObservationsTableContextProvider = (props: PropsWithChildren { const codesContext = useContext(CodesContext); const hasLoadedCodes = Boolean(codesContext.codesDataLoader.data); + const [sortModel, setSortModel] = useState([]); + const apiRef = observationsTableContext._muiDataGridApiRef; const hasError = useCallback( @@ -529,6 +531,16 @@ const ObservationsTable = (props: ISpeciesObservationTableProps) => { editMode="row" columns={observationColumns} rows={observationsTableContext.rows} + rowCount={observationsTableContext.observationCount} + paginationModel={observationsTableContext.paginationModel} + pageSizeOptions={[10]} + onPaginationModelChange={(model) => { + observationsTableContext.updatePaginationModel(model); + }} + paginationMode="server" + sortingMode="server" + sortModel={sortModel} + onSortModelChange={(model) => setSortModel(model)} onRowEditStart={(params) => observationsTableContext.onRowEditStart(params.id)} onRowEditStop={(_params, event) => { event.defaultMuiPrevented = true; @@ -536,9 +548,6 @@ const ObservationsTable = (props: ISpeciesObservationTableProps) => { localeText={{ noRowsLabel: 'No Records' }} - initialState={{ - pagination: { paginationModel: { pageSize: 10 } } - }} onRowSelectionModelChange={observationsTableContext.onRowSelectionModelChange} rowSelectionModel={observationsTableContext.rowSelectionModel} getRowHeight={() => 'auto'} From 3710d6048c63b291fba59d7ffbc8c6b804361eb7 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 26 Jan 2024 14:30:13 -0800 Subject: [PATCH 64/85] wired up sort model changes --- app/src/contexts/observationsTableContext.tsx | 41 ++++++++++++++++--- .../observations-table/ObservationsTable.tsx | 16 +++----- app/src/utils/Utils.tsx | 8 ++++ 3 files changed, 50 insertions(+), 15 deletions(-) diff --git a/app/src/contexts/observationsTableContext.tsx b/app/src/contexts/observationsTableContext.tsx index bc938b1c97..ce1b5f8b2a 100644 --- a/app/src/contexts/observationsTableContext.tsx +++ b/app/src/contexts/observationsTableContext.tsx @@ -3,6 +3,7 @@ import { GridPaginationModel, GridRowId, GridRowSelectionModel, + GridSortModel, GridValidRowModel, useGridApiRef } from '@mui/x-data-grid'; @@ -14,6 +15,7 @@ import { default as dayjs } from 'dayjs'; import { APIError } from 'hooks/api/useAxios'; import { useBiohubApi } from 'hooks/useBioHubApi'; import { createContext, PropsWithChildren, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { firstOrNull } from 'utils/Utils'; import { v4 as uuidv4 } from 'uuid'; import { RowValidationError, TableValidationModel } from '../components/data-grid/DataGridValidationAlert'; import { SurveyContext } from './surveyContext'; @@ -149,6 +151,8 @@ export type IObservationsTableContext = { updatePaginationModel: (model: GridPaginationModel) => void; paginationModel: GridPaginationModel; + updateSortModel: (mode: GridSortModel) => void; + sortModel: GridSortModel; }; export const ObservationsTableContext = createContext({ @@ -173,7 +177,9 @@ export const ObservationsTableContext = createContext observationCount: 0, setObservationCount: () => undefined, updatePaginationModel: () => undefined, - paginationModel: { page: 0, pageSize: 0 } + paginationModel: { page: 0, pageSize: 0 }, + updateSortModel: () => undefined, + sortModel: [] }); export const ObservationsTableContextProvider = (props: PropsWithChildren>) => { @@ -213,16 +219,37 @@ export const ObservationsTableContextProvider = (props: PropsWithChildren([]); + const [sortModel, setSortModel] = useState([]); const updatePaginationModel = (model: GridPaginationModel) => { setPaginationModel(model); }; + const updateSortModel = (model: GridSortModel) => { + console.log("Updating Sort Model") + setSortModel(model); + }; + + // initial load with pagination values useEffect(() => { - observationsContext.observationsDataLoader.refresh({ page: 1, limit: 10 }); + observationsContext.observationsDataLoader.refresh({ + page: paginationModel.page + 1, // +1 to correct an off by one error with pagination + limit: paginationModel.pageSize + }); }, []); + // Fetch new rows based on sort/ pagination model changes + useEffect(() => { + console.log("Models have changed") + const sort = firstOrNull(sortModel); + observationsContext.observationsDataLoader.refresh({ + page: paginationModel.page + 1, // +1 to correct an off by one error with pagination + limit: paginationModel.pageSize, + sort: sort?.field || undefined, + order: sort?.sort || undefined + }); + }, [paginationModel, sortModel]); + /** * Gets all rows from the table, including values that have been edited in the table. */ @@ -735,7 +762,9 @@ export const ObservationsTableContextProvider = (props: PropsWithChildren { const codesContext = useContext(CodesContext); const hasLoadedCodes = Boolean(codesContext.codesDataLoader.data); - const [sortModel, setSortModel] = useState([]); - const apiRef = observationsTableContext._muiDataGridApiRef; const hasError = useCallback( @@ -542,14 +540,12 @@ const ObservationsTable = (props: ISpeciesObservationTableProps) => { rows={observationsTableContext.rows} rowCount={observationsTableContext.observationCount} paginationModel={observationsTableContext.paginationModel} - pageSizeOptions={[10]} - onPaginationModelChange={(model) => { - observationsTableContext.updatePaginationModel(model); - }} + pageSizeOptions={[10, 15, 20]} + onPaginationModelChange={(model) => observationsTableContext.updatePaginationModel(model)} paginationMode="server" sortingMode="server" - sortModel={sortModel} - onSortModelChange={(model) => setSortModel(model)} + sortModel={observationsTableContext.sortModel} + onSortModelChange={(model) => observationsTableContext.updateSortModel(model)} onRowEditStart={(params) => observationsTableContext.onRowEditStart(params.id)} onRowEditStop={(_params, event) => { event.defaultMuiPrevented = true; diff --git a/app/src/utils/Utils.tsx b/app/src/utils/Utils.tsx index f9cd30a8d2..a434056ef9 100644 --- a/app/src/utils/Utils.tsx +++ b/app/src/utils/Utils.tsx @@ -498,3 +498,11 @@ export const setMessageSnackbar = (message: string, context: IDialogContext) => ) }); }; + +/** + * This will grab the first element from an array or return null if nothing is found + * + * @param arr array to check + * @returns T + */ +export const firstOrNull = (arr: T[]): T | null => (arr.length > 0 ? arr[0] : null); From 7ac1c370bbfdb52324ba16639193b21e62223150 Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Fri, 26 Jan 2024 14:53:36 -0800 Subject: [PATCH 65/85] SIMSBIOHUB-445: Fixed pagination start at page 2 bug in API --- api/src/repositories/observation-repository.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/src/repositories/observation-repository.ts b/api/src/repositories/observation-repository.ts index 5a2c1ff5ff..442a66e560 100644 --- a/api/src/repositories/observation-repository.ts +++ b/api/src/repositories/observation-repository.ts @@ -225,6 +225,8 @@ export class ObservationRepository extends BaseRepository { surveyId: number, pagination?: ApiPaginationOptions ): Promise { + defaultLog.debug({ label: 'getSurveyObservationsWithSamplingData', surveyId, pagination }); + const knex = getKnex(); const allRowsQuery = knex @@ -272,7 +274,7 @@ export class ObservationRepository extends BaseRepository { const paginatedQuery = !pagination ? allRowsQuery - : allRowsQuery.limit(pagination.limit).offset(pagination.page * pagination.limit); + : allRowsQuery.limit(pagination.limit).offset((pagination.page - 1) * pagination.limit); const query = pagination?.sort && pagination.order ? paginatedQuery.orderBy(pagination.sort, pagination.order) : paginatedQuery; From 2a5f632d0d47103bf2d130c3b65613a612e686f1 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 26 Jan 2024 15:38:41 -0800 Subject: [PATCH 66/85] clean up --- app/src/components/fields/DateTimeFields.tsx | 1 - app/src/contexts/observationsTableContext.tsx | 4 ---- .../telemetry-table/ManualTelemetryTableContainer.tsx | 1 - app/src/features/surveys/view/SurveySpatialData.tsx | 1 - app/src/features/surveys/view/survey-animals/animal.ts | 2 +- 5 files changed, 1 insertion(+), 8 deletions(-) diff --git a/app/src/components/fields/DateTimeFields.tsx b/app/src/components/fields/DateTimeFields.tsx index c3a649623c..298b1635d1 100644 --- a/app/src/components/fields/DateTimeFields.tsx +++ b/app/src/components/fields/DateTimeFields.tsx @@ -101,7 +101,6 @@ export const DateTimeFields: React.FC = (props) => { maxDate={dayjs(DATE_LIMIT.max)} value={formattedDateValue} onChange={(value) => { - console.log(value); if (!value || value === 'Invalid Date') { // The creation input value will be 'Invalid Date' when the date field is cleared (empty), and will // contain an actual date string value if the field is not empty but is invalid. diff --git a/app/src/contexts/observationsTableContext.tsx b/app/src/contexts/observationsTableContext.tsx index ce1b5f8b2a..133a93a297 100644 --- a/app/src/contexts/observationsTableContext.tsx +++ b/app/src/contexts/observationsTableContext.tsx @@ -226,7 +226,6 @@ export const ObservationsTableContextProvider = (props: PropsWithChildren { - console.log("Updating Sort Model") setSortModel(model); }; @@ -240,7 +239,6 @@ export const ObservationsTableContextProvider = (props: PropsWithChildren { - console.log("Models have changed") const sort = firstOrNull(sortModel); observationsContext.observationsDataLoader.refresh({ page: paginationModel.page + 1, // +1 to correct an off by one error with pagination @@ -574,8 +572,6 @@ export const ObservationsTableContextProvider = (props: PropsWithChildren { - console.log('REFRESH OBSERVATION TRIGGERED'); - console.log(paginationModel); return observationsContext.observationsDataLoader.refresh({ page: paginationModel.page, limit: paginationModel.pageSize diff --git a/app/src/features/surveys/telemetry/telemetry-table/ManualTelemetryTableContainer.tsx b/app/src/features/surveys/telemetry/telemetry-table/ManualTelemetryTableContainer.tsx index c3bd80b358..936052a033 100644 --- a/app/src/features/surveys/telemetry/telemetry-table/ManualTelemetryTableContainer.tsx +++ b/app/src/features/surveys/telemetry/telemetry-table/ManualTelemetryTableContainer.tsx @@ -144,7 +144,6 @@ const ManualTelemetryTableContainer = () => { }); }) .catch((error) => { - console.log(error); showSnackBar({ snackbarMessage: ( diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 3238a2c9a7..1efac65361 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -131,7 +131,6 @@ const SurveySpatialData = () => { }; const updateLayout = (data: SurveySpatialDataLayout) => { - console.log(`Layout: ${data}`); // setLayout(data); }; diff --git a/app/src/features/surveys/view/survey-animals/animal.ts b/app/src/features/surveys/view/survey-animals/animal.ts index 5e205b729b..64287d86b4 100644 --- a/app/src/features/surveys/view/survey-animals/animal.ts +++ b/app/src/features/surveys/view/survey-animals/animal.ts @@ -454,7 +454,7 @@ export class Critter { _formatCritterQualitativeMeasurements(animal_measurements: IAnimalMeasurement[]): ICritterQualitativeMeasurement[] { const filteredQualitativeMeasurements = animal_measurements.filter((measurement) => { if (measurement.qualitative_option_id && measurement.value) { - console.log('Qualitative measurement must only contain option_id and no value.'); + // Qualitative measurement must only contain option_id and no value return false; } return measurement.qualitative_option_id; From 1d96f45109ddae6450df047e3a9378c543791c33 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 26 Jan 2024 15:41:49 -0800 Subject: [PATCH 67/85] survey table can now remove sorting options --- .../features/surveys/view/SurveySpatialObservationDataTable.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx index 246d652890..847bd6d2c4 100644 --- a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx +++ b/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx @@ -213,7 +213,6 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT disableColumnFilter disableColumnMenu disableVirtualization - sortingOrder={['asc', 'desc']} data-testid="survey-spatial-observation-data-table" /> )} From 643a516486fe4b9be6f840e093bc7fdebcacb535 Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Fri, 26 Jan 2024 16:05:26 -0800 Subject: [PATCH 68/85] SIMSBIOHUB-445: code cleanup for API --- .../survey/{surveyId}/observations/index.ts | 5 +++-- .../survey/{surveyId}/observations/spatial.ts | 2 +- .../repositories/observation-repository.ts | 19 +++++++++++++------ api/src/services/observation-service.ts | 12 +++++++++++- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts index 9cada160bf..ae159382d9 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts @@ -137,7 +137,7 @@ export const surveyObservationsResponseSchema: SchemaObject = { } }; -const paginationSchema: SchemaObject = { +const paginationResponseSchema: SchemaObject = { type: 'object', required: ['total', 'per_page', 'current_page', 'last_page'], properties: { @@ -233,7 +233,7 @@ GET.apiDoc = { properties: { ...surveyObservationsResponseSchema.properties, supplementaryObservationData: { ...surveyObservationsSupplementaryData }, - pagination: { ...paginationSchema } + pagination: { ...paginationResponseSchema } }, title: 'Survey get response object, for view purposes' } @@ -361,6 +361,7 @@ PUT.apiDoc = { export function getSurveyObservations(): RequestHandler { return async (req, res) => { const surveyId = Number(req.params.surveyId); + const page: number | undefined = req.query.page ? Number(req.query.page) : undefined; const limit: number | undefined = req.query.limit ? Number(req.query.limit) : undefined; const sort: string | undefined = req.query.sort ? String(req.query.sort) : undefined; diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/spatial.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/spatial.ts index 7107a580a0..ab990f6118 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/spatial.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/spatial.ts @@ -142,7 +142,7 @@ export function getSurveyObservationsGeometry(): RequestHandler { return res.status(200).json(observationData); } catch (error) { - defaultLog.error({ label: 'getSurveyObservations', message: 'error', error }); + defaultLog.error({ label: 'getSurveyObservationsGeometry', message: 'error', error }); await connection.rollback(); throw error; } finally { diff --git a/api/src/repositories/observation-repository.ts b/api/src/repositories/observation-repository.ts index 442a66e560..a8d96fb315 100644 --- a/api/src/repositories/observation-repository.ts +++ b/api/src/repositories/observation-repository.ts @@ -238,7 +238,8 @@ export class ObservationRepository extends BaseRepository { .leftJoin({ ml: 'method_lookup' }, 'ml.method_lookup_id', 'ssm.method_lookup_id') ) .select([ - 'so.*', // Select all columns from survey_observation + // Select all columns from survey_observation + 'so.*', // Select columns from the joined survey_sample_site table 'sss.survey_sample_site_id', @@ -254,9 +255,9 @@ export class ObservationRepository extends BaseRepository { 'ssp.start_date', 'ssp.start_time' ]) - // 'ssp.name as survey_sample_period_name', ]) - .from({ so: 'survey_observation' }) // Alias survey_observation as so + // Alias survey_observation as so + .from({ so: 'survey_observation' }) // Join sample site onto observation .leftJoin({ sss: 'survey_sample_site' }, 'so.survey_sample_site_id', 'sss.survey_sample_site_id') // Join survey_sample_site @@ -266,7 +267,7 @@ export class ObservationRepository extends BaseRepository { { ssmwn: 'survey_sample_method_with_name' }, 'so.survey_sample_method_id', 'ssmwn.survey_sample_method_id' - ) // Join survey_sample_method + ) // Join sample period onto observation .leftJoin({ ssp: 'survey_sample_period' }, 'so.survey_sample_period_id', 'ssp.survey_sample_period_id') // Join survey_sample_period @@ -285,14 +286,20 @@ export class ObservationRepository extends BaseRepository { } /** - * TODO jsdoc + * Gets a set of GeoJson geometries representing the set of all lat/long points for the + * given survey's observations. + * + * @param {number} surveyId + * @return {*} {Promise} + * @memberof ObservationRepository */ async getSurveyObservationsGeometry(surveyId: number): Promise { const knex = getKnex(); const query = knex .select('survey_observation_id', knex.raw('ST_AsGeoJSON(ST_MakePoint(longitude, latitude)) as geojson')) - .from('survey_observation'); + .from('survey_observation') + .where('survey_id', surveyId); const response = await this.connection.knex(query, ObservationGeometryRecord); diff --git a/api/src/services/observation-service.ts b/api/src/services/observation-service.ts index f398498e89..157621f01d 100644 --- a/api/src/services/observation-service.ts +++ b/api/src/services/observation-service.ts @@ -144,7 +144,17 @@ export class ObservationService extends DBService { return { surveyObservations, supplementaryObservationData }; } - // TODO jsdoc + /** + * Gets a set of GeoJson geometries representing the set of all lat/long points for the + * given survey's observations. + * + * @param {number} surveyId + * @return {*} {Promise<{ + * surveyObservationsGeometry: ObservationGeometryRecord[]; + * supplementaryObservationData: ObservationSupplementaryData; + * }>} + * @memberof ObservationService + */ async getSurveyObservationsGeometryWithSupplementaryData( surveyId: number ): Promise<{ From 5e4d43e282654908857226a4ff04d16e68572c77 Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Mon, 29 Jan 2024 10:49:29 -0800 Subject: [PATCH 69/85] SIMSBIOHUB-445: Renamed components and interfaces --- .../survey/{surveyId}/observations/spatial.ts | 4 +- .../repositories/observation-repository.ts | 4 +- .../observations/SurveyObservationPage.tsx | 1 - .../surveys/view/SurveySpatialData.tsx | 114 ++++++------------ .../surveys/view/SurveySpatialDataTable.tsx | 42 ------- ...veyMapToolBar.tsx => SurveyMapToolbar.tsx} | 75 +++++------- .../interfaces/useObservationApi.interface.ts | 2 +- 7 files changed, 69 insertions(+), 173 deletions(-) delete mode 100644 app/src/features/surveys/view/SurveySpatialDataTable.tsx rename app/src/features/surveys/view/components/{SurveyMapToolBar.tsx => SurveyMapToolbar.tsx} (63%) diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/spatial.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/spatial.ts index ab990f6118..a350ec1e1d 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/spatial.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/spatial.ts @@ -85,12 +85,12 @@ GET.apiDoc = { type: 'array', items: { type: 'object', - required: ['survey_observation_id', 'geojson'], + required: ['survey_observation_id', 'geometry'], properties: { survey_observation_id: { type: 'integer' }, - geojson: { + geometry: { type: 'object' } } diff --git a/api/src/repositories/observation-repository.ts b/api/src/repositories/observation-repository.ts index a8d96fb315..83ac8f5bca 100644 --- a/api/src/repositories/observation-repository.ts +++ b/api/src/repositories/observation-repository.ts @@ -40,7 +40,7 @@ export type ObservationRecordWithSamplingData = z.infer JSON.parse(jsonString)) + geometry: z.string().transform((jsonString) => JSON.parse(jsonString)) }); export type ObservationGeometryRecord = z.infer; @@ -297,7 +297,7 @@ export class ObservationRepository extends BaseRepository { const knex = getKnex(); const query = knex - .select('survey_observation_id', knex.raw('ST_AsGeoJSON(ST_MakePoint(longitude, latitude)) as geojson')) + .select('survey_observation_id', knex.raw('ST_AsGeoJSON(ST_MakePoint(longitude, latitude)) as geometry')) .from('survey_observation') .where('survey_id', surveyId); diff --git a/app/src/features/surveys/observations/SurveyObservationPage.tsx b/app/src/features/surveys/observations/SurveyObservationPage.tsx index 2e6f701b33..4c3ae19676 100644 --- a/app/src/features/surveys/observations/SurveyObservationPage.tsx +++ b/app/src/features/surveys/observations/SurveyObservationPage.tsx @@ -1,6 +1,5 @@ import Box from '@mui/material/Box'; import CircularProgress from '@mui/material/CircularProgress'; -// import Paper from '@mui/material/Paper'; import Stack from '@mui/material/Stack'; import { ObservationsTableContext, ObservationsTableContextProvider } from 'contexts/observationsTableContext'; import { SurveyContext } from 'contexts/surveyContext'; diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 1efac65361..6eb171e702 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -10,20 +10,22 @@ import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; import { useContext, useEffect, useMemo, useState } from 'react'; import { INonEditableGeometries } from 'utils/mapUtils'; -import SurveyMapToolBar, { SurveySpatialDataLayout, SurveySpatialDataSet } from './components/SurveyMapToolBar'; +import SurveySpatialToolbar, { SurveySpatialDatasetViewEnum } from './components/SurveyMapToolbar'; import SurveyMap from './SurveyMap'; import SurveySpatialObservationDataTable from './SurveySpatialObservationDataTable'; import SurveySpatialTelemetryDataTable from './SurveySpatialTelemetryDataTable'; const SurveySpatialData = () => { + const [activeView, setActiveView] = useState(SurveySpatialDatasetViewEnum.OBSERVATIONS); + const observationsContext = useContext(ObservationsContext); const telemetryContext = useContext(TelemetryDataContext); const taxonomyContext = useContext(TaxonomyContext); const surveyContext = useContext(SurveyContext); const codesContext = useContext(CodesContext); + const { projectId, surveyId } = useContext(SurveyContext); const biohubApi = useBiohubApi(); - const { projectId, surveyId } = useContext(SurveyContext); const observationsGeometryDataLoader = useDataLoader(() => biohubApi.observation.getObservationsGeometry(projectId, surveyId) @@ -31,9 +33,6 @@ const SurveySpatialData = () => { observationsGeometryDataLoader.load(); - //TODO: look into adding this to the query param - const [currentTab, setCurrentTab] = useState(SurveySpatialDataSet.OBSERVATIONS); - // TODO is this actually needed? useEffect(() => { codesContext.codesDataLoader.load(); @@ -50,7 +49,7 @@ const SurveySpatialData = () => { } }, [surveyContext.deploymentDataLoader.data]); - // Fetch/ Cache all taxonomic data for the observations + // Fetch/cache all taxonomic data for the observations useEffect(() => { const cacheTaxonomicData = async () => { if (observationsContext.observationsDataLoader.data) { @@ -91,124 +90,83 @@ const SurveySpatialData = () => { }, [telemetryContext.telemetryDataLoader.data]); const observationPoints: INonEditableGeometries[] = useMemo(() => { - // TODO type change from any - return (observationsGeometryDataLoader.data?.surveyObservationsGeometry ?? []).map((observation: any) => { + return (observationsGeometryDataLoader.data?.surveyObservationsGeometry ?? []).map((observation) => { return { feature: { type: 'Feature', properties: {}, - geometry: observation.geojson + geometry: observation.geometry }, popupComponent: undefined }; }); }, [observationsGeometryDataLoader.data]); - // TODO: this needs to be saved between page visits - // const [layout, setLayout] = useState(SurveySpatialDataLayout.MAP); - - const isLoading = () => { - let isLoading = false; - if (currentTab === SurveySpatialDataSet.OBSERVATIONS) { - isLoading = - codesContext.codesDataLoader.isLoading || - surveyContext.sampleSiteDataLoader.isLoading || - observationsContext.observationsDataLoader.isLoading; - } - - if (currentTab === SurveySpatialDataSet.TELEMETRY) { - isLoading = - codesContext.codesDataLoader.isLoading || - surveyContext.deploymentDataLoader.isLoading || - surveyContext.critterDataLoader.isLoading; - } - - return isLoading; - }; - - const updateDataSet = (data: SurveySpatialDataSet) => { - setCurrentTab(data); - }; + let isLoading = false; + if (activeView === SurveySpatialDatasetViewEnum.OBSERVATIONS) { + isLoading = + codesContext.codesDataLoader.isLoading || + surveyContext.sampleSiteDataLoader.isLoading || + observationsContext.observationsDataLoader.isLoading; + } - const updateLayout = (data: SurveySpatialDataLayout) => { - // setLayout(data); - }; + if (activeView === SurveySpatialDatasetViewEnum.TELEMETRY) { + isLoading = + codesContext.codesDataLoader.isLoading || + surveyContext.deploymentDataLoader.isLoading || + surveyContext.critterDataLoader.isLoading; + } let mapPoints: INonEditableGeometries[] = []; - switch (currentTab) { - case SurveySpatialDataSet.OBSERVATIONS: + switch (activeView) { + case SurveySpatialDatasetViewEnum.OBSERVATIONS: mapPoints = observationPoints; break; - case SurveySpatialDataSet.TELEMETRY: + case SurveySpatialDatasetViewEnum.TELEMETRY: mapPoints = telemetryPoints; break; - case SurveySpatialDataSet.MARKED_ANIMALS: + case SurveySpatialDatasetViewEnum.MARKED_ANIMALS: mapPoints = []; break; default: - mapPoints = []; break; } return ( - - + - {currentTab === SurveySpatialDataSet.OBSERVATIONS && ( - + {activeView === SurveySpatialDatasetViewEnum.OBSERVATIONS && ( + )} - {currentTab === SurveySpatialDataSet.TELEMETRY && } + {activeView === SurveySpatialDatasetViewEnum.TELEMETRY && ( + + )} - - {/* {layout === SurveySpatialDataLayout.MAP && ( - - - - )} - - {layout === SurveySpatialDataLayout.TABLE && ( - - - - )} - - {layout === SurveySpatialDataLayout.SPLIT && ( - - - - - - - - - - - )} */} ); }; diff --git a/app/src/features/surveys/view/SurveySpatialDataTable.tsx b/app/src/features/surveys/view/SurveySpatialDataTable.tsx deleted file mode 100644 index 00d4212642..0000000000 --- a/app/src/features/surveys/view/SurveySpatialDataTable.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableCell from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; -import TableHead from '@mui/material/TableHead'; -import TableRow from '@mui/material/TableRow'; -import { v4 as uuid } from 'uuid'; - -interface ISurveySpatialDataTableProps { - tableHeaders: string[]; - tableRows: string[][]; -} -const SurveySpatialDataTable = (props: ISurveySpatialDataTableProps) => { - return ( - <> - -
- - - {props.tableHeaders.map((header) => ( - - {header} - - ))} - - - - {props.tableRows.map((items: string[]) => ( - - {items.map((value: string) => ( - {value} - ))} - - ))} - -
-
- - ); -}; - -export default SurveySpatialDataTable; diff --git a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx b/app/src/features/surveys/view/components/SurveyMapToolbar.tsx similarity index 63% rename from app/src/features/surveys/view/components/SurveyMapToolBar.tsx rename to app/src/features/surveys/view/components/SurveyMapToolbar.tsx index 084b80505c..9f9dec0f66 100644 --- a/app/src/features/surveys/view/components/SurveyMapToolBar.tsx +++ b/app/src/features/surveys/view/components/SurveyMapToolbar.tsx @@ -5,7 +5,7 @@ import Button from '@mui/material/Button'; import Divider from '@mui/material/Divider'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; -import Menu from '@mui/material/Menu'; +import Menu, { MenuProps } from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import ToggleButton from '@mui/material/ToggleButton'; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; @@ -14,51 +14,38 @@ import Typography from '@mui/material/Typography'; import { useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; -export enum SurveySpatialDataSet { - OBSERVATIONS = 'Observations', - TELEMETRY = 'Telemetry', - MARKED_ANIMALS = 'Marked Animals' +export enum SurveySpatialDatasetViewEnum { + OBSERVATIONS = 'OBSERVATIONS', + TELEMETRY = 'TELEMETRY', + MARKED_ANIMALS = 'MARKED_ANIMALS' } -export enum SurveySpatialDataLayout { - MAP = 'Map', - TABLE = 'Table', - SPLIT = 'Split' -} - -interface IToolBarButtons { +interface ISurveySpatialDatasetView { label: string; icon: string; - value: SurveySpatialDataSet; + value: SurveySpatialDatasetViewEnum; isLoading: boolean; } -interface ISurveyMapToolBarProps { - updateDataSet: (data: SurveySpatialDataSet) => void; - updateLayout: (data: SurveySpatialDataLayout) => void; - toggleButtons: IToolBarButtons[]; - currentTab: SurveySpatialDataSet; + +interface ISurveySptialToolbarProps { + updateDatasetView: (view: SurveySpatialDatasetViewEnum) => void; + views: ISurveySpatialDatasetView[]; + activeView: SurveySpatialDatasetViewEnum; } -const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { - const [layout, setLayout] = useState(SurveySpatialDataLayout.MAP); - const [anchorEl, setAnchorEl] = useState(null); - const open = Boolean(anchorEl); +const SurveySpatialToolbar = (props: ISurveySptialToolbarProps) => { + const [anchorEl, setAnchorEl] = useState(null); - const updateDataSet = (event: React.MouseEvent, dataset: SurveySpatialDataSet) => { - if (!dataset) { + const updateDatasetView = (_event: React.MouseEvent, view: SurveySpatialDatasetViewEnum) => { + if (!view) { return; } - props.updateDataSet(dataset); + props.updateDatasetView(view); }; - const updateLayout = (event: React.MouseEvent, newAlignment: SurveySpatialDataLayout) => { - setLayout(newAlignment); - props.updateLayout(newAlignment); - }; - - const handleMenuClick = (e: any) => { - setAnchorEl(e.currentTarget); + const handleMenuClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); }; const handleMenuClose = () => { @@ -69,7 +56,7 @@ const SurveyMapToolBar = (props: ISurveyMapToolBarProps) => { <> { { letterSpacing: '0.02rem' } }}> - {props.toggleButtons.map((item) => ( + {props.views.map((view) => ( } - value={item.value}> - {item.label} + startIcon={} + value={view.value}> + {view.label} ))} - - - MAP - TABLE - SPLIT - ); }; -export default SurveyMapToolBar; +export default SurveySpatialToolbar; diff --git a/app/src/interfaces/useObservationApi.interface.ts b/app/src/interfaces/useObservationApi.interface.ts index ce807057f4..37b6aad721 100644 --- a/app/src/interfaces/useObservationApi.interface.ts +++ b/app/src/interfaces/useObservationApi.interface.ts @@ -14,7 +14,7 @@ export interface IGetSurveyObservationsResponse { export interface IGetSurveyObservationsGeometryResponse { surveyObservationsGeometry: { survey_observation_id: number; - geojson: any; // TODO actually type `{ type: "Point", coordinates: [number, number] }`. Does this type exist in our app already? + geometry: GeoJSON.Point; }[]; supplementaryObservationData: ISupplementaryObservationData; } From 1e44f9bf6bbfdb3d81e75a0b1416312fa50ef255 Mon Sep 17 00:00:00 2001 From: jeznorth Date: Mon, 29 Jan 2024 10:50:59 -0800 Subject: [PATCH 70/85] Fixing telemetry toolbar spacing --- .../ManualTelemetryTableContainer.tsx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/app/src/features/surveys/telemetry/telemetry-table/ManualTelemetryTableContainer.tsx b/app/src/features/surveys/telemetry/telemetry-table/ManualTelemetryTableContainer.tsx index 936052a033..d43ae4229d 100644 --- a/app/src/features/surveys/telemetry/telemetry-table/ManualTelemetryTableContainer.tsx +++ b/app/src/features/surveys/telemetry/telemetry-table/ManualTelemetryTableContainer.tsx @@ -200,7 +200,12 @@ const ManualTelemetryTableContainer = () => { - + - - + + { disabled={telemetryTableContext.isSaving}> Discard Changes - + {hideableColumns.length > 0 && ( <> setColumnVisibilityMenuAnchorEl(event.currentTarget)} disabled={telemetryTableContext.isSaving}> - {/* Column Visibility */} { - + From 04666e393f79a3d085797631e945ab5475d8c338 Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Mon, 29 Jan 2024 10:56:17 -0800 Subject: [PATCH 71/85] SIMSBIOHUB-445: Remove unused useEffect --- app/src/features/surveys/view/SurveySpatialData.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/SurveySpatialData.tsx index 6eb171e702..5ab72e10bd 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/SurveySpatialData.tsx @@ -33,15 +33,6 @@ const SurveySpatialData = () => { observationsGeometryDataLoader.load(); - // TODO is this actually needed? - useEffect(() => { - codesContext.codesDataLoader.load(); - surveyContext.deploymentDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); - surveyContext.critterDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); - surveyContext.sampleSiteDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); - observationsContext.observationsDataLoader.refresh(); - }, []); - useEffect(() => { if (surveyContext.deploymentDataLoader.data) { const deploymentIds = surveyContext.deploymentDataLoader.data.map((item) => item.deployment_id); From b524cf33b2954f9935ff16d2ffc4de3885f3fc87 Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Mon, 29 Jan 2024 11:00:12 -0800 Subject: [PATCH 72/85] SIMSBIOHUB-445: Removed commented out code --- app/src/features/surveys/view/SurveyPage.tsx | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/app/src/features/surveys/view/SurveyPage.tsx b/app/src/features/surveys/view/SurveyPage.tsx index 8d25fca20c..5e1ff4223b 100644 --- a/app/src/features/surveys/view/SurveyPage.tsx +++ b/app/src/features/surveys/view/SurveyPage.tsx @@ -51,19 +51,6 @@ const SurveyPage: React.FC = () => { - - {/* - - - - */} - - {/* - - - - */} - From 981f388c713ffd9924003bf7b81a688f7f60d4eb Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Mon, 29 Jan 2024 11:01:33 -0800 Subject: [PATCH 73/85] SIMSBIOHUB-445: Deleted summary results files. --- .../summary-results/SurveySummaryResults.tsx | 143 ------------ .../components/FileSummaryResults.tsx | 212 ------------------ .../components/SummaryResultsErrors.tsx | 94 -------- .../components/SummaryResultsLoading.tsx | 77 ------- 4 files changed, 526 deletions(-) delete mode 100644 app/src/features/surveys/view/summary-results/SurveySummaryResults.tsx delete mode 100644 app/src/features/surveys/view/summary-results/components/FileSummaryResults.tsx delete mode 100644 app/src/features/surveys/view/summary-results/components/SummaryResultsErrors.tsx delete mode 100644 app/src/features/surveys/view/summary-results/components/SummaryResultsLoading.tsx diff --git a/app/src/features/surveys/view/summary-results/SurveySummaryResults.tsx b/app/src/features/surveys/view/summary-results/SurveySummaryResults.tsx deleted file mode 100644 index c7fbef680f..0000000000 --- a/app/src/features/surveys/view/summary-results/SurveySummaryResults.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import Box from '@mui/material/Box'; -import Divider from '@mui/material/Divider'; -import Toolbar from '@mui/material/Toolbar'; -import Typography from '@mui/material/Typography'; -import ComponentDialog from 'components/dialog/ComponentDialog'; -import FileUpload from 'components/file-upload/FileUpload'; -import { IUploadHandler } from 'components/file-upload/FileUploadItem'; -import { DialogContext } from 'contexts/dialogContext'; -import { SurveyContext } from 'contexts/surveyContext'; -import NoSurveySectionData from 'features/surveys/components/NoSurveySectionData'; -import { useBiohubApi } from 'hooks/useBioHubApi'; -import { useContext, useEffect, useState } from 'react'; -import FileSummaryResults from './components/FileSummaryResults'; -import SummaryResultsErrors from './components/SummaryResultsErrors'; -import SummaryResultsLoading from './components/SummaryResultsLoading'; - -export enum ClassGrouping { - NOTICE = 'Notice', - ERROR = 'Error', - WARNING = 'Warning' -} - -const SurveySummaryResults = () => { - const biohubApi = useBiohubApi(); - const surveyContext = useContext(SurveyContext); - const dialogContext = useContext(DialogContext); - - const projectId = surveyContext.projectId as number; - const surveyId = surveyContext.surveyId as number; - - const [openImportSummaryResults, setOpenImportSummaryResults] = useState(false); - - // provide file name for 'loading' ui before submission responds - const [fileName, setFileName] = useState(''); - useEffect(() => { - surveyContext.summaryDataLoader.load(projectId, surveyId); - }, [surveyContext.summaryDataLoader, projectId, surveyId]); - - const summaryData = surveyContext.summaryDataLoader.data?.surveySummaryData; - - const importSummaryResults = (): IUploadHandler => { - return (file, cancelToken, handleFileUploadProgress) => { - return biohubApi.survey - .uploadSurveySummaryResults(projectId, surveyId, file, cancelToken, handleFileUploadProgress) - .finally(() => { - setFileName(file.name); - surveyContext.summaryDataLoader.refresh(projectId, surveyId); - }); - }; - }; - - const showDeleteDialog = () => { - if (summaryData) { - dialogContext.setYesNoDialog({ - dialogTitle: 'Delete Summary Results Data?', - dialogText: - 'Are you sure you want to delete the summary results data for this survey? This action cannot be undone.', - yesButtonProps: { color: 'error' }, - yesButtonLabel: 'Delete', - noButtonProps: { color: 'primary' }, - noButtonLabel: 'Cancel', - open: true, - onYes: async () => { - await biohubApi.survey.deleteSummarySubmission(projectId, surveyId, summaryData.survey_summary_submission_id); - surveyContext.summaryDataLoader.refresh(projectId, surveyId); - dialogContext.setYesNoDialog({ open: false }); - }, - onClose: () => dialogContext.setYesNoDialog({ open: false }), - onNo: () => dialogContext.setYesNoDialog({ open: false }) - }); - } - }; - - const viewFileContents = async () => { - if (!summaryData) { - return; - } - - let response; - - try { - response = await biohubApi.survey.getSummarySubmissionSignedURL( - projectId, - surveyId, - summaryData?.survey_summary_submission_id - ); - } catch { - return; - } - - if (!response) { - return; - } - - window.open(response); - }; - - return ( - <> - - Summary Results - - - - - - {/* Data is still loading/ validating */} - {!summaryData && !surveyContext.summaryDataLoader.isReady && } - - {/* No summary */} - {!surveyContext.summaryDataLoader.data && surveyContext.summaryDataLoader.isReady && ( - - )} - - {/* Got a summary with errors */} - {summaryData && !surveyContext.summaryDataLoader.isLoading && summaryData.messages.length > 0 && ( - - )} - - {/* All done */} - {surveyContext.summaryDataLoader.data && ( - - )} - - - setOpenImportSummaryResults(false)}> - - - - ); -}; - -export default SurveySummaryResults; diff --git a/app/src/features/surveys/view/summary-results/components/FileSummaryResults.tsx b/app/src/features/surveys/view/summary-results/components/FileSummaryResults.tsx deleted file mode 100644 index da346dab04..0000000000 --- a/app/src/features/surveys/view/summary-results/components/FileSummaryResults.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import { - mdiDotsVertical, - mdiFileAlertOutline, - mdiFileOutline, - mdiInformationOutline, - mdiLockOutline, - mdiTrashCanOutline, - mdiTrayArrowDown -} from '@mdi/js'; -import Icon from '@mdi/react'; -import { Theme } from '@mui/material'; -import Box from '@mui/material/Box'; -import IconButton from '@mui/material/IconButton'; -import Link from '@mui/material/Link'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import Menu from '@mui/material/Menu'; -import MenuItem from '@mui/material/MenuItem'; -import Paper from '@mui/material/Paper'; -import { makeStyles } from '@mui/styles'; -import clsx from 'clsx'; -import { SubmitStatusChip } from 'components/chips/SubmitStatusChip'; -import RemoveOrResubmitDialog from 'components/publish/components/RemoveOrResubmitDialog'; -import { ProjectRoleGuard, SystemRoleGuard } from 'components/security/Guards'; -import { PublishStatus } from 'constants/attachments'; -import { PROJECT_PERMISSION, SYSTEM_ROLE } from 'constants/roles'; -import { SurveyContext } from 'contexts/surveyContext'; -import { IGetSummaryResultsResponse } from 'interfaces/useSummaryResultsApi.interface'; -import React from 'react'; - -//TODO: PRODUCTION_BANDAGE: Remove from `SubmitStatusChip` and `Remove or Resubmit` button - -interface IFileResultsProps { - fileData: IGetSummaryResultsResponse; - downloadFile: () => void; - showDelete: () => void; -} - -const useStyles = makeStyles((theme: Theme) => ({ - importFile: { - display: 'flex', - paddingTop: theme.spacing(1), - paddingBottom: theme.spacing(1), - paddingRight: theme.spacing(2), - paddingLeft: '20px', - overflow: 'hidden', - '& .importFile-icon': { - color: '#1a5a96' - }, - '&.error': { - borderColor: '#ebccd1', - '& .importFile-icon': { - color: theme.palette.error.main - }, - '& .MuiLink-root': { - color: theme.palette.error.main - }, - '& .MuiChip-root': { - display: 'none' - } - } - }, - browseLink: { - cursor: 'pointer' - }, - fileDownload: { - overflow: 'hidden', - textOverflow: 'ellipsis', - textDecoration: 'underline', - cursor: 'pointer' - } -})); -const FileSummaryResults = (props: IFileResultsProps) => { - const { fileData, downloadFile, showDelete } = props; - const classes = useStyles(); - - const surveyContext = React.useContext(SurveyContext); - const surveyName = surveyContext.surveyDataLoader.data?.surveyData.survey_details.survey_name; - - const [anchorEl, setAnchorEl] = React.useState(null); - const [openRemoveOrResubmitDialog, setOpenRemoveOrResubmitDialog] = React.useState(false); - - const handleClose = () => { - setAnchorEl(null); - }; - - const status = fileData.surveySummarySupplementaryData?.event_timestamp - ? PublishStatus.SUBMITTED - : PublishStatus.UNSUBMITTED; - - let icon: string = mdiFileOutline; - let severity: 'error' | 'info' | 'success' | 'warning' = 'info'; - - if (fileData.surveySummaryData.messages.some((item) => item.class.toUpperCase() === 'ERROR')) { - icon = mdiFileAlertOutline; - severity = 'error'; - } else if (fileData.surveySummaryData.messages.some((item) => item.class.toUpperCase() === 'WARNING')) { - icon = mdiFileAlertOutline; - severity = 'warning'; - } else if (fileData.surveySummaryData.messages.some((item) => item.class.toUpperCase() === 'INFO')) { - icon = mdiInformationOutline; - } else if (status === PublishStatus.SUBMITTED) { - icon = mdiLockOutline; - } - - return ( - <> - setOpenRemoveOrResubmitDialog(false)} - /> - - - - - - - - - downloadFile()}> - {fileData.surveySummaryData.fileName} - - - - - - - - - - - - { - setAnchorEl(e.currentTarget); - }}> - - - { - handleClose(); - }} - anchorOrigin={{ - vertical: 'top', - horizontal: 'right' - }} - transformOrigin={{ - vertical: 'top', - horizontal: 'right' - }}> - { - downloadFile(); - handleClose(); - }}> - - - - Download - - {status === PublishStatus.UNSUBMITTED && ( - - { - showDelete(); - handleClose(); - }}> - - - - Delete - - - )} - {status === PublishStatus.SUBMITTED && ( - - - { - setOpenRemoveOrResubmitDialog(true); - handleClose(); - }} - data-testid="attachment-action-menu-delete"> - - - - Remove or Resubmit - - - - )} - - - - - - - ); -}; - -export default FileSummaryResults; diff --git a/app/src/features/surveys/view/summary-results/components/SummaryResultsErrors.tsx b/app/src/features/surveys/view/summary-results/components/SummaryResultsErrors.tsx deleted file mode 100644 index 73c69107c8..0000000000 --- a/app/src/features/surveys/view/summary-results/components/SummaryResultsErrors.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { mdiAlertCircleOutline } from '@mdi/js'; -import Icon from '@mdi/react'; -import Alert from '@mui/material/Alert'; -import AlertTitle from '@mui/material/AlertTitle'; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import { IGetSummarySubmissionResponseMessages } from 'interfaces/useSummaryResultsApi.interface'; -import { ClassGrouping } from '../SurveySummaryResults'; - -interface IFileErrorResultsProps { - messages: IGetSummarySubmissionResponseMessages[]; -} -const SummaryResultsErrors = (props: IFileErrorResultsProps) => { - const { messages } = props; - const errorGrouping = new Map(); - const warningGrouping = new Map(); - const noticeGrouping = new Map(); - - const groupMessages = () => { - messages.forEach((item) => { - switch (item.class) { - case ClassGrouping.ERROR: - if (!errorGrouping.has(item.type)) { - errorGrouping.set(item.type, [item]); - } else { - errorGrouping.get(item.type)?.push(item); - } - break; - case ClassGrouping.WARNING: - if (!warningGrouping.has(item.type)) { - warningGrouping.set(item.type, [item]); - } else { - warningGrouping.get(item.type)?.push(item); - } - break; - case ClassGrouping.NOTICE: - if (!noticeGrouping.has(item.type)) { - noticeGrouping.set(item.type, [item]); - } else { - noticeGrouping.get(item.type)?.push(item); - } - break; - default: - break; - } - }); - }; - groupMessages(); - - const buildMessages = (group: Map) => { - return ( - - {[...group].map((item) => { - return ( - - - {item[0]} - - - {item[1].map((message) => { - return ( -
  • - - {message.message} - -
  • - ); - })} -
    -
    - ); - })} -
    - ); - }; - - return ( - <> - - {errorGrouping.size > 0 && ( - - }> - Failed to import summary results - One or more errors occurred while attempting to import your summary results file. - {buildMessages(errorGrouping)} - - - )} - - - ); -}; - -export default SummaryResultsErrors; diff --git a/app/src/features/surveys/view/summary-results/components/SummaryResultsLoading.tsx b/app/src/features/surveys/view/summary-results/components/SummaryResultsLoading.tsx deleted file mode 100644 index 230cdbfe04..0000000000 --- a/app/src/features/surveys/view/summary-results/components/SummaryResultsLoading.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { mdiFileOutline } from '@mdi/js'; -import Icon from '@mdi/react'; -import { Theme } from '@mui/material'; -import Box from '@mui/material/Box'; -import Paper from '@mui/material/Paper'; -import Typography from '@mui/material/Typography'; -import { makeStyles } from '@mui/styles'; -import clsx from 'clsx'; -import BorderLinearProgress from '../../components/BorderLinearProgress'; - -const useStyles = makeStyles((theme: Theme) => ({ - importFile: { - display: 'flex', - paddingTop: theme.spacing(1), - paddingBottom: theme.spacing(1), - paddingRight: theme.spacing(2), - paddingLeft: '20px', - overflow: 'hidden', - '& .importFile-icon': { - color: theme.palette.text.secondary - }, - '&.error': { - borderColor: theme.palette.error.main, - '& .importFile-icon': { - color: theme.palette.error.main - } - } - }, - browseLink: { - cursor: 'pointer' - }, - fileLoading: { - overflow: 'hidden', - textOverflow: 'ellipsis', - textDecoration: 'underline', - cursor: 'pointer' - } -})); - -interface ILoadingProps { - fileLoading: string; -} - -const SummaryResultsLoading = (props: ILoadingProps) => { - const { fileLoading } = props; - const classes = useStyles(); - - return ( - <> - - - - - - - - - {fileLoading} - - - - - - - Importing file. Please wait... - - - - - - - - - ); -}; - -export default SummaryResultsLoading; From 9a701e3d732e3080d74ca38c18ee6084aecaf6f0 Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Mon, 29 Jan 2024 11:04:55 -0800 Subject: [PATCH 74/85] SIMSBIOHUB-445: Continued refactor --- app/src/features/surveys/view/SurveyPage.tsx | 2 +- .../spatial-data}/SurveySpatialData.tsx | 4 ++-- .../SurveySpatialObservationDataTable.tsx | 0 .../SurveySpatialTelemetryDataTable.tsx | 18 +++++++++++------- .../SurveySpatialToolbar.tsx} | 0 5 files changed, 14 insertions(+), 10 deletions(-) rename app/src/features/surveys/view/{ => components/spatial-data}/SurveySpatialData.tsx (98%) rename app/src/features/surveys/view/{ => components/spatial-data}/SurveySpatialObservationDataTable.tsx (100%) rename app/src/features/surveys/view/{ => components/spatial-data}/SurveySpatialTelemetryDataTable.tsx (93%) rename app/src/features/surveys/view/components/{SurveyMapToolbar.tsx => spatial-data/SurveySpatialToolbar.tsx} (100%) diff --git a/app/src/features/surveys/view/SurveyPage.tsx b/app/src/features/surveys/view/SurveyPage.tsx index 5e1ff4223b..69748d5ecd 100644 --- a/app/src/features/surveys/view/SurveyPage.tsx +++ b/app/src/features/surveys/view/SurveyPage.tsx @@ -11,7 +11,7 @@ import SurveyStudyArea from './components/SurveyStudyArea'; import SurveyAnimals from './SurveyAnimals'; import SurveyAttachments from './SurveyAttachments'; import SurveyHeader from './SurveyHeader'; -import SurveySpatialData from './SurveySpatialData'; +import SurveySpatialData from './components/spatial-data/SurveySpatialData'; //TODO: PRODUCTION_BANDAGE: Remove diff --git a/app/src/features/surveys/view/SurveySpatialData.tsx b/app/src/features/surveys/view/components/spatial-data/SurveySpatialData.tsx similarity index 98% rename from app/src/features/surveys/view/SurveySpatialData.tsx rename to app/src/features/surveys/view/components/spatial-data/SurveySpatialData.tsx index 5ab72e10bd..857d432d58 100644 --- a/app/src/features/surveys/view/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/components/spatial-data/SurveySpatialData.tsx @@ -10,8 +10,8 @@ import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; import { useContext, useEffect, useMemo, useState } from 'react'; import { INonEditableGeometries } from 'utils/mapUtils'; -import SurveySpatialToolbar, { SurveySpatialDatasetViewEnum } from './components/SurveyMapToolbar'; -import SurveyMap from './SurveyMap'; +import SurveySpatialToolbar, { SurveySpatialDatasetViewEnum } from './SurveySpatialToolbar'; +import SurveyMap from '../../SurveyMap'; import SurveySpatialObservationDataTable from './SurveySpatialObservationDataTable'; import SurveySpatialTelemetryDataTable from './SurveySpatialTelemetryDataTable'; diff --git a/app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx b/app/src/features/surveys/view/components/spatial-data/SurveySpatialObservationDataTable.tsx similarity index 100% rename from app/src/features/surveys/view/SurveySpatialObservationDataTable.tsx rename to app/src/features/surveys/view/components/spatial-data/SurveySpatialObservationDataTable.tsx diff --git a/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx b/app/src/features/surveys/view/components/spatial-data/SurveySpatialTelemetryDataTable.tsx similarity index 93% rename from app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx rename to app/src/features/surveys/view/components/spatial-data/SurveySpatialTelemetryDataTable.tsx index 912388fd30..6271c0fe7e 100644 --- a/app/src/features/surveys/view/SurveySpatialTelemetryDataTable.tsx +++ b/app/src/features/surveys/view/components/spatial-data/SurveySpatialTelemetryDataTable.tsx @@ -6,7 +6,11 @@ import { StyledDataGrid } from 'components/data-grid/StyledDataGrid'; import { SurveyContext } from 'contexts/surveyContext'; import dayjs from 'dayjs'; import { useContext, useMemo } from 'react'; -import { ICritterDeployment } from '../telemetry/ManualTelemetryList'; +import { ICritterDeployment } from '../../../telemetry/ManualTelemetryList'; + +// Set height so we the skeleton loader will match table rows +const rowHeight = 52; + interface ITelemetryData { id: number; critter_id: string | null; @@ -14,11 +18,14 @@ interface ITelemetryData { start: string; end: string; } + interface ISurveySpatialTelemetryDataTableProps { isLoading: boolean; } + const SurveySpatialTelemetryDataTable = (props: ISurveySpatialTelemetryDataTableProps) => { const surveyContext = useContext(SurveyContext); + const flattenedCritterDeployments: ICritterDeployment[] = useMemo(() => { const data: ICritterDeployment[] = []; // combine all critter and deployments into a flat list @@ -64,9 +71,6 @@ const SurveySpatialTelemetryDataTable = (props: ISurveySpatialTelemetryDataTable } ]; - // Set height so we the skeleton loader will match table rows - const RowHeight = 52; - // Skeleton Loader template const SkeletonRow = () => ( row.id} columns={columns} diff --git a/app/src/features/surveys/view/components/SurveyMapToolbar.tsx b/app/src/features/surveys/view/components/spatial-data/SurveySpatialToolbar.tsx similarity index 100% rename from app/src/features/surveys/view/components/SurveyMapToolbar.tsx rename to app/src/features/surveys/view/components/spatial-data/SurveySpatialToolbar.tsx From 5954932c27a7a887f179412969455e55d8be9272 Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Mon, 29 Jan 2024 12:29:59 -0800 Subject: [PATCH 75/85] SIMSBIOHUB-445: PR updates; API test updates --- .../{surveyId}/observations/index.test.ts | 14 ++++++-- .../survey/{surveyId}/observations/index.ts | 21 +++++++++++- .../observation-repository.test.ts | 6 ++-- .../repositories/observation-repository.ts | 2 +- api/src/services/observation-service.test.ts | 15 +++++--- api/src/services/platform-service.test.ts | 9 ++--- app/src/contexts/observationsContext.tsx | 2 -- .../spatial-data/SurveySpatialData.tsx | 34 +++++++++++-------- app/src/hooks/api/useObservationApi.ts | 9 ++++- 9 files changed, 79 insertions(+), 33 deletions(-) diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.test.ts index 5770de3c8d..1cf45e3e59 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.test.ts @@ -828,7 +828,7 @@ describe('getSurveyObservationsWithSupplementaryData', () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); const getSurveyObservationsStub = sinon - .stub(ObservationService.prototype, 'getSurveyObservationsWithSupplementaryData') + .stub(ObservationService.prototype, 'getSurveyObservationsWithSupplementaryAndSamplingData') .resolves({ surveyObservations: ([ { survey_observation_id: 1 }, @@ -851,7 +851,15 @@ describe('getSurveyObservationsWithSupplementaryData', () => { expect(mockRes.statusValue).to.equal(200); expect(mockRes.jsonValue).to.eql({ surveyObservations: [{ survey_observation_id: 1 }, { survey_observation_id: 2 }], - supplementaryObservationData: { observationCount: 2 } + supplementaryObservationData: { observationCount: 2 }, + pagination: { + current_page: 1, + last_page: 1, + order: undefined, + per_page: 2, + sort: undefined, + total: 2 + } }); }); @@ -861,7 +869,7 @@ describe('getSurveyObservationsWithSupplementaryData', () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); sinon - .stub(ObservationService.prototype, 'getSurveyObservationsWithSupplementaryData') + .stub(ObservationService.prototype, 'getSurveyObservationsWithSupplementaryAndSamplingData') .rejects(new Error('a test error')); const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts index ae159382d9..8b4c4670c7 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts @@ -352,6 +352,19 @@ PUT.apiDoc = { } }; +/** + * This record maps observation table sampling site site ID columns to sampling data + * columns that can be sorted on. + * + * TODO We should probably modify frontend functionality to make requests to sort on these + * columns. + */ +const samplingSiteSortingColumnName: Record = { + survey_sample_site_id: 'survey_sample_site_name', + survey_sample_method_id: 'survey_sample_method_name', + survey_sample_period_id: 'survey_sample_period_start_datetime' +}; + /** * Fetch all observations for a survey. * @@ -364,9 +377,15 @@ export function getSurveyObservations(): RequestHandler { const page: number | undefined = req.query.page ? Number(req.query.page) : undefined; const limit: number | undefined = req.query.limit ? Number(req.query.limit) : undefined; - const sort: string | undefined = req.query.sort ? String(req.query.sort) : undefined; const order: 'asc' | 'desc' | undefined = req.query.order ? (String(req.query.order) as 'asc' | 'desc') : undefined; + const sortQuery: string | undefined = req.query.sort ? String(req.query.sort) : undefined; + let sort = sortQuery; + + if (sortQuery && samplingSiteSortingColumnName[sortQuery]) { + sort = samplingSiteSortingColumnName[sortQuery]; + } + defaultLog.debug({ label: 'getSurveyObservations', surveyId }); const connection = getDBConnection(req['keycloak_token']); diff --git a/api/src/repositories/observation-repository.test.ts b/api/src/repositories/observation-repository.test.ts index cbd884fe92..0621fbb6bb 100644 --- a/api/src/repositories/observation-repository.test.ts +++ b/api/src/repositories/observation-repository.test.ts @@ -100,7 +100,7 @@ describe('ObservationRepository', () => { }); }); - describe('getSurveyObservations', () => { + describe('getSurveyObservationsWithSamplingData', () => { it('get all observations for a survey when some observation records exist', async () => { const mockRows = [{}, {}]; const mockQueryResponse = ({ rows: mockRows, rowCount: 2 } as unknown) as QueryResult; @@ -113,7 +113,7 @@ describe('ObservationRepository', () => { const surveyId = 1; - const response = await repository.getSurveyObservations(surveyId); + const response = await repository.getSurveyObservationsWithSamplingData(surveyId); expect(response).to.be.eql(mockRows); }); @@ -130,7 +130,7 @@ describe('ObservationRepository', () => { const surveyId = 1; - const response = await repository.getSurveyObservations(surveyId); + const response = await repository.getSurveyObservationsWithSamplingData(surveyId); expect(response).to.be.eql(mockRows); }); diff --git a/api/src/repositories/observation-repository.ts b/api/src/repositories/observation-repository.ts index 83ac8f5bca..b91f65eee0 100644 --- a/api/src/repositories/observation-repository.ts +++ b/api/src/repositories/observation-repository.ts @@ -288,7 +288,7 @@ export class ObservationRepository extends BaseRepository { /** * Gets a set of GeoJson geometries representing the set of all lat/long points for the * given survey's observations. - * + * * @param {number} surveyId * @return {*} {Promise} * @memberof ObservationRepository diff --git a/api/src/services/observation-service.test.ts b/api/src/services/observation-service.test.ts index 030db00482..84130b89ba 100644 --- a/api/src/services/observation-service.test.ts +++ b/api/src/services/observation-service.test.ts @@ -4,6 +4,7 @@ import sinonChai from 'sinon-chai'; import { InsertObservation, ObservationRecord, + ObservationRecordWithSamplingData, ObservationRepository, UpdateObservation } from '../repositories/observation-repository'; @@ -108,11 +109,11 @@ describe('ObservationService', () => { }); }); - describe('getSurveyObservationsWithSupplementaryData', () => { + describe('getSurveyObservationsWithSupplementaryAndSamplingData', () => { it('Gets observations by survey id', async () => { const mockDBConnection = getMockDBConnection(); - const mockObservations: ObservationRecord[] = [ + const mockObservations: ObservationRecordWithSamplingData[] = [ { survey_observation_id: 11, survey_id: 1, @@ -127,6 +128,9 @@ describe('ObservationService', () => { update_date: null, update_user: null, revision_count: 0, + survey_sample_method_name: 'METHOD_NAME', + survey_sample_period_start_datetime: '2000-01-01 00:00:00', + survey_sample_site_name: 'SITE_NAME', survey_sample_site_id: 1, survey_sample_method_id: 1, survey_sample_period_id: 1 @@ -145,6 +149,9 @@ describe('ObservationService', () => { update_date: '2023-04-04', update_user: 2, revision_count: 1, + survey_sample_method_name: 'METHOD_NAME', + survey_sample_period_start_datetime: '2000-01-01 00:00:00', + survey_sample_site_name: 'SITE_NAME', survey_sample_site_id: 1, survey_sample_method_id: 1, survey_sample_period_id: 1 @@ -156,7 +163,7 @@ describe('ObservationService', () => { }; const getSurveyObservationsStub = sinon - .stub(ObservationRepository.prototype, 'getSurveyObservations') + .stub(ObservationRepository.prototype, 'getSurveyObservationsWithSamplingData') .resolves(mockObservations); const getSurveyObservationSupplementaryDataStub = sinon @@ -167,7 +174,7 @@ describe('ObservationService', () => { const observationService = new ObservationService(mockDBConnection); - const response = await observationService.getSurveyObservationsWithSupplementaryData(surveyId); + const response = await observationService.getSurveyObservationsWithSupplementaryAndSamplingData(surveyId); expect(getSurveyObservationsStub).to.be.calledOnceWith(surveyId); expect(getSurveyObservationSupplementaryDataStub).to.be.calledOnceWith(surveyId); diff --git a/api/src/services/platform-service.test.ts b/api/src/services/platform-service.test.ts index 1f0cb7e76a..609520a2b7 100644 --- a/api/src/services/platform-service.test.ts +++ b/api/src/services/platform-service.test.ts @@ -3,6 +3,7 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; +import { ObservationRecord } from '../repositories/observation-repository'; import { getMockDBConnection } from '../__mocks__/db'; import { AttachmentService } from './attachment-service'; import { HistoryPublishService } from './history-publish-service'; @@ -129,9 +130,9 @@ describe('PlatformService', () => { .stub(SurveyService.prototype, 'getSurveyPurposeAndMethodology') .resolves({ additional_details: 'a description of the purpose' } as any); - const getSurveyObservationsWithSupplementaryDataStub = sinon - .stub(ObservationService.prototype, 'getSurveyObservationsWithSupplementaryData') - .resolves({ surveyObservations: [{ survey_observation_id: 2 } as any], supplementaryData: [] } as any); + const getAllSurveyObservationsStub = sinon + .stub(ObservationService.prototype, 'getAllSurveyObservations') + .resolves([({ survey_observation_id: 2 } as unknown) as ObservationRecord]); const getSurveyLocationsDataStub = sinon .stub(SurveyService.prototype, 'getSurveyLocationsData') @@ -141,7 +142,7 @@ describe('PlatformService', () => { expect(getSurveyDataStub).to.have.been.calledOnceWith(1); expect(getSurveyPurposeAndMethodologyStub).to.have.been.calledOnceWith(1); - expect(getSurveyObservationsWithSupplementaryDataStub).to.have.been.calledOnceWith(1); + expect(getAllSurveyObservationsStub).to.have.been.calledOnceWith(1); expect(getSurveyLocationsDataStub).to.have.been.calledOnceWith(1); expect(response).to.eql({ id: '1', diff --git a/app/src/contexts/observationsContext.tsx b/app/src/contexts/observationsContext.tsx index f2d0e0304d..566686bff3 100644 --- a/app/src/contexts/observationsContext.tsx +++ b/app/src/contexts/observationsContext.tsx @@ -31,8 +31,6 @@ export const ObservationsContextProvider = (props: PropsWithChildren { cacheTaxonomicData(); }, [observationsContext.observationsDataLoader.data]); + /** + * Because Telemetry data is client-side paginated, we can collect all spatial points from + * traversing the array of telemetry data. + */ const telemetryPoints: INonEditableGeometries[] = useMemo(() => { const telemetryData = telemetryContext.telemetryDataLoader.data; if (!telemetryData) { @@ -80,6 +84,10 @@ const SurveySpatialData = () => { }); }, [telemetryContext.telemetryDataLoader.data]); + /** + * Because Observations data is server-side paginated, we must collect all spatial points from + * a dedicated endpoint. + */ const observationPoints: INonEditableGeometries[] = useMemo(() => { return (observationsGeometryDataLoader.data?.surveyObservationsGeometry ?? []).map((observation) => { return { @@ -108,20 +116,18 @@ const SurveySpatialData = () => { surveyContext.critterDataLoader.isLoading; } - let mapPoints: INonEditableGeometries[] = []; - switch (activeView) { - case SurveySpatialDatasetViewEnum.OBSERVATIONS: - mapPoints = observationPoints; - break; - case SurveySpatialDatasetViewEnum.TELEMETRY: - mapPoints = telemetryPoints; - break; - case SurveySpatialDatasetViewEnum.MARKED_ANIMALS: - mapPoints = []; - break; - default: - break; - } + const mapPoints: INonEditableGeometries[] = useMemo(() => { + switch (activeView) { + case SurveySpatialDatasetViewEnum.OBSERVATIONS: + return observationPoints; + case SurveySpatialDatasetViewEnum.TELEMETRY: + return telemetryPoints; + case SurveySpatialDatasetViewEnum.MARKED_ANIMALS: + default: + return []; + } + }, [activeView, observationPoints, telemetryPoints]) + return ( diff --git a/app/src/hooks/api/useObservationApi.ts b/app/src/hooks/api/useObservationApi.ts index 4325489f57..8bc724eb5d 100644 --- a/app/src/hooks/api/useObservationApi.ts +++ b/app/src/hooks/api/useObservationApi.ts @@ -73,7 +73,14 @@ const useObservationApi = (axios: AxiosInstance) => { return data; }; - // TODO promise type; jsdoc. + /** + * Fetches all geojson geometry points for all observation records belonging to + * the given survey. + * + * @param {number} projectId + * @param {number} surveyId + * @return {*} {Promise} + */ const getObservationsGeometry = async ( projectId: number, surveyId: number From 3feddaee779ba339edb8fc6933be523d05dbf08a Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Mon, 29 Jan 2024 12:36:36 -0800 Subject: [PATCH 76/85] SIMSBIOHUB-445: Fix app tests --- .../list/FundingSourcesTable.test.tsx | 4 ++-- .../list/FundingSourcesTableNoRowsOverlay.tsx | 12 ------------ 2 files changed, 2 insertions(+), 14 deletions(-) delete mode 100644 app/src/features/funding-sources/list/FundingSourcesTableNoRowsOverlay.tsx diff --git a/app/src/features/funding-sources/list/FundingSourcesTable.test.tsx b/app/src/features/funding-sources/list/FundingSourcesTable.test.tsx index bc6d610c15..1c7d673c9e 100644 --- a/app/src/features/funding-sources/list/FundingSourcesTable.test.tsx +++ b/app/src/features/funding-sources/list/FundingSourcesTable.test.tsx @@ -13,12 +13,12 @@ describe('FundingSourcesTable', () => { const onEdit = jest.fn(); const onDelete = jest.fn(); - const { getByTestId } = render( + const { getByText } = render( ); await waitFor(() => { - expect(getByTestId('funding-source-table-empty')).toBeVisible(); + expect(getByText('No Funding Sources available')).toBeVisible(); }); }); diff --git a/app/src/features/funding-sources/list/FundingSourcesTableNoRowsOverlay.tsx b/app/src/features/funding-sources/list/FundingSourcesTableNoRowsOverlay.tsx deleted file mode 100644 index d33fe67456..0000000000 --- a/app/src/features/funding-sources/list/FundingSourcesTableNoRowsOverlay.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import Typography from '@mui/material/Typography'; -import { GridOverlay } from '@mui/x-data-grid'; - -const NoRowsOverlay = (props: { className: string }) => ( - - - No funding sources found - - -); - -export default NoRowsOverlay; From 477b0f5467385d3a05251fdceafdd7a8fa10ee3d Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Mon, 29 Jan 2024 13:01:19 -0800 Subject: [PATCH 77/85] SIMSBIOHUB-445: Remove uuid() from survey map --- app/src/features/surveys/view/SurveyMap.tsx | 22 ++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/app/src/features/surveys/view/SurveyMap.tsx b/app/src/features/surveys/view/SurveyMap.tsx index f41fc7c5df..b31e9fdffe 100644 --- a/app/src/features/surveys/view/SurveyMap.tsx +++ b/app/src/features/surveys/view/SurveyMap.tsx @@ -8,12 +8,12 @@ import { LatLngBoundsExpression } from 'leaflet'; import { GeoJSON, LayersControl, MapContainer as LeafletMapContainer } from 'react-leaflet'; import { calculateUpdatedMapBounds } from 'utils/mapBoundaryUploadHelpers'; import { coloredPoint, INonEditableGeometries } from 'utils/mapUtils'; -import { v4 as uuidv4 } from 'uuid'; interface ISurveyMapProps { mapPoints: INonEditableGeometries[]; isLoading: boolean; } + // TODO: need a way to pass in the map dimensions depending on the screen size const SurveyMap = (props: ISurveyMapProps) => { let bounds: LatLngBoundsExpression | undefined; @@ -38,14 +38,18 @@ const SurveyMap = (props: ISurveyMapProps) => { - {props.mapPoints?.map((nonEditableGeo: INonEditableGeometries) => ( - coloredPoint({ latlng, fillColor: '#1f7dff', borderColor: '#ffffff' })}> - {nonEditableGeo.popupComponent} - - ))} + {props.mapPoints?.map((nonEditableGeo: INonEditableGeometries, index: number) => { + const key = nonEditableGeo.feature.id ?? index; + + return ( + coloredPoint({ latlng, fillColor: '#1f7dff', borderColor: '#ffffff' })}> + {nonEditableGeo.popupComponent} + + ); + })} From 60a89bae5a8a8e9bd0316e14dc75cda928ab8be1 Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Mon, 29 Jan 2024 13:12:37 -0800 Subject: [PATCH 78/85] SIMSBIOHUB-445: Code cleanup; Linter. --- .../ManualTelemetryTableContainer.tsx | 12 ++---------- app/src/features/surveys/view/SurveyPage.tsx | 2 +- .../components/spatial-data/SurveySpatialData.tsx | 5 ++--- .../SurveySpatialObservationDataTable.tsx | 15 ++++++++------- 4 files changed, 13 insertions(+), 21 deletions(-) diff --git a/app/src/features/surveys/telemetry/telemetry-table/ManualTelemetryTableContainer.tsx b/app/src/features/surveys/telemetry/telemetry-table/ManualTelemetryTableContainer.tsx index d43ae4229d..933b5f2e9f 100644 --- a/app/src/features/surveys/telemetry/telemetry-table/ManualTelemetryTableContainer.tsx +++ b/app/src/features/surveys/telemetry/telemetry-table/ManualTelemetryTableContainer.tsx @@ -200,12 +200,7 @@ const ManualTelemetryTableContainer = () => { - + - + diff --git a/app/src/features/surveys/view/components/spatial-data/SurveySpatialData.tsx b/app/src/features/surveys/view/components/spatial-data/SurveySpatialData.tsx index b0124f3990..b2612ada7e 100644 --- a/app/src/features/surveys/view/components/spatial-data/SurveySpatialData.tsx +++ b/app/src/features/surveys/view/components/spatial-data/SurveySpatialData.tsx @@ -10,10 +10,10 @@ import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; import { useContext, useEffect, useMemo, useState } from 'react'; import { INonEditableGeometries } from 'utils/mapUtils'; -import SurveySpatialToolbar, { SurveySpatialDatasetViewEnum } from './SurveySpatialToolbar'; import SurveyMap from '../../SurveyMap'; import SurveySpatialObservationDataTable from './SurveySpatialObservationDataTable'; import SurveySpatialTelemetryDataTable from './SurveySpatialTelemetryDataTable'; +import SurveySpatialToolbar, { SurveySpatialDatasetViewEnum } from './SurveySpatialToolbar'; const SurveySpatialData = () => { const [activeView, setActiveView] = useState(SurveySpatialDatasetViewEnum.OBSERVATIONS); @@ -126,8 +126,7 @@ const SurveySpatialData = () => { default: return []; } - }, [activeView, observationPoints, telemetryPoints]) - + }, [activeView, observationPoints, telemetryPoints]); return ( diff --git a/app/src/features/surveys/view/components/spatial-data/SurveySpatialObservationDataTable.tsx b/app/src/features/surveys/view/components/spatial-data/SurveySpatialObservationDataTable.tsx index 847bd6d2c4..7163082602 100644 --- a/app/src/features/surveys/view/components/spatial-data/SurveySpatialObservationDataTable.tsx +++ b/app/src/features/surveys/view/components/spatial-data/SurveySpatialObservationDataTable.tsx @@ -11,6 +11,9 @@ import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; import { useContext, useEffect, useState } from 'react'; +// Set height so we the skeleton loader will match table rows +const rowHeight = 52; + interface IObservationTableRow { survey_observation_id: number; itis_scientific_name: string | undefined; @@ -24,9 +27,11 @@ interface IObservationTableRow { latitude: number | null; longitude: number | null; } + interface ISurveySpatialObservationDataTableProps { isLoading: boolean; } + const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataTableProps) => { const biohubApi = useBiohubApi(); const surveyContext = useContext(SurveyContext); @@ -141,9 +146,6 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT } ]; - // Set height so we the skeleton loader will match table rows - const RowHeight = 52; - // Skeleton Loader template const SkeletonRow = () => ( ); - console.log(`Page: ${page} Page Size: ${pageSize}`); return ( <> {props.isLoading ? ( @@ -190,8 +191,8 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT ) : ( Date: Mon, 29 Jan 2024 15:57:05 -0800 Subject: [PATCH 79/85] SIMSBIOHUB-445: Some changes to pagination required fields; Added test coverage to observations/index.ts --- .../{surveyId}/observations/index.test.ts | 209 ++++++++++++++++-- .../survey/{surveyId}/observations/index.ts | 6 +- app/src/contexts/observationsTableContext.tsx | 16 +- .../interfaces/useObservationApi.interface.ts | 4 +- 4 files changed, 209 insertions(+), 26 deletions(-) diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.test.ts index 1cf45e3e59..f0d96976f1 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.test.ts @@ -557,7 +557,7 @@ describe('insertUpdateSurveyObservations', () => { }); }); -describe('getSurveyObservationsWithSupplementaryData', () => { +describe('getSurveyObservations', () => { afterEach(() => { sinon.restore(); }); @@ -638,7 +638,15 @@ describe('getSurveyObservationsWithSupplementaryData', () => { it('returns an empty array', () => { const apiResponse = { surveyObservations: [], - supplementaryObservationData: { observationCount: 0 } + supplementaryObservationData: { observationCount: 0 }, + pagination: { + total: 0, + per_page: undefined, + current_page: 1, + last_page: 1, + sort: undefined, + order: undefined + } }; const response = responseValidator.validateResponse(200, apiResponse); @@ -664,7 +672,15 @@ describe('getSurveyObservationsWithSupplementaryData', () => { revision_count: 1 } ], - supplementaryObservationData: { observationCount: 1 } + supplementaryObservationData: { observationCount: 1 }, + pagination: { + total: 1, + per_page: undefined, + current_page: 1, + last_page: 1, + sort: undefined, + order: undefined + } }; const response = responseValidator.validateResponse(200, apiResponse); @@ -692,7 +708,15 @@ describe('getSurveyObservationsWithSupplementaryData', () => { revision_count: 1 } ], - supplementaryObservationData: { observationCount: 1 } + supplementaryObservationData: { observationCount: 1 }, + pagination: { + total: 1, + per_page: undefined, + current_page: 1, + last_page: 1, + sort: undefined, + order: undefined + } }; const response = responseValidator.validateResponse(200, apiResponse); @@ -721,7 +745,15 @@ describe('getSurveyObservationsWithSupplementaryData', () => { revision_count: 1 } ], - supplementaryObservationData: { observationCount: 1 } + supplementaryObservationData: { observationCount: 1 }, + pagination: { + total: 1, + per_page: undefined, + current_page: 1, + last_page: 1, + sort: undefined, + order: undefined + } }; const response = responseValidator.validateResponse(200, apiResponse); @@ -750,7 +782,15 @@ describe('getSurveyObservationsWithSupplementaryData', () => { revision_count: 1 } ], - supplementaryObservationData: { observationCount: 1 } + supplementaryObservationData: { observationCount: 1 }, + pagination: { + total: 1, + per_page: undefined, + current_page: 1, + last_page: 1, + sort: undefined, + order: undefined + } }; const response = responseValidator.validateResponse(200, apiResponse); @@ -779,7 +819,15 @@ describe('getSurveyObservationsWithSupplementaryData', () => { revision_count: 1 } ], - supplementaryObservationData: { observationCount: 1 } + supplementaryObservationData: { observationCount: 1 }, + pagination: { + total: 1, + per_page: undefined, + current_page: 1, + last_page: 1, + sort: undefined, + order: undefined + } }; const response = responseValidator.validateResponse(200, apiResponse); @@ -808,7 +856,15 @@ describe('getSurveyObservationsWithSupplementaryData', () => { revision_count: 1 } ], - supplementaryObservationData: { observationCount: 1 } + supplementaryObservationData: { observationCount: 1 }, + pagination: { + total: 1, + per_page: undefined, + current_page: 1, + last_page: 1, + sort: undefined, + order: undefined + } }; const response = responseValidator.validateResponse(200, apiResponse); @@ -818,11 +874,134 @@ describe('getSurveyObservationsWithSupplementaryData', () => { expect(response.errors[0].path).to.equal('surveyObservations/0'); expect(response.errors[0].message).to.equal(`must have required property 'count'`); }); + + it('is missing pagination', async () => { + const apiResponse = { + surveyObservations: [ + { + survey_observation_id: 1, + wldtaxonomic_units_id: 1234, + count: 99, + latitude: 48.103322, + longitude: -122.798892, + observation_date: '1970-01-01', + observation_time: '00:00:00', + create_user: 1, + create_date: '1970-01-01', + update_user: 1, + update_date: '1970-01-01', + revision_count: 1 + } + ], + supplementaryObservationData: { observationCount: 1 } + }; + + const response = responseValidator.validateResponse(200, apiResponse); + + expect(response.message).to.equal('The response was not valid.'); + expect(response.errors.length).to.equal(1); + expect(response.errors[0].path).to.equal('response'); + expect(response.errors[0].message).to.equal(`must have required property 'pagination'`); + }); + }); + }); + }); + + it('retrieves survey observations with pagination', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + const getSurveyObservationsStub = sinon + .stub(ObservationService.prototype, 'getSurveyObservationsWithSupplementaryAndSamplingData') + .resolves({ + surveyObservations: ([ + { survey_observation_id: 11 }, + { survey_observation_id: 12 } + ] as unknown) as ObservationRecord[], + supplementaryObservationData: { observationCount: 59 } }); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + + mockReq.query = { + page: '4', + limit: '10', + sort: 'count', + order: 'asc' + }; + + const requestHandler = observationRecords.getSurveyObservations(); + await requestHandler(mockReq, mockRes, mockNext); + + expect(getSurveyObservationsStub).to.have.been.calledOnceWith(2); + expect(mockRes.statusValue).to.equal(200); + expect(mockRes.jsonValue).to.eql({ + surveyObservations: [{ survey_observation_id: 11 }, { survey_observation_id: 12 }], + supplementaryObservationData: { observationCount: 59 }, + pagination: { + total: 59, + current_page: 4, + last_page: 6, + order: 'asc', + per_page: 10, + sort: 'count' + } + }); + }); + + it('retrieves survey observations with some pagination options', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + const getSurveyObservationsStub = sinon + .stub(ObservationService.prototype, 'getSurveyObservationsWithSupplementaryAndSamplingData') + .resolves({ + surveyObservations: ([ + { survey_observation_id: 16 }, + { survey_observation_id: 17 } + ] as unknown) as ObservationRecord[], + supplementaryObservationData: { observationCount: 50 } + }); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + + mockReq.query = { + page: '2', + limit: '15' + }; + + const requestHandler = observationRecords.getSurveyObservations(); + await requestHandler(mockReq, mockRes, mockNext); + + expect(getSurveyObservationsStub).to.have.been.calledOnceWith(2); + expect(mockRes.statusValue).to.equal(200); + expect(mockRes.jsonValue).to.eql({ + surveyObservations: [{ survey_observation_id: 16 }, { survey_observation_id: 17 }], + supplementaryObservationData: { observationCount: 50 }, + pagination: { + total: 50, + current_page: 2, + last_page: 4, + order: undefined, + per_page: 15, + sort: undefined + } }); }); - it('retrieves survey observations', async () => { + it('retrieves survey observations with no pagination', async () => { const dbConnectionObj = getMockDBConnection(); sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); @@ -831,8 +1010,8 @@ describe('getSurveyObservationsWithSupplementaryData', () => { .stub(ObservationService.prototype, 'getSurveyObservationsWithSupplementaryAndSamplingData') .resolves({ surveyObservations: ([ - { survey_observation_id: 1 }, - { survey_observation_id: 2 } + { survey_observation_id: 16 }, + { survey_observation_id: 17 } ] as unknown) as ObservationRecord[], supplementaryObservationData: { observationCount: 2 } }); @@ -850,15 +1029,15 @@ describe('getSurveyObservationsWithSupplementaryData', () => { expect(getSurveyObservationsStub).to.have.been.calledOnceWith(2); expect(mockRes.statusValue).to.equal(200); expect(mockRes.jsonValue).to.eql({ - surveyObservations: [{ survey_observation_id: 1 }, { survey_observation_id: 2 }], + surveyObservations: [{ survey_observation_id: 16 }, { survey_observation_id: 17 }], supplementaryObservationData: { observationCount: 2 }, pagination: { + total: 2, current_page: 1, last_page: 1, order: undefined, - per_page: 2, - sort: undefined, - total: 2 + per_page: undefined, + sort: undefined } }); }); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts index 8b4c4670c7..dac9586469 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts @@ -139,7 +139,7 @@ export const surveyObservationsResponseSchema: SchemaObject = { const paginationResponseSchema: SchemaObject = { type: 'object', - required: ['total', 'per_page', 'current_page', 'last_page'], + required: ['total', 'current_page', 'last_page'], properties: { total: { type: 'integer' @@ -229,7 +229,7 @@ GET.apiDoc = { 'application/json': { schema: { ...surveyObservationsResponseSchema, - required: ['surveyObservations', 'supplementaryObservationData'], + required: ['surveyObservations', 'supplementaryObservationData', 'pagination'], properties: { ...surveyObservationsResponseSchema.properties, supplementaryObservationData: { ...surveyObservationsSupplementaryData }, @@ -408,7 +408,7 @@ export function getSurveyObservations(): RequestHandler { ...observationData, pagination: { total: observationCount, - per_page: limit ?? observationCount, + per_page: limit, current_page: page ?? 1, last_page: limit ? Math.ceil(observationCount / limit) : 1, sort, diff --git a/app/src/contexts/observationsTableContext.tsx b/app/src/contexts/observationsTableContext.tsx index 133a93a297..353070951d 100644 --- a/app/src/contexts/observationsTableContext.tsx +++ b/app/src/contexts/observationsTableContext.tsx @@ -212,9 +212,7 @@ export const ObservationsTableContextProvider = (props: PropsWithChildren({}); - // Pagination States - - // const [totalRows, setTotalRows] = useState(0); + // Pagination State const [paginationModel, setPaginationModel] = useState({ page: 0, pageSize: 10 @@ -232,8 +230,10 @@ export const ObservationsTableContextProvider = (props: PropsWithChildren { observationsContext.observationsDataLoader.refresh({ - page: paginationModel.page + 1, // +1 to correct an off by one error with pagination - limit: paginationModel.pageSize + limit: paginationModel.pageSize, + + // API pagination pages begin at 1, but MUI DataGrid pagination begins at 0. + page: paginationModel.page + 1 }); }, []); @@ -241,10 +241,12 @@ export const ObservationsTableContextProvider = (props: PropsWithChildren { const sort = firstOrNull(sortModel); observationsContext.observationsDataLoader.refresh({ - page: paginationModel.page + 1, // +1 to correct an off by one error with pagination limit: paginationModel.pageSize, sort: sort?.field || undefined, - order: sort?.sort || undefined + order: sort?.sort || undefined, + + // API pagination pages begin at 1, but MUI DataGrid pagination begins at 0. + page: paginationModel.page + 1 }); }, [paginationModel, sortModel]); diff --git a/app/src/interfaces/useObservationApi.interface.ts b/app/src/interfaces/useObservationApi.interface.ts index 37b6aad721..429f53408b 100644 --- a/app/src/interfaces/useObservationApi.interface.ts +++ b/app/src/interfaces/useObservationApi.interface.ts @@ -5,9 +5,11 @@ export interface IGetSurveyObservationsResponse { supplementaryObservationData: ISupplementaryObservationData; pagination: { total: number; - per_page: number; current_page: number; last_page: number; + per_page?: number; + sort?: string; + order?: 'asc' | 'desc'; }; } From b72e67eeefcf730dfb969ee2f24e478506a2ba2f Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Mon, 29 Jan 2024 17:40:16 -0800 Subject: [PATCH 80/85] SIMSBIOHUB-414: Added metadata endpoint for survey observations --- .../survey/{surveyId}/observations/index.ts | 6 +- .../{surveyObservationId}/index.ts | 189 ++++++++++++++++++ .../repositories/observation-repository.ts | 23 +++ api/src/services/observation-service.ts | 11 + 4 files changed, 226 insertions(+), 3 deletions(-) create mode 100644 api/src/paths/project/{projectId}/survey/{surveyId}/observations/{surveyObservationId}/index.ts diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts index dac9586469..7b9cd97731 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts @@ -178,7 +178,7 @@ GET.apiDoc = { in: 'path', name: 'projectId', schema: { - type: 'number', + type: 'integer', minimum: 1 }, required: true @@ -187,7 +187,7 @@ GET.apiDoc = { in: 'path', name: 'surveyId', schema: { - type: 'number', + type: 'integer', minimum: 1 }, required: true @@ -303,7 +303,7 @@ PUT.apiDoc = { oneOf: [{ type: 'integer' }, { type: 'string' }] }, count: { - type: 'number' + type: 'integer' }, latitude: { type: 'number' diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/{surveyObservationId}/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/{surveyObservationId}/index.ts new file mode 100644 index 0000000000..a490e90427 --- /dev/null +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/{surveyObservationId}/index.ts @@ -0,0 +1,189 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { PROJECT_PERMISSION, SYSTEM_ROLE } from '../../../../../../../constants/roles'; +import { getDBConnection } from '../../../../../../../database/db'; +import { authorizeRequestHandler } from '../../../../../../../request-handlers/security/authorization'; +import { ObservationService } from '../../../../../../../services/observation-service'; +import { getLogger } from '../../../../../../../utils/logger'; + +const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/observation/{surveyObservationId}'); + +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + or: [ + { + validProjectPermissions: [ + PROJECT_PERMISSION.COORDINATOR, + PROJECT_PERMISSION.COLLABORATOR, + PROJECT_PERMISSION.OBSERVER + ], + surveyId: Number(req.params.surveyId), + discriminator: 'ProjectPermission' + }, + { + validSystemRoles: [SYSTEM_ROLE.DATA_ADMINISTRATOR], + discriminator: 'SystemRole' + } + ] + }; + }), + getSurveyObservation() +]; + +GET.apiDoc = { + description: 'Get single observation for the survey.', + tags: ['observation'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'path', + name: 'projectId', + schema: { + type: 'integer', + minimum: 1 + }, + required: true + }, + { + in: 'path', + name: 'surveyId', + schema: { + type: 'integer', + minimum: 1 + }, + required: true + }, + { + in: 'path', + name: 'surveyObservationId', + schema: { + type: 'integer', + minimum: 1 + }, + required: true + } + ], + responses: { + 200: { + description: 'Survey Observations get response.', + content: { + 'application/json': { + schema: { + type: 'object', + required: [ + 'survey_observation_id', + 'wldtaxonomic_units_id', + 'latitude', + 'longitude', + 'count', + 'observation_date', + 'observation_time', + 'create_user', + 'create_date', + 'update_user', + 'update_date', + 'revision_count' + ], + properties: { + survey_observation_id: { + type: 'integer' + }, + wldtaxonomic_units_id: { + type: 'integer' + }, + latitude: { + type: 'number' + }, + longitude: { + type: 'number' + }, + count: { + type: 'integer' + }, + observation_date: { + type: 'string' + }, + observation_time: { + type: 'string' + }, + create_date: { + oneOf: [{ type: 'object' }, { type: 'string', format: 'date' }], + description: 'ISO 8601 date string for the project start date' + }, + create_user: { + type: 'integer', + minimum: 1 + }, + update_date: { + oneOf: [{ type: 'object' }, { type: 'string', format: 'date' }], + description: 'ISO 8601 date string for the project start date', + nullable: true + }, + update_user: { + type: 'integer', + minimum: 1, + nullable: true + }, + revision_count: { + type: 'integer', + minimum: 0 + } + } + } + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +/** + * Fetch all observations for a survey. + * + * @export + * @return {*} {RequestHandler} + */ +export function getSurveyObservation(): RequestHandler { + return async (req, res) => { + const surveyObservationId = Number(req.params.surveyObservationId); + + defaultLog.debug({ label: 'getSurveyObservation', surveyObservationId }); + + const connection = getDBConnection(req['keycloak_token']); + + try { + await connection.open(); + + const observationService = new ObservationService(connection); + + const observationData = await observationService.getSurveyObservationById(surveyObservationId); + + return res.status(200).json(observationData); + } catch (error) { + defaultLog.error({ label: 'getSurveyObservation', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/repositories/observation-repository.ts b/api/src/repositories/observation-repository.ts index b91f65eee0..d167840b48 100644 --- a/api/src/repositories/observation-repository.ts +++ b/api/src/repositories/observation-repository.ts @@ -306,6 +306,29 @@ export class ObservationRepository extends BaseRepository { return response.rows; } + /** + * Retrieves a single observation record + * + * @param {number} surveyId + * @return {*} {Promise} + * @memberof ObservationRepository + */ + async getSurveyObservationById(surveyObservationId: number): Promise { + const knex = getKnex(); + const query = knex.queryBuilder().select('*').from('survey_observation').where('survey_observation_id', surveyObservationId); + + const response = await this.connection.knex(query, ObservationRecord); + + if (!response.rowCount) { + throw new ApiExecuteSQLError('Failed to get observation record', [ + 'ObservationRepository->getSurveyObservationById', + 'rowCount was null or undefined, expected rowCount = 1' + ]); + } + + return response.rows[0]; + } + /** * Retrieves all observation records for the given survey * diff --git a/api/src/services/observation-service.ts b/api/src/services/observation-service.ts index 157621f01d..a59341be15 100644 --- a/api/src/services/observation-service.ts +++ b/api/src/services/observation-service.ts @@ -123,6 +123,17 @@ export class ObservationService extends DBService { return this.observationRepository.getAllSurveyObservations(surveyId); } + /** + * Retrieves a single observation records by ID + * + * @param {number} surveyId + * @return {*} {Promise} + * @memberof ObservationRepository + */ + async getSurveyObservationById(surveyObservationId: number): Promise { + return this.observationRepository.getSurveyObservationById(surveyObservationId); + } + /** * Retrieves all observation records for the given survey along with supplementary data * From f121bfd7d3fb96097858eccef92b9a74e1884b74 Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Tue, 30 Jan 2024 13:44:42 -0800 Subject: [PATCH 81/85] SIMSBIOHUB-414: Added hook call to fetch single observation record --- .../surveys/observations/ObservationsMap.tsx | 228 ------------------ app/src/hooks/api/useObservationApi.ts | 21 ++ 2 files changed, 21 insertions(+), 228 deletions(-) delete mode 100644 app/src/features/surveys/observations/ObservationsMap.tsx diff --git a/app/src/features/surveys/observations/ObservationsMap.tsx b/app/src/features/surveys/observations/ObservationsMap.tsx deleted file mode 100644 index c450e29f7f..0000000000 --- a/app/src/features/surveys/observations/ObservationsMap.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import { mdiRefresh } from '@mdi/js'; -import Icon from '@mdi/react'; -import { Button, IconButton } from '@mui/material'; -import Box from '@mui/material/Box'; -import BaseLayerControls from 'components/map/components/BaseLayerControls'; -import { SetMapBounds } from 'components/map/components/Bounds'; -import FullScreenScrollingEventHandler from 'components/map/components/FullScreenScrollingEventHandler'; -import StaticLayers from 'components/map/components/StaticLayers'; -import { MapBaseCss } from 'components/map/styles/MapBaseCss'; -import { ALL_OF_BC_BOUNDARY, MAP_DEFAULT_CENTER } from 'constants/spatial'; -import { ObservationsContext } from 'contexts/observationsContext'; -import { SurveyContext } from 'contexts/surveyContext'; -import { Feature, Position } from 'geojson'; -import { useBiohubApi } from 'hooks/useBioHubApi'; -import { LatLngBoundsExpression } from 'leaflet'; -import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; -import { GeoJSON, LayersControl, MapContainer as LeafletMapContainer, Popup } from 'react-leaflet'; -import { Link as RouterLink } from 'react-router-dom'; -import { calculateUpdatedMapBounds } from 'utils/mapBoundaryUploadHelpers'; -import { coloredPoint, INonEditableGeometries } from 'utils/mapUtils'; -import { v4 as uuidv4 } from 'uuid'; - -const ObservationsMap = () => { - const biohubApi = useBiohubApi(); - const observationsContext = useContext(ObservationsContext); - const surveyContext = useContext(SurveyContext); - const [speciesNames, setSpeciesNames] = useState<{ id: string; label: string }[]>([]); - - const handleGetSpecies = useCallback( - async (taxonomic_ids: number[]) => { - const response = await biohubApi.taxonomy.getSpeciesFromIds(taxonomic_ids); - - setSpeciesNames(response.searchResponse); - }, - [biohubApi.taxonomy] - ); - - const speciesIds = useMemo(() => { - const observations = observationsContext.observationsDataLoader.data?.surveyObservations; - - if (!observations) { - return []; - } - - return observations.map((observation) => observation.wldtaxonomic_units_id); - }, [observationsContext.observationsDataLoader.data]); - - useEffect(() => { - handleGetSpecies(speciesIds); - }, [handleGetSpecies, speciesIds]); - - const handleCheckSpeciesName = useCallback( - (id: number) => { - const speciesName = speciesNames.find((item) => Number(item.id) === id); - - return speciesName ? speciesName.label : ''; - }, - [speciesNames] - ); - - const surveyObservations: INonEditableGeometries[] = useMemo(() => { - const observations = observationsContext.observationsDataLoader.data?.surveyObservations; - - if (!observations) { - return []; - } - - return observations - .filter((observation) => observation.latitude !== undefined && observation.longitude !== undefined) - .map((observation) => { - const link = observation.survey_observation_id - ? `observations/#view-${observation.survey_observation_id}` - : 'observations'; - - return { - feature: { - type: 'Feature', - properties: {}, - geometry: { - type: 'Point', - coordinates: [observation.longitude, observation.latitude] as Position - } - }, - popupComponent: ( - -
    {(speciesNames.length && handleCheckSpeciesName(observation.wldtaxonomic_units_id)) || ''}
    - -
    - ) - }; - }); - }, [observationsContext.observationsDataLoader.data, handleCheckSpeciesName, speciesNames]); - - const studyAreaFeatures: { geojson: Feature; name: string }[] = useMemo(() => { - const locations = surveyContext.surveyDataLoader.data?.surveyData.locations; - if (!locations) { - return []; - } - - return locations.flatMap((item) => { - return item.geojson.map((geojson) => { - return { - geojson, - name: item.name - }; - }); - }); - }, [surveyContext.surveyDataLoader.data]); - - const sampleSiteFeatures: { geojson: Feature; name: string }[] = useMemo(() => { - const sites = surveyContext.sampleSiteDataLoader.data?.sampleSites; - if (!sites) { - return []; - } - return sites.map((item) => { - return { - geojson: item.geojson, - name: item.name - }; - }); - }, [surveyContext.sampleSiteDataLoader.data]); - - const getDefaultMapBounds = useCallback((): LatLngBoundsExpression | undefined => { - const features = surveyObservations.map((observation) => observation.feature); - - const studyAreaFeaturesGeoJSON = studyAreaFeatures.map((item) => item.geojson); - const sampleSiteFeaturesGeoJSON = sampleSiteFeatures.map((item) => item.geojson); - - return calculateUpdatedMapBounds([...features, ...studyAreaFeaturesGeoJSON, ...sampleSiteFeaturesGeoJSON]); - }, [surveyObservations, studyAreaFeatures, sampleSiteFeatures]); - - // set default bounds to encompass all of BC - const [bounds, setBounds] = useState( - calculateUpdatedMapBounds([ALL_OF_BC_BOUNDARY]) - ); - - const zoomToBoundaryExtent = useCallback(() => { - setBounds(getDefaultMapBounds()); - }, [getDefaultMapBounds]); - - useEffect(() => { - // Once all data loaders have finished loading it will zoom the map to include all features - if ( - !surveyContext.surveyDataLoader.isLoading && - !surveyContext.sampleSiteDataLoader.isLoading && - !observationsContext.observationsDataLoader.isLoading - ) { - zoomToBoundaryExtent(); - } - }, [ - observationsContext.observationsDataLoader.isLoading, - surveyContext.sampleSiteDataLoader.isLoading, - surveyContext.surveyDataLoader.isLoading, - zoomToBoundaryExtent - ]); - - return ( - <> - - - - - - {surveyObservations?.map((nonEditableGeo: INonEditableGeometries) => ( - coloredPoint({ latlng, fillColor: '#1f7dff', borderColor: '#ffffff' })}> - {nonEditableGeo.popupComponent} - - ))} - - - ({ - geoJSON: feature.geojson, - tooltip: Study Area: {feature.name} - })) - }, - { - layerName: 'Sample Sites', - layerColors: { color: '#1f7dff', fillColor: '#1f7dff' }, - features: sampleSiteFeatures.map((feature) => ({ - geoJSON: feature.geojson, - tooltip: Sampling Site: {feature.name} - })) - } - ]} - /> - - - {(surveyObservations.length > 0 || studyAreaFeatures.length > 0 || sampleSiteFeatures.length > 0) && ( - - zoomToBoundaryExtent()}> - - - - )} - - ); -}; - -export default ObservationsMap; diff --git a/app/src/hooks/api/useObservationApi.ts b/app/src/hooks/api/useObservationApi.ts index 8bc724eb5d..89e3a5c0ce 100644 --- a/app/src/hooks/api/useObservationApi.ts +++ b/app/src/hooks/api/useObservationApi.ts @@ -73,6 +73,26 @@ const useObservationApi = (axios: AxiosInstance) => { return data; }; + /** + * Retrieves all survey observation records for the given survey + * + * @param {number} projectId + * @param {number} surveyId + * @param {ApiPaginationOptions} [pagination] + * @return {*} {Promise} + */ + const getObservationRecord = async ( + projectId: number, + surveyId: number, + surveyObservationId: number + ): Promise => { + const { data } = await axios.get( + `/api/project/${projectId}/survey/${surveyId}/observations/${surveyObservationId}` + ); + + return data; + }; + /** * Fetches all geojson geometry points for all observation records belonging to * the given survey. @@ -165,6 +185,7 @@ const useObservationApi = (axios: AxiosInstance) => { return { insertUpdateObservationRecords, getObservationRecords, + getObservationRecord, getObservationsGeometry, deleteObservationRecords, uploadCsvForImport, From ea3c16dc09a0fe5af9bc66d843dfe8c0a2a4c3fc Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Tue, 30 Jan 2024 13:45:46 -0800 Subject: [PATCH 82/85] SIMSBIOHUB-414: Memoize bounds --- app/src/features/surveys/view/SurveyMap.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/src/features/surveys/view/SurveyMap.tsx b/app/src/features/surveys/view/SurveyMap.tsx index b31e9fdffe..1de8546d2c 100644 --- a/app/src/features/surveys/view/SurveyMap.tsx +++ b/app/src/features/surveys/view/SurveyMap.tsx @@ -5,6 +5,7 @@ import FullScreenScrollingEventHandler from 'components/map/components/FullScree import { MapBaseCss } from 'components/map/styles/MapBaseCss'; import { ALL_OF_BC_BOUNDARY, MAP_DEFAULT_CENTER } from 'constants/spatial'; import { LatLngBoundsExpression } from 'leaflet'; +import { useMemo } from 'react'; import { GeoJSON, LayersControl, MapContainer as LeafletMapContainer } from 'react-leaflet'; import { calculateUpdatedMapBounds } from 'utils/mapBoundaryUploadHelpers'; import { coloredPoint, INonEditableGeometries } from 'utils/mapUtils'; @@ -16,12 +17,14 @@ interface ISurveyMapProps { // TODO: need a way to pass in the map dimensions depending on the screen size const SurveyMap = (props: ISurveyMapProps) => { - let bounds: LatLngBoundsExpression | undefined; - if (props.mapPoints.length > 0) { - bounds = calculateUpdatedMapBounds(props.mapPoints.map((item) => item.feature)); - } else { - bounds = calculateUpdatedMapBounds([ALL_OF_BC_BOUNDARY]); - } + const bounds: LatLngBoundsExpression | undefined = useMemo(() => { + if (props.mapPoints.length > 0) { + return calculateUpdatedMapBounds(props.mapPoints.map((item) => item.feature)); + } else { + return calculateUpdatedMapBounds([ALL_OF_BC_BOUNDARY]); + } + }, [props.mapPoints]); + return ( <> {props.isLoading ? ( From 154de4806a06714e9319d3bb23a70e3cb97c0791 Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Tue, 30 Jan 2024 14:05:25 -0800 Subject: [PATCH 83/85] SIMSBIOHUB-445: Linting and sonarcloud changes --- .../repositories/observation-repository.ts | 6 +- api/src/services/observation-service.ts | 6 +- .../data-grid/StyledDataGridOverlay.tsx | 2 +- .../SurveySpatialTelemetryDataTable.tsx | 66 +++++++++---------- 4 files changed, 42 insertions(+), 38 deletions(-) diff --git a/api/src/repositories/observation-repository.ts b/api/src/repositories/observation-repository.ts index d167840b48..3ac66c7441 100644 --- a/api/src/repositories/observation-repository.ts +++ b/api/src/repositories/observation-repository.ts @@ -315,7 +315,11 @@ export class ObservationRepository extends BaseRepository { */ async getSurveyObservationById(surveyObservationId: number): Promise { const knex = getKnex(); - const query = knex.queryBuilder().select('*').from('survey_observation').where('survey_observation_id', surveyObservationId); + const query = knex + .queryBuilder() + .select('*') + .from('survey_observation') + .where('survey_observation_id', surveyObservationId); const response = await this.connection.knex(query, ObservationRecord); diff --git a/api/src/services/observation-service.ts b/api/src/services/observation-service.ts index a59341be15..65649db5bd 100644 --- a/api/src/services/observation-service.ts +++ b/api/src/services/observation-service.ts @@ -130,9 +130,9 @@ export class ObservationService extends DBService { * @return {*} {Promise} * @memberof ObservationRepository */ - async getSurveyObservationById(surveyObservationId: number): Promise { - return this.observationRepository.getSurveyObservationById(surveyObservationId); - } + async getSurveyObservationById(surveyObservationId: number): Promise { + return this.observationRepository.getSurveyObservationById(surveyObservationId); + } /** * Retrieves all observation records for the given survey along with supplementary data diff --git a/app/src/components/data-grid/StyledDataGridOverlay.tsx b/app/src/components/data-grid/StyledDataGridOverlay.tsx index deb197ddbf..a18f6a3a60 100644 --- a/app/src/components/data-grid/StyledDataGridOverlay.tsx +++ b/app/src/components/data-grid/StyledDataGridOverlay.tsx @@ -4,7 +4,7 @@ import { GridOverlay } from '@mui/x-data-grid'; const StyledDataGridOverlay = (props: { message?: string }) => ( - {props.message || 'No records found'} + {props.message ?? 'No records found'} ); diff --git a/app/src/features/surveys/view/components/spatial-data/SurveySpatialTelemetryDataTable.tsx b/app/src/features/surveys/view/components/spatial-data/SurveySpatialTelemetryDataTable.tsx index 6271c0fe7e..edef1f604e 100644 --- a/app/src/features/surveys/view/components/spatial-data/SurveySpatialTelemetryDataTable.tsx +++ b/app/src/features/surveys/view/components/spatial-data/SurveySpatialTelemetryDataTable.tsx @@ -23,6 +23,39 @@ interface ISurveySpatialTelemetryDataTableProps { isLoading: boolean; } +// Skeleton Loader template +const SkeletonRow = () => ( + + + + + + + + + + + +); + const SurveySpatialTelemetryDataTable = (props: ISurveySpatialTelemetryDataTableProps) => { const surveyContext = useContext(SurveyContext); @@ -71,39 +104,6 @@ const SurveySpatialTelemetryDataTable = (props: ISurveySpatialTelemetryDataTable } ]; - // Skeleton Loader template - const SkeletonRow = () => ( - - - - - - - - - - - - ); - return ( <> {props.isLoading ? ( From 1a1da255d478a51e2e5af89c2cc865417b095bb0 Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Tue, 30 Jan 2024 14:16:38 -0800 Subject: [PATCH 84/85] SIMSBIOHUB-445: More code quality changes --- .../SurveySpatialObservationDataTable.tsx | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/app/src/features/surveys/view/components/spatial-data/SurveySpatialObservationDataTable.tsx b/app/src/features/surveys/view/components/spatial-data/SurveySpatialObservationDataTable.tsx index 7163082602..23a694fab3 100644 --- a/app/src/features/surveys/view/components/spatial-data/SurveySpatialObservationDataTable.tsx +++ b/app/src/features/surveys/view/components/spatial-data/SurveySpatialObservationDataTable.tsx @@ -32,6 +32,40 @@ interface ISurveySpatialObservationDataTableProps { isLoading: boolean; } +// Skeleton Loader template +const SkeletonRow = () => ( + + + + + + + + + + + +); + const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataTableProps) => { const biohubApi = useBiohubApi(); const surveyContext = useContext(SurveyContext); @@ -146,40 +180,6 @@ const SurveySpatialObservationDataTable = (props: ISurveySpatialObservationDataT } ]; - // Skeleton Loader template - const SkeletonRow = () => ( - - - - - - - - - - - - ); - return ( <> {props.isLoading ? ( From 2f1e4e995f4ddc21ea2e436bc9f7e6c2e605fafc Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Wed, 31 Jan 2024 09:48:05 -0800 Subject: [PATCH 85/85] SIMSBIOHUB-445: PR changes --- .../survey/{surveyId}/observations/index.ts | 30 ++++++++++++------- .../survey/{surveyId}/sample-site/delete.ts | 2 +- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts index 7b9cd97731..8b6000183d 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observations/index.ts @@ -142,25 +142,31 @@ const paginationResponseSchema: SchemaObject = { required: ['total', 'current_page', 'last_page'], properties: { total: { - type: 'integer' + type: 'integer', + description: 'The total number of observation records belonging to the survey' }, per_page: { type: 'integer', - minimum: 1 + minimum: 1, + description: 'The number of records shown per page' }, current_page: { - type: 'integer' + type: 'integer', + description: 'The current page being fetched' }, last_page: { type: 'integer', - minimum: 1 + minimum: 1, + description: 'The total number of pages' }, sort: { - type: 'string' + type: 'string', + description: 'The column that is being sorted on' }, order: { type: 'string', - enum: ['asc', 'desc'] + enum: ['asc', 'desc'], + description: 'The sort order of the response' } } }; @@ -198,7 +204,8 @@ GET.apiDoc = { required: false, schema: { type: 'integer', - minimum: 1 + minimum: 1, + description: 'The current page number being fetched' } }, { @@ -208,18 +215,21 @@ GET.apiDoc = { schema: { type: 'integer', minimum: 1, - maximum: 100 + maximum: 100, + description: 'The number of records per page' } }, { in: 'query', name: 'sort', - required: false + required: false, + description: 'The column being sorted on' }, { in: 'query', name: 'order', - required: false + required: false, + description: 'The order of the sort, i.e. asc or desc' } ], responses: { diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/delete.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/delete.ts index 4198781309..c869ecb28d 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/delete.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/sample-site/delete.ts @@ -118,7 +118,7 @@ export function deleteSurveySampleSiteRecords(): RequestHandler { const response = await observationService.getObservationsCountBySampleSiteIds(surveyId, surveySampleSiteIds); if (response.observationCount > 0) { - throw new HTTP500(`Cannot delete a sample sites that is associated with an observation`); + throw new HTTP500(`Cannot delete a sampling site that is associated with an observation`); } for (const surveySampleSiteId of surveySampleSiteIds) {