From 2d29f9f1acba3d0742d2432d17738ff7ffa8aee4 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 14 Nov 2023 12:23:17 -0800 Subject: [PATCH 01/88] got basic UI section and router wired up --- app/src/features/surveys/SurveyRouter.tsx | 7 ++++ .../telemetry/ManualTelemetrySection.tsx | 33 +++++++++++++++++++ app/src/features/surveys/view/SurveyPage.tsx | 7 ++++ 3 files changed, 47 insertions(+) create mode 100644 app/src/features/surveys/telemetry/ManualTelemetrySection.tsx diff --git a/app/src/features/surveys/SurveyRouter.tsx b/app/src/features/surveys/SurveyRouter.tsx index 8fc5a80639..6911262ef2 100644 --- a/app/src/features/surveys/SurveyRouter.tsx +++ b/app/src/features/surveys/SurveyRouter.tsx @@ -33,6 +33,13 @@ const SurveyRouter: React.FC = () => { + + <>Butts + + {/* Sample Site Routes */} diff --git a/app/src/features/surveys/telemetry/ManualTelemetrySection.tsx b/app/src/features/surveys/telemetry/ManualTelemetrySection.tsx new file mode 100644 index 0000000000..21ceb365e4 --- /dev/null +++ b/app/src/features/surveys/telemetry/ManualTelemetrySection.tsx @@ -0,0 +1,33 @@ +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Divider from '@mui/material/Divider'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import { Link as RouterLink } from 'react-router-dom'; +import NoSurveySectionData from '../components/NoSurveySectionData'; + +const ManualTelemetrySection = () => { + return ( + + + + Manual Telemetry + + + + + + + + + ); +}; + +export default ManualTelemetrySection; diff --git a/app/src/features/surveys/view/SurveyPage.tsx b/app/src/features/surveys/view/SurveyPage.tsx index 6548acf184..4ef665d059 100644 --- a/app/src/features/surveys/view/SurveyPage.tsx +++ b/app/src/features/surveys/view/SurveyPage.tsx @@ -19,6 +19,7 @@ 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'; @@ -85,6 +86,12 @@ const SurveyPage: React.FC = () => { + + + + + + From 6bc2c1a32751a3f4bd6d111e1e848f8bde864976 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 14 Nov 2023 13:35:01 -0800 Subject: [PATCH 02/88] routed to new page --- app/src/features/surveys/SurveyRouter.tsx | 3 ++- app/src/features/surveys/telemetry/ManualTelemetryPage.tsx | 5 +++++ .../features/surveys/telemetry/ManualTelemetrySection.tsx | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 app/src/features/surveys/telemetry/ManualTelemetryPage.tsx diff --git a/app/src/features/surveys/SurveyRouter.tsx b/app/src/features/surveys/SurveyRouter.tsx index 6911262ef2..bd80011738 100644 --- a/app/src/features/surveys/SurveyRouter.tsx +++ b/app/src/features/surveys/SurveyRouter.tsx @@ -10,6 +10,7 @@ import EditSurveyPage from './edit/EditSurveyPage'; import SamplingSiteEditPage from './observations/sampling-sites/edit/SamplingSiteEditPage'; import SamplingSitePage from './observations/sampling-sites/SamplingSitePage'; import { SurveyObservationPage } from './observations/SurveyObservationPage'; +import ManualTelemetryPage from './telemetry/ManualTelemetryPage'; /** * Router for all `/admin/projects/:id/surveys/:survey_id/*` pages. @@ -37,7 +38,7 @@ const SurveyRouter: React.FC = () => { exact path="/admin/projects/:id/surveys/:survey_id/telemetry" title={getTitle('Manual Telemetry')}> - <>Butts + {/* Sample Site Routes */} diff --git a/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx b/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx new file mode 100644 index 0000000000..099772cc21 --- /dev/null +++ b/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx @@ -0,0 +1,5 @@ +const ManualTelemetryPage = () => { + return <>; +}; + +export default ManualTelemetryPage; diff --git a/app/src/features/surveys/telemetry/ManualTelemetrySection.tsx b/app/src/features/surveys/telemetry/ManualTelemetrySection.tsx index 21ceb365e4..f0711d2602 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetrySection.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetrySection.tsx @@ -23,7 +23,7 @@ const ManualTelemetrySection = () => { - + From 155deee547de690536bf43c4fd1dd1a6ebdac9f3 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 15 Nov 2023 10:30:31 -0800 Subject: [PATCH 03/88] basic list component created --- .../surveys/telemetry/ManualTelemetryList.tsx | 71 +++++++++++++++++++ .../surveys/telemetry/ManualTelemetryPage.tsx | 45 +++++++++++- 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 app/src/features/surveys/telemetry/ManualTelemetryList.tsx diff --git a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx new file mode 100644 index 0000000000..064f11115d --- /dev/null +++ b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx @@ -0,0 +1,71 @@ +import { mdiPlus } 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 List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import { SurveyContext } from 'contexts/surveyContext'; +import { useContext } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; + +export interface ManualTelemetryListProps {} + +const ManualTelemetryList = () => { + const surveyContext = useContext(SurveyContext); + + if (!surveyContext.surveyDataLoader.data) { + return ; + } + + return ( + + + + Deployments + + + + + + + Woot + + + + + ); +}; + +export default ManualTelemetryList; diff --git a/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx b/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx index 099772cc21..d9afa6cf15 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx @@ -1,5 +1,48 @@ +import Box from '@mui/material/Box'; +import CircularProgress from '@mui/material/CircularProgress'; +import { grey } from '@mui/material/colors'; +import Paper from '@mui/material/Paper'; +import { SurveyContext } from 'contexts/surveyContext'; +import { useContext } from 'react'; +import SurveyObservationHeader from '../observations/SurveyObservationHeader'; +import ManualTelemetryList from './ManualTelemetryList'; + const ManualTelemetryPage = () => { - return <>; + const surveyContext = useContext(SurveyContext); + + if (!surveyContext.surveyDataLoader.data) { + return ; + } + + return ( + + + + + + + + <>Table goes here + + + + ); }; export default ManualTelemetryPage; From 84e7a198274e42bef7c180b92c396e6bdaf253d4 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 15 Nov 2023 17:13:39 -0800 Subject: [PATCH 04/88] built basic list layout --- .../surveys/telemetry/ManualTelemetryList.tsx | 67 ++++++++++++++----- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx index 064f11115d..e10df51f70 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx @@ -1,17 +1,19 @@ -import { mdiPlus } from '@mdi/js'; +import { mdiChevronDown, mdiPlus } from '@mdi/js'; import Icon from '@mdi/react'; +import Accordion from '@mui/material/Accordion'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import AccordionSummary from '@mui/material/AccordionSummary'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import CircularProgress from '@mui/material/CircularProgress'; -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; +import { grey } from '@mui/material/colors'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import { SurveyContext } from 'contexts/surveyContext'; import { useContext } from 'react'; import { Link as RouterLink } from 'react-router-dom'; -export interface ManualTelemetryListProps {} +// export interface ManualTelemetryListProps {} const ManualTelemetryList = () => { const surveyContext = useContext(SurveyContext); @@ -47,22 +49,55 @@ const ManualTelemetryList = () => { - - - Woot - - + } + sx={{ + flex: '1 1 auto', + overflow: 'hidden', + py: 0.25, + pr: 1.5, + pl: 2, + gap: '24px', + '& .MuiAccordionSummary-content': { + flex: '1 1 auto', + overflow: 'hidden', + whiteSpace: 'nowrap' + } + }}> + + Deployment Name + + + + Look at all these details son + + + ); From d37a3e77b351cf69ff459260435d835848c30b45 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 16 Nov 2023 10:51:35 -0800 Subject: [PATCH 05/88] added deployment to survey context, wired up list with deployment api --- app/src/contexts/surveyContext.tsx | 14 +++++ .../surveys/telemetry/ManualTelemetryCard.tsx | 56 +++++++++++++++++ .../surveys/telemetry/ManualTelemetryList.tsx | 62 +++++-------------- 3 files changed, 85 insertions(+), 47 deletions(-) create mode 100644 app/src/features/surveys/telemetry/ManualTelemetryCard.tsx diff --git a/app/src/contexts/surveyContext.tsx b/app/src/contexts/surveyContext.tsx index 625747f504..40f500dcd4 100644 --- a/app/src/contexts/surveyContext.tsx +++ b/app/src/contexts/surveyContext.tsx @@ -1,3 +1,4 @@ +import { IAnimalDeployment } from 'features/surveys/view/survey-animals/device'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader, { DataLoader } from 'hooks/useDataLoader'; import { IGetObservationSubmissionResponse } from 'interfaces/useDwcaApi.interface'; @@ -61,6 +62,14 @@ export interface ISurveyContext { */ sampleSiteDataLoader: DataLoader<[project_id: number, survey_id: number], IGetSampleSiteResponse, unknown>; + /** + * The Data Loader used to load critter deployments for a given survey + * + * @type {DataLoader<[project_id: number, survey_id: number], IGetSampleSiteResponse, unknown>} + * @memberof ISurveyContext + */ + deploymentDataLoader: DataLoader<[project_id: number, survey_id: number], IAnimalDeployment[], unknown>; + /** * The project ID belonging to the current project * @@ -88,6 +97,7 @@ export const SurveyContext = createContext({ summaryDataLoader: {} as DataLoader<[project_id: number, survey_id: number], IGetSummaryResultsResponse, unknown>, artifactDataLoader: {} as DataLoader<[project_id: number, survey_id: number], IGetSurveyAttachmentsResponse, unknown>, sampleSiteDataLoader: {} as DataLoader<[project_id: number, survey_id: number], IGetSampleSiteResponse, unknown>, + deploymentDataLoader: {} as DataLoader<[project_id: number, survey_id: number], IAnimalDeployment[], unknown>, projectId: -1, surveyId: -1 }); @@ -99,6 +109,7 @@ export const SurveyContextProvider = (props: PropsWithChildren = useParams(); @@ -122,6 +133,7 @@ export const SurveyContextProvider = (props: PropsWithChildren { + return ( + + } + sx={{ + flex: '1 1 auto', + overflow: 'hidden', + py: 0.25, + pr: 1.5, + pl: 2, + gap: '24px', + '& .MuiAccordionSummary-content': { + flex: '1 1 auto', + overflow: 'hidden', + whiteSpace: 'nowrap' + } + }}> + + {props.name} + + + + {props.details} + + + ); +}; + +export default ManualTelemetryCard; diff --git a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx index e10df51f70..b1adced424 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx @@ -1,8 +1,5 @@ -import { mdiChevronDown, mdiPlus } from '@mdi/js'; +import { mdiPlus } from '@mdi/js'; import Icon from '@mdi/react'; -import Accordion from '@mui/material/Accordion'; -import AccordionDetails from '@mui/material/AccordionDetails'; -import AccordionSummary from '@mui/material/AccordionSummary'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import CircularProgress from '@mui/material/CircularProgress'; @@ -10,18 +7,21 @@ import { grey } from '@mui/material/colors'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import { SurveyContext } from 'contexts/surveyContext'; -import { useContext } from 'react'; +import { useContext, useMemo } from 'react'; import { Link as RouterLink } from 'react-router-dom'; +import ManualTelemetryCard from './ManualTelemetryCard'; -// export interface ManualTelemetryListProps {} +// export interface ManualTelemetryListProps { const ManualTelemetryList = () => { const surveyContext = useContext(SurveyContext); + surveyContext.deploymentDataLoader.load(surveyContext.projectId, surveyContext.surveyId); + const deployments = useMemo(() => surveyContext.deploymentDataLoader.data, [surveyContext.deploymentDataLoader.data]); - if (!surveyContext.surveyDataLoader.data) { + if (surveyContext.deploymentDataLoader.isLoading) { return ; } - + console.log(deployments); return ( { p: 1, background: grey[100] }}> - - } - sx={{ - flex: '1 1 auto', - overflow: 'hidden', - py: 0.25, - pr: 1.5, - pl: 2, - gap: '24px', - '& .MuiAccordionSummary-content': { - flex: '1 1 auto', - overflow: 'hidden', - whiteSpace: 'nowrap' - } - }}> - - Deployment Name - - - - Look at all these details son - - + {deployments?.map((item) => ( + + ))} From bf5e59f4a867bad9d4d444b570cd71849afdd4cd Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 16 Nov 2023 12:03:33 -0800 Subject: [PATCH 06/88] wired up list --- .../features/surveys/telemetry/ManualTelemetryList.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx index b1adced424..41ef18fe91 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx @@ -7,7 +7,7 @@ import { grey } from '@mui/material/colors'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import { SurveyContext } from 'contexts/surveyContext'; -import { useContext, useMemo } from 'react'; +import { useContext, useEffect, useMemo } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import ManualTelemetryCard from './ManualTelemetryCard'; @@ -15,13 +15,16 @@ import ManualTelemetryCard from './ManualTelemetryCard'; const ManualTelemetryList = () => { const surveyContext = useContext(SurveyContext); - surveyContext.deploymentDataLoader.load(surveyContext.projectId, surveyContext.surveyId); + + useEffect(() => { + surveyContext.deploymentDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); + }, []); const deployments = useMemo(() => surveyContext.deploymentDataLoader.data, [surveyContext.deploymentDataLoader.data]); if (surveyContext.deploymentDataLoader.isLoading) { return ; } - console.log(deployments); + return ( Date: Thu, 16 Nov 2023 13:46:20 -0800 Subject: [PATCH 07/88] added skeleton loader component --- app/src/components/loading/SkeletonList.tsx | 49 +++++++++++++++++++ .../sampling-sites/SamplingSiteList.tsx | 35 ++----------- .../surveys/telemetry/ManualTelemetryList.tsx | 13 ++--- 3 files changed, 61 insertions(+), 36 deletions(-) create mode 100644 app/src/components/loading/SkeletonList.tsx diff --git a/app/src/components/loading/SkeletonList.tsx b/app/src/components/loading/SkeletonList.tsx new file mode 100644 index 0000000000..5b641e5eed --- /dev/null +++ b/app/src/components/loading/SkeletonList.tsx @@ -0,0 +1,49 @@ +import Box from '@mui/material/Box'; +import { grey } from '@mui/material/colors'; +import Fade from '@mui/material/Fade'; +import Paper from '@mui/material/Paper'; +import Skeleton from '@mui/material/Skeleton'; + +export interface ISkeletonListProps { + isLoading: boolean; + timeout?: number; + numberOfLines?: number; +} + +const SkeletonList = (props: ISkeletonListProps) => { + return ( + + + + {/* create an array of X items to build multiple skeleton components*/} + {Array(props.numberOfLines ?? 10) + .fill(null) + .map(() => ( + + + + ))} + + + + ); +}; + +export default SkeletonList; diff --git a/app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx b/app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx index 7c09f0828e..a8663459f6 100644 --- a/app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx +++ b/app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx @@ -13,7 +13,6 @@ import AccordionSummary from '@mui/material/AccordionSummary'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import { grey } from '@mui/material/colors'; -import Fade from '@mui/material/Fade'; import IconButton from '@mui/material/IconButton'; import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; @@ -21,10 +20,10 @@ import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import Menu, { MenuProps } from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; -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 SkeletonList from 'components/loading/SkeletonList'; import { CodesContext } from 'contexts/codesContext'; import { DialogContext } from 'contexts/dialogContext'; import { SurveyContext } from 'contexts/surveyContext'; @@ -202,34 +201,10 @@ const SamplingSiteList = () => { - {/* Display spinner if data loaders are still waiting for a response */} - - - - - - - - - - - - - - - - - + {/* Display list of skeleton components while waiting for a response */} + { }, []); const deployments = useMemo(() => surveyContext.deploymentDataLoader.data, [surveyContext.deploymentDataLoader.data]); - if (surveyContext.deploymentDataLoader.isLoading) { - return ; - } - return ( { fontSize: '1.125rem', fontWeight: 700 }}> - Deployments + Deployments ‌ + + ({deployments?.length ?? 0}) + + {/* Display list of skeleton components while waiting for a response */} + Date: Thu, 16 Nov 2023 13:47:13 -0800 Subject: [PATCH 08/88] clean up --- .../sampling-sites/SamplingSiteList.tsx | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx b/app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx index a8663459f6..6f0fccf0a2 100644 --- a/app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx +++ b/app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx @@ -20,7 +20,6 @@ import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import Menu, { MenuProps } from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; -import Skeleton from '@mui/material/Skeleton'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import SkeletonList from 'components/loading/SkeletonList'; @@ -32,21 +31,6 @@ import { useContext, useEffect, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { getCodesName } from 'utils/Utils'; -const SampleSiteSkeleton = () => ( - - - -); - const SamplingSiteList = () => { const surveyContext = useContext(SurveyContext); const codesContext = useContext(CodesContext); From 08cd378728f402b1116d3a389a3f63bf94e1bf20 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 16 Nov 2023 15:58:36 -0800 Subject: [PATCH 09/88] updated api for list --- app/src/contexts/surveyContext.tsx | 22 ++++--- .../surveys/telemetry/ManualTelemetryList.tsx | 15 +++-- .../TelemetryDeviceFormContent.tsx | 12 ++-- app/src/hooks/api/useSurveyApi.ts | 2 +- app/src/hooks/useBioHubApi.ts | 6 +- app/src/hooks/useTelemetryApi.ts | 63 ++++++++++++++++--- 6 files changed, 90 insertions(+), 30 deletions(-) diff --git a/app/src/contexts/surveyContext.tsx b/app/src/contexts/surveyContext.tsx index 40f500dcd4..67c9beedd6 100644 --- a/app/src/contexts/surveyContext.tsx +++ b/app/src/contexts/surveyContext.tsx @@ -1,6 +1,6 @@ -import { IAnimalDeployment } from 'features/surveys/view/survey-animals/device'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader, { DataLoader } from 'hooks/useDataLoader'; +import { ICritterDeploymentResponse } from 'hooks/useTelemetryApi'; import { IGetObservationSubmissionResponse } from 'interfaces/useDwcaApi.interface'; import { IGetSummaryResultsResponse } from 'interfaces/useSummaryResultsApi.interface'; import { @@ -68,7 +68,11 @@ export interface ISurveyContext { * @type {DataLoader<[project_id: number, survey_id: number], IGetSampleSiteResponse, unknown>} * @memberof ISurveyContext */ - deploymentDataLoader: DataLoader<[project_id: number, survey_id: number], IAnimalDeployment[], unknown>; + critterDeploymentDataLoader: DataLoader< + [project_id: number, survey_id: number], + ICritterDeploymentResponse[], + unknown + >; /** * The project ID belonging to the current project @@ -97,7 +101,11 @@ export const SurveyContext = createContext({ summaryDataLoader: {} as DataLoader<[project_id: number, survey_id: number], IGetSummaryResultsResponse, unknown>, artifactDataLoader: {} as DataLoader<[project_id: number, survey_id: number], IGetSurveyAttachmentsResponse, unknown>, sampleSiteDataLoader: {} as DataLoader<[project_id: number, survey_id: number], IGetSampleSiteResponse, unknown>, - deploymentDataLoader: {} as DataLoader<[project_id: number, survey_id: number], IAnimalDeployment[], unknown>, + critterDeploymentDataLoader: {} as DataLoader< + [project_id: number, survey_id: number], + ICritterDeploymentResponse[], + unknown + >, projectId: -1, surveyId: -1 }); @@ -109,7 +117,7 @@ export const SurveyContextProvider = (props: PropsWithChildren = useParams(); @@ -133,7 +141,7 @@ export const SurveyContextProvider = (props: PropsWithChildren { const surveyContext = useContext(SurveyContext); useEffect(() => { - surveyContext.deploymentDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); + surveyContext.critterDeploymentDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); }, []); - const deployments = useMemo(() => surveyContext.deploymentDataLoader.data, [surveyContext.deploymentDataLoader.data]); + const deployments = useMemo( + () => surveyContext.critterDeploymentDataLoader.data, + [surveyContext.critterDeploymentDataLoader.data] + ); return ( @@ -52,7 +55,7 @@ const ManualTelemetryList = () => { {/* Display list of skeleton components while waiting for a response */} - + { background: grey[100] }}> {deployments?.map((item) => ( - + ))} diff --git a/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx b/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx index e4fcde5a34..24b59ce60d 100644 --- a/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx +++ b/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx @@ -4,8 +4,8 @@ import TelemetrySelectField from 'components/fields/TelemetrySelectField'; import FormikDevDebugger from 'components/formik/FormikDevDebugger'; import { AttachmentType } from 'constants/attachments'; import { Field, useFormikContext } from 'formik'; +import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; -import { useTelemetryApi } from 'hooks/useTelemetryApi'; import { useEffect } from 'react'; import { ANIMAL_FORM_MODE, IAnimal } from '../animal'; import { DeploymentFormSection } from './DeploymentFormSection'; @@ -18,12 +18,14 @@ interface TelemetryDeviceFormContentProps { const TelemetryDeviceFormContent = (props: TelemetryDeviceFormContentProps) => { const { index, mode } = props; - const api = useTelemetryApi(); + const biohubApi = useBiohubApi(); const { values, validateField } = useFormikContext(); const device = values.device?.[index]; - const { data: deviceDetails, refresh } = useDataLoader(() => api.devices.getDeviceDetails(device.device_id)); + const { data: deviceDetails, refresh } = useDataLoader(() => + biohubApi.telemetry.devices.getDeviceDetails(device.device_id) + ); const validateDeviceMake = async (value: number | '') => { const deviceMake = deviceDetails?.device?.device_make; @@ -88,7 +90,7 @@ const TelemetryDeviceFormContent = (props: TelemetryDeviceFormContentProps) => { name={`device.${index}.frequency_unit`} id="frequency_unit" fetchData={async () => { - const codeVals = await api.devices.getCodeValues('frequency_unit'); + const codeVals = await biohubApi.telemetry.devices.getCodeValues('frequency_unit'); return codeVals.map((a) => a.description); }} /> @@ -101,7 +103,7 @@ const TelemetryDeviceFormContent = (props: TelemetryDeviceFormContentProps) => { label="Device Manufacturer" name={`device.${index}.device_make`} id="manufacturer" - fetchData={api.devices.getCollarVendors} + fetchData={biohubApi.telemetry.devices.getCollarVendors} controlProps={{ disabled: mode === ANIMAL_FORM_MODE.EDIT, required: true }} validate={validateDeviceMake} /> diff --git a/app/src/hooks/api/useSurveyApi.ts b/app/src/hooks/api/useSurveyApi.ts index c3a399f8be..245dd59f0c 100644 --- a/app/src/hooks/api/useSurveyApi.ts +++ b/app/src/hooks/api/useSurveyApi.ts @@ -579,7 +579,7 @@ const useSurveyApi = (axios: AxiosInstance) => { }; /** - * Update a deployment with a new timespan. + * Update a deployment with a new time span. * * @param {number} projectId * @param {number} surveyId diff --git a/app/src/hooks/useBioHubApi.ts b/app/src/hooks/useBioHubApi.ts index cc4f2466dd..682de3b074 100644 --- a/app/src/hooks/useBioHubApi.ts +++ b/app/src/hooks/useBioHubApi.ts @@ -18,6 +18,7 @@ import useSpatialApi from './api/useSpatialApi'; import useSurveyApi from './api/useSurveyApi'; import useTaxonomyApi from './api/useTaxonomyApi'; import useUserApi from './api/useUserApi'; +import { useTelemetryApi } from './useTelemetryApi'; /** * Returns a set of supported api methods. @@ -60,6 +61,8 @@ export const useBiohubApi = () => { const samplingSite = useSamplingSiteApi(apiAxios); + const telemetry = useTelemetryApi(apiAxios); + return useMemo( () => ({ project, @@ -77,7 +80,8 @@ export const useBiohubApi = () => { publish, spatial, funding, - samplingSite + samplingSite, + telemetry }), // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/app/src/hooks/useTelemetryApi.ts b/app/src/hooks/useTelemetryApi.ts index 19e9b78a24..7e9d2a0b8d 100644 --- a/app/src/hooks/useTelemetryApi.ts +++ b/app/src/hooks/useTelemetryApi.ts @@ -1,13 +1,60 @@ -import { ConfigContext } from 'contexts/configContext'; -import { useContext } from 'react'; -import useAxios from './api/useAxios'; +import { AxiosInstance } from 'axios'; +import { v4 } from 'uuid'; import { useDeviceApi } from './telemetry/useDeviceApi'; -export const useTelemetryApi = () => { - const config = useContext(ConfigContext); - const apiAxios = useAxios(config?.API_HOST); - const devices = useDeviceApi(apiAxios); - return { devices }; +export interface ICritterDeploymentResponse { + critter_id: string; + device_id: number; + deployment_id: string; + survey_critter_id: string; + alias: string; + attachment_start: string; + attachment_end?: string; + taxon: string; +} + +export const useTelemetryApi = (axios: AxiosInstance) => { + const devices = useDeviceApi(axios); + + const getCritterAndDeployments = (projectId: number, surveyId: number): Promise => { + // This endpoint will fetch a list of critters and their deployments + // const { data } = await axios.get(`/api/project/${projectId}/survey/${surveyId}/critters/deployments`); + // return data; + return Promise.resolve([ + { + critter_id: v4().toString(), + device_id: 123, + deployment_id: v4().toString(), + survey_critter_id: '', + alias: 'Jingles the moose', + attachment_start: '2023-01-01T08:00:00.000Z', + attachment_end: undefined, + taxon: 'Moose' + }, + { + critter_id: v4().toString(), + device_id: 12333, + deployment_id: v4().toString(), + survey_critter_id: '', + alias: 'Big Tom', + attachment_start: '2023-02-01T08:00:00.000Z', + attachment_end: undefined, + taxon: 'Moose' + }, + { + critter_id: v4().toString(), + device_id: 5544, + deployment_id: v4().toString(), + survey_critter_id: '', + alias: 'Little Timmy', + attachment_start: '2023-02-015T08:00:00.000Z', + attachment_end: undefined, + taxon: 'Caribou' + } + ]); + }; + + return { devices, getCritterAndDeployments }; }; type TelemetryApiReturnType = ReturnType; From 4576dce040e1ef8a0ee7525319964e713b8b1d7a Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 16 Nov 2023 16:45:46 -0800 Subject: [PATCH 10/88] added telemetry form to list --- app/src/components/loading/SkeletonList.tsx | 2 + .../surveys/telemetry/ManualTelemetryCard.tsx | 1 - .../surveys/telemetry/ManualTelemetryList.tsx | 136 ++++++++++++------ .../view/survey-animals/AddEditAnimal.tsx | 6 +- .../view/survey-animals/SurveyAnimalsPage.tsx | 4 +- .../TelemetryDeviceFormContent.tsx | 1 + app/src/hooks/useBioHubApi.ts | 1 + 7 files changed, 102 insertions(+), 49 deletions(-) diff --git a/app/src/components/loading/SkeletonList.tsx b/app/src/components/loading/SkeletonList.tsx index 5b641e5eed..99725ccf2f 100644 --- a/app/src/components/loading/SkeletonList.tsx +++ b/app/src/components/loading/SkeletonList.tsx @@ -3,6 +3,7 @@ import { grey } from '@mui/material/colors'; import Fade from '@mui/material/Fade'; import Paper from '@mui/material/Paper'; import Skeleton from '@mui/material/Skeleton'; +import { v4 } from 'uuid'; export interface ISkeletonListProps { isLoading: boolean; @@ -28,6 +29,7 @@ const SkeletonList = (props: ISkeletonListProps) => { .fill(null) .map(() => ( { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); const surveyContext = useContext(SurveyContext); + const [showDialog, setShowDialog] = useState(false); + useEffect(() => { surveyContext.critterDeploymentDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); }, []); @@ -25,52 +36,93 @@ const ManualTelemetryList = () => { ); return ( - - + { + // if (formMode === ANIMAL_FORM_MODE.ADD) { + // formikArrayHelpers.remove(selectedIndex); + // } + // setFormMode(ANIMAL_FORM_MODE.EDIT); }}> - Butts + + + + + { + // section is always going to be telemetry + // await handleSaveTelemetry(values); + // submitForm(); + // setFormMode(ANIMAL_FORM_MODE.EDIT); + setShowDialog(false); + }} + loading={false}> + Save + + + + + + - Deployments ‌ - - ({deployments?.length ?? 0}) + + Deployments ‌ + + ({deployments?.length ?? 0}) + - - - - - {/* Display list of skeleton components while waiting for a response */} - - - {deployments?.map((item) => ( - - ))} + + + + {/* Display list of skeleton components while waiting for a response */} + + + {deployments?.map((item) => ( + + ))} + - + ); }; diff --git a/app/src/features/surveys/view/survey-animals/AddEditAnimal.tsx b/app/src/features/surveys/view/survey-animals/AddEditAnimal.tsx index 4c9cac64d0..e50c67883d 100644 --- a/app/src/features/surveys/view/survey-animals/AddEditAnimal.tsx +++ b/app/src/features/surveys/view/survey-animals/AddEditAnimal.tsx @@ -21,10 +21,10 @@ import { SurveyAnimalsI18N } from 'constants/i18n'; import { DialogContext } from 'contexts/dialogContext'; import { SurveyContext } from 'contexts/surveyContext'; import { FieldArrayRenderProps, useFormikContext } from 'formik'; +import { useBiohubApi } from 'hooks/useBioHubApi'; import { useCritterbaseApi } from 'hooks/useCritterbaseApi'; import useDataLoader from 'hooks/useDataLoader'; import { useQuery } from 'hooks/useQuery'; -import { useTelemetryApi } from 'hooks/useTelemetryApi'; import { IDetailedCritterWithInternalId } from 'interfaces/useSurveyApi.interface'; import { useContext, useEffect, useMemo, useState } from 'react'; import { setMessageSnackbar } from 'utils/Utils'; @@ -54,7 +54,7 @@ export const AddEditAnimal = (props: IAddEditAnimalProps) => { const { section, critterData, telemetrySaveAction, deploymentRemoveAction, formikArrayHelpers } = props; const theme = useTheme(); - const telemetryApi = useTelemetryApi(); + const biohubApi = useBiohubApi(); const cbApi = useCritterbaseApi(); const surveyContext = useContext(SurveyContext); const dialogContext = useContext(DialogContext); @@ -64,7 +64,7 @@ export const AddEditAnimal = (props: IAddEditAnimalProps) => { useFormikContext(); const { data: allFamilies, refresh: refreshFamilies } = useDataLoader(cbApi.family.getAllFamilies); - const { refresh: refreshDeviceDetails } = useDataLoader(telemetryApi.devices.getDeviceDetails); + const { refresh: refreshDeviceDetails } = useDataLoader(biohubApi.telemetry.devices.getDeviceDetails); const { data: measurements, refresh: refreshMeasurements } = useDataLoader(cbApi.lookup.getTaxonMeasurements); const [showDialog, setShowDialog] = useState(false); diff --git a/app/src/features/surveys/view/survey-animals/SurveyAnimalsPage.tsx b/app/src/features/surveys/view/survey-animals/SurveyAnimalsPage.tsx index 2c6bde419a..9b68f15834 100644 --- a/app/src/features/surveys/view/survey-animals/SurveyAnimalsPage.tsx +++ b/app/src/features/surveys/view/survey-animals/SurveyAnimalsPage.tsx @@ -8,7 +8,6 @@ import { FieldArray, FieldArrayRenderProps, Formik } from 'formik'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; import { useQuery } from 'hooks/useQuery'; -import { useTelemetryApi } from 'hooks/useTelemetryApi'; import { IDetailedCritterWithInternalId } from 'interfaces/useSurveyApi.interface'; import { isEqual as _deepEquals, omitBy } from 'lodash'; import { useContext, useMemo, useState } from 'react'; @@ -31,7 +30,6 @@ export const SurveyAnimalsPage = () => { const { cid: survey_critter_id } = useQuery(); const [openAddDialog, setOpenAddDialog] = useState(false); const bhApi = useBiohubApi(); - const telemetryApi = useTelemetryApi(); const dialogContext = useContext(DialogContext); const { surveyId, projectId, artifactDataLoader } = useContext(SurveyContext); @@ -207,7 +205,7 @@ export const SurveyAnimalsPage = () => { const formDevice = new Device({ collar_id: existingDevice?.collar_id, ...formValues }); if (existingDevice && !_deepEquals(new Device(existingDevice), formDevice)) { try { - await telemetryApi.devices.upsertCollar(formDevice); + await bhApi.telemetry.devices.upsertCollar(formDevice); } catch (error) { throw new Error(`Failed to update collar ${formDevice.collar_id}`); } diff --git a/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx b/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx index 24b59ce60d..7638f5fb8d 100644 --- a/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx +++ b/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx @@ -22,6 +22,7 @@ const TelemetryDeviceFormContent = (props: TelemetryDeviceFormContentProps) => { const { values, validateField } = useFormikContext(); const device = values.device?.[index]; + console.log(values.device); const { data: deviceDetails, refresh } = useDataLoader(() => biohubApi.telemetry.devices.getDeviceDetails(device.device_id) diff --git a/app/src/hooks/useBioHubApi.ts b/app/src/hooks/useBioHubApi.ts index 682de3b074..d67692c787 100644 --- a/app/src/hooks/useBioHubApi.ts +++ b/app/src/hooks/useBioHubApi.ts @@ -61,6 +61,7 @@ export const useBiohubApi = () => { const samplingSite = useSamplingSiteApi(apiAxios); + // This needs to be undone as only 1 endpoint for telemetry currently exists, while everything else should be in it's own useApi setup const telemetry = useTelemetryApi(apiAxios); return useMemo( From d93a65922e1a30eda101e1a8bd042d867b92b624 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 17 Nov 2023 11:09:40 -0800 Subject: [PATCH 11/88] dialog properly showing --- app/src/contexts/surveyContext.tsx | 5 +- .../surveys/telemetry/ManualTelemetryList.tsx | 108 ++++++++++++------ .../view/survey-animals/AddEditAnimal.tsx | 6 +- .../view/survey-animals/SurveyAnimalsPage.tsx | 5 +- .../TelemetryDeviceFormContent.tsx | 12 +- app/src/hooks/useBioHubApi.ts | 7 +- app/src/hooks/useTelemetryApi.ts | 9 +- 7 files changed, 94 insertions(+), 58 deletions(-) diff --git a/app/src/contexts/surveyContext.tsx b/app/src/contexts/surveyContext.tsx index 67c9beedd6..b9bf69b09f 100644 --- a/app/src/contexts/surveyContext.tsx +++ b/app/src/contexts/surveyContext.tsx @@ -1,6 +1,6 @@ import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader, { DataLoader } from 'hooks/useDataLoader'; -import { ICritterDeploymentResponse } from 'hooks/useTelemetryApi'; +import { ICritterDeploymentResponse, useTelemetryApi } from 'hooks/useTelemetryApi'; import { IGetObservationSubmissionResponse } from 'interfaces/useDwcaApi.interface'; import { IGetSummaryResultsResponse } from 'interfaces/useSummaryResultsApi.interface'; import { @@ -112,12 +112,13 @@ export const SurveyContext = createContext({ export const SurveyContextProvider = (props: PropsWithChildren>) => { const biohubApi = useBiohubApi(); + const telemetryApi = useTelemetryApi(); const surveyDataLoader = useDataLoader(biohubApi.survey.getSurveyForView); const observationDataLoader = useDataLoader(biohubApi.dwca.getObservationSubmission); const summaryDataLoader = useDataLoader(biohubApi.survey.getSurveySummarySubmission); const artifactDataLoader = useDataLoader(biohubApi.survey.getSurveyAttachments); const sampleSiteDataLoader = useDataLoader(biohubApi.samplingSite.getSampleSites); - const critterDeploymentDataLoader = useDataLoader(biohubApi.telemetry.getCritterAndDeployments); + const critterDeploymentDataLoader = useDataLoader(telemetryApi.getCritterAndDeployments); const urlParams: Record = useParams(); diff --git a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx index f09d5eba75..e54a75a439 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx @@ -13,13 +13,20 @@ import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import ListFader from 'components/loading/SkeletonList'; import { SurveyContext } from 'contexts/surveyContext'; +import { Formik } from 'formik'; import { useContext, useEffect, useMemo, useState } from 'react'; +import yup from 'utils/YupSchema'; import { ANIMAL_FORM_MODE } from '../view/survey-animals/animal'; +import { AnimalTelemetryDeviceSchema } from '../view/survey-animals/telemetry-device/device'; import TelemetryDeviceFormContent from '../view/survey-animals/telemetry-device/TelemetryDeviceFormContent'; import ManualTelemetryCard from './ManualTelemetryCard'; // export interface ManualTelemetryListProps { +const AnimalDeploymentSchema = yup.object().shape({ + device: yup.array().of(AnimalTelemetryDeviceSchema).required() +}); + const ManualTelemetryList = () => { const theme = useTheme(); const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); @@ -37,44 +44,71 @@ const ManualTelemetryList = () => { return ( <> - { - // if (formMode === ANIMAL_FORM_MODE.ADD) { - // formikArrayHelpers.remove(selectedIndex); - // } - // setFormMode(ANIMAL_FORM_MODE.EDIT); + { + // handleCritterSave }}> - Butts - - - - - { - // section is always going to be telemetry - // await handleSaveTelemetry(values); - // submitForm(); - // setFormMode(ANIMAL_FORM_MODE.EDIT); - setShowDialog(false); - }} - loading={false}> - Save - - - - + { + // if (formMode === ANIMAL_FORM_MODE.ADD) { + // formikArrayHelpers.remove(selectedIndex); + // } + // setFormMode(ANIMAL_FORM_MODE.EDIT); + }}> + Butts + + + + + { + // section is always going to be telemetry + // await handleSaveTelemetry(values); + // submitForm(); + // setFormMode(ANIMAL_FORM_MODE.EDIT); + setShowDialog(false); + }} + loading={false}> + Save + + + + + { const { section, critterData, telemetrySaveAction, deploymentRemoveAction, formikArrayHelpers } = props; const theme = useTheme(); - const biohubApi = useBiohubApi(); + const telemetryApi = useTelemetryApi(); const cbApi = useCritterbaseApi(); const surveyContext = useContext(SurveyContext); const dialogContext = useContext(DialogContext); @@ -64,7 +64,7 @@ export const AddEditAnimal = (props: IAddEditAnimalProps) => { useFormikContext(); const { data: allFamilies, refresh: refreshFamilies } = useDataLoader(cbApi.family.getAllFamilies); - const { refresh: refreshDeviceDetails } = useDataLoader(biohubApi.telemetry.devices.getDeviceDetails); + const { refresh: refreshDeviceDetails } = useDataLoader(telemetryApi.devices.getDeviceDetails); const { data: measurements, refresh: refreshMeasurements } = useDataLoader(cbApi.lookup.getTaxonMeasurements); const [showDialog, setShowDialog] = useState(false); diff --git a/app/src/features/surveys/view/survey-animals/SurveyAnimalsPage.tsx b/app/src/features/surveys/view/survey-animals/SurveyAnimalsPage.tsx index 9b68f15834..7566b0f416 100644 --- a/app/src/features/surveys/view/survey-animals/SurveyAnimalsPage.tsx +++ b/app/src/features/surveys/view/survey-animals/SurveyAnimalsPage.tsx @@ -8,6 +8,7 @@ import { FieldArray, FieldArrayRenderProps, Formik } from 'formik'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; import { useQuery } from 'hooks/useQuery'; +import { useTelemetryApi } from 'hooks/useTelemetryApi'; import { IDetailedCritterWithInternalId } from 'interfaces/useSurveyApi.interface'; import { isEqual as _deepEquals, omitBy } from 'lodash'; import { useContext, useMemo, useState } from 'react'; @@ -30,6 +31,7 @@ export const SurveyAnimalsPage = () => { const { cid: survey_critter_id } = useQuery(); const [openAddDialog, setOpenAddDialog] = useState(false); const bhApi = useBiohubApi(); + const telemetryApi = useTelemetryApi(); const dialogContext = useContext(DialogContext); const { surveyId, projectId, artifactDataLoader } = useContext(SurveyContext); @@ -97,6 +99,7 @@ export const SurveyAnimalsPage = () => { deployments = []; } animal.device = deployments; + console.log(animal); return animal; }, [critterData, deploymentData, survey_critter_id, defaultFormValues]); @@ -205,7 +208,7 @@ export const SurveyAnimalsPage = () => { const formDevice = new Device({ collar_id: existingDevice?.collar_id, ...formValues }); if (existingDevice && !_deepEquals(new Device(existingDevice), formDevice)) { try { - await bhApi.telemetry.devices.upsertCollar(formDevice); + await telemetryApi.devices.upsertCollar(formDevice); } catch (error) { throw new Error(`Failed to update collar ${formDevice.collar_id}`); } diff --git a/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx b/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx index 7638f5fb8d..20d6fc765d 100644 --- a/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx +++ b/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx @@ -4,8 +4,8 @@ import TelemetrySelectField from 'components/fields/TelemetrySelectField'; import FormikDevDebugger from 'components/formik/FormikDevDebugger'; import { AttachmentType } from 'constants/attachments'; import { Field, useFormikContext } from 'formik'; -import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; +import { useTelemetryApi } from 'hooks/useTelemetryApi'; import { useEffect } from 'react'; import { ANIMAL_FORM_MODE, IAnimal } from '../animal'; import { DeploymentFormSection } from './DeploymentFormSection'; @@ -18,15 +18,13 @@ interface TelemetryDeviceFormContentProps { const TelemetryDeviceFormContent = (props: TelemetryDeviceFormContentProps) => { const { index, mode } = props; - const biohubApi = useBiohubApi(); + const telemetryApi = useTelemetryApi(); const { values, validateField } = useFormikContext(); const device = values.device?.[index]; console.log(values.device); - const { data: deviceDetails, refresh } = useDataLoader(() => - biohubApi.telemetry.devices.getDeviceDetails(device.device_id) - ); + const { data: deviceDetails, refresh } = useDataLoader(() => telemetryApi.devices.getDeviceDetails(device.device_id)); const validateDeviceMake = async (value: number | '') => { const deviceMake = deviceDetails?.device?.device_make; @@ -91,7 +89,7 @@ const TelemetryDeviceFormContent = (props: TelemetryDeviceFormContentProps) => { name={`device.${index}.frequency_unit`} id="frequency_unit" fetchData={async () => { - const codeVals = await biohubApi.telemetry.devices.getCodeValues('frequency_unit'); + const codeVals = await telemetryApi.devices.getCodeValues('frequency_unit'); return codeVals.map((a) => a.description); }} /> @@ -104,7 +102,7 @@ const TelemetryDeviceFormContent = (props: TelemetryDeviceFormContentProps) => { label="Device Manufacturer" name={`device.${index}.device_make`} id="manufacturer" - fetchData={biohubApi.telemetry.devices.getCollarVendors} + fetchData={telemetryApi.devices.getCollarVendors} controlProps={{ disabled: mode === ANIMAL_FORM_MODE.EDIT, required: true }} validate={validateDeviceMake} /> diff --git a/app/src/hooks/useBioHubApi.ts b/app/src/hooks/useBioHubApi.ts index d67692c787..cc4f2466dd 100644 --- a/app/src/hooks/useBioHubApi.ts +++ b/app/src/hooks/useBioHubApi.ts @@ -18,7 +18,6 @@ import useSpatialApi from './api/useSpatialApi'; import useSurveyApi from './api/useSurveyApi'; import useTaxonomyApi from './api/useTaxonomyApi'; import useUserApi from './api/useUserApi'; -import { useTelemetryApi } from './useTelemetryApi'; /** * Returns a set of supported api methods. @@ -61,9 +60,6 @@ export const useBiohubApi = () => { const samplingSite = useSamplingSiteApi(apiAxios); - // This needs to be undone as only 1 endpoint for telemetry currently exists, while everything else should be in it's own useApi setup - const telemetry = useTelemetryApi(apiAxios); - return useMemo( () => ({ project, @@ -81,8 +77,7 @@ export const useBiohubApi = () => { publish, spatial, funding, - samplingSite, - telemetry + samplingSite }), // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/app/src/hooks/useTelemetryApi.ts b/app/src/hooks/useTelemetryApi.ts index 7e9d2a0b8d..212eac6ce9 100644 --- a/app/src/hooks/useTelemetryApi.ts +++ b/app/src/hooks/useTelemetryApi.ts @@ -1,5 +1,8 @@ -import { AxiosInstance } from 'axios'; +import axios from 'axios'; +import { ConfigContext } from 'contexts/configContext'; +import { useContext } from 'react'; import { v4 } from 'uuid'; +import useAxios from './api/useAxios'; import { useDeviceApi } from './telemetry/useDeviceApi'; export interface ICritterDeploymentResponse { @@ -13,7 +16,9 @@ export interface ICritterDeploymentResponse { taxon: string; } -export const useTelemetryApi = (axios: AxiosInstance) => { +export const useTelemetryApi = () => { + const config = useContext(ConfigContext); + const apiAxios = useAxios(config?.API_HOST); const devices = useDeviceApi(axios); const getCritterAndDeployments = (projectId: number, surveyId: number): Promise => { From 99fc1dcdb4d40a8998a46016fc0236e9efe32752 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 17 Nov 2023 12:32:25 -0800 Subject: [PATCH 12/88] added selector, form is submitting --- .../surveys/telemetry/ManualTelemetryList.tsx | 93 +++++++++++-------- .../TelemetryDeviceFormContent.tsx | 3 +- 2 files changed, 55 insertions(+), 41 deletions(-) diff --git a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx index e54a75a439..f00275f8d6 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx @@ -1,7 +1,7 @@ import { mdiPlus } from '@mdi/js'; import Icon from '@mdi/react'; -import LoadingButton from '@mui/lab/LoadingButton/LoadingButton'; -import { useMediaQuery, useTheme } from '@mui/material'; +import { LoadingButton } from '@mui/lab'; +import { MenuItem, Select, useMediaQuery, useTheme } from '@mui/material'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import { grey } from '@mui/material/colors'; @@ -9,6 +9,8 @@ import Dialog from '@mui/material/Dialog'; import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import ListFader from 'components/loading/SkeletonList'; @@ -33,6 +35,7 @@ const ManualTelemetryList = () => { const surveyContext = useContext(SurveyContext); const [showDialog, setShowDialog] = useState(false); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { surveyContext.critterDeploymentDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); @@ -68,46 +71,56 @@ const ManualTelemetryList = () => { validateOnBlur={false} validateOnChange={true} onSubmit={async (values, actions) => { + console.log('THINGS ARE BEING SUBMITTED'); + setIsLoading(true); // handleCritterSave }}> - { - // if (formMode === ANIMAL_FORM_MODE.ADD) { - // formikArrayHelpers.remove(selectedIndex); - // } - // setFormMode(ANIMAL_FORM_MODE.EDIT); - }}> - Butts - - - - - { - // section is always going to be telemetry - // await handleSaveTelemetry(values); - // submitForm(); - // setFormMode(ANIMAL_FORM_MODE.EDIT); - setShowDialog(false); - }} - loading={false}> - Save - - - - + {(formikProps) => ( + { + // if (formMode === ANIMAL_FORM_MODE.ADD) { + // formikArrayHelpers.remove(selectedIndex); + // } + // setFormMode(ANIMAL_FORM_MODE.EDIT); + }}> + Butts + + <> + + Critter + + + + + + + { + formikProps.submitForm(); + }}> + Save + + + + + )} { const { index, mode } = props; const telemetryApi = useTelemetryApi(); - const { values, validateField } = useFormikContext(); + const { values, validateField, errors } = useFormikContext(); + console.log(errors); const device = values.device?.[index]; console.log(values.device); From 549d47ca0fa28e6a0f7ec66a8676ba78c182c9c6 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 17 Nov 2023 14:51:38 -0800 Subject: [PATCH 13/88] updated API, added menu button, added critter selector --- app/src/contexts/surveyContext.tsx | 18 ++++++ .../surveys/telemetry/ManualTelemetryCard.tsx | 56 +++++++++++-------- .../surveys/telemetry/ManualTelemetryList.tsx | 27 +++++++-- app/src/hooks/useTelemetryApi.ts | 3 +- 4 files changed, 72 insertions(+), 32 deletions(-) diff --git a/app/src/contexts/surveyContext.tsx b/app/src/contexts/surveyContext.tsx index b9bf69b09f..9aaca266ac 100644 --- a/app/src/contexts/surveyContext.tsx +++ b/app/src/contexts/surveyContext.tsx @@ -4,6 +4,7 @@ import { ICritterDeploymentResponse, useTelemetryApi } from 'hooks/useTelemetryA import { IGetObservationSubmissionResponse } from 'interfaces/useDwcaApi.interface'; import { IGetSummaryResultsResponse } from 'interfaces/useSummaryResultsApi.interface'; import { + IDetailedCritterWithInternalId, IGetSampleSiteResponse, IGetSurveyAttachmentsResponse, IGetSurveyForViewResponse @@ -74,6 +75,14 @@ export interface ISurveyContext { unknown >; + /** + * The Data Loader used to load critters for a given survey + * + * @type {DataLoader<[project_id: number, survey_id: number], IDetailedCritterWithInternalId[], unknown>} + * @memberof ISurveyContext + */ + critterDataLoader: DataLoader<[project_id: number, survey_id: number], IDetailedCritterWithInternalId[], unknown>; + /** * The project ID belonging to the current project * @@ -106,6 +115,11 @@ export const SurveyContext = createContext({ ICritterDeploymentResponse[], unknown >, + critterDataLoader: {} as DataLoader< + [project_id: number, survey_id: number], + IDetailedCritterWithInternalId[], + unknown + >, projectId: -1, surveyId: -1 }); @@ -119,6 +133,7 @@ export const SurveyContextProvider = (props: PropsWithChildren = useParams(); @@ -143,6 +158,7 @@ export const SurveyContextProvider = (props: PropsWithChildren { sx={{ boxShadow: 'none' }}> - } - sx={{ - flex: '1 1 auto', - overflow: 'hidden', - py: 0.25, - pr: 1.5, - pl: 2, - gap: '24px', - '& .MuiAccordionSummary-content': { - flex: '1 1 auto', - overflow: 'hidden', - whiteSpace: 'nowrap' - } - }}> - + } sx={{ + flex: '1 1 auto', overflow: 'hidden', - textOverflow: 'ellipsis', - typography: 'body2', - fontWeight: 700, - fontSize: '0.9rem' + py: 0.25, + pr: 1.5, + pl: 2, + gap: '24px', + '& .MuiAccordionSummary-content': { + flex: '1 1 auto', + overflow: 'hidden', + whiteSpace: 'nowrap' + } }}> - {props.name} - - + + {props.name} + + + ) => console.log('Menu has been clicked')} + aria-label="settings"> + + + { const [showDialog, setShowDialog] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [critterId, setCritterId] = useState(null); useEffect(() => { surveyContext.critterDeploymentDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); + surveyContext.critterDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); }, []); + const deployments = useMemo( () => surveyContext.critterDeploymentDataLoader.data, [surveyContext.critterDeploymentDataLoader.data] ); + const critters = useMemo(() => surveyContext.critterDataLoader.data, [surveyContext.critterDataLoader.data]); return ( <> @@ -51,6 +55,7 @@ const ManualTelemetryList = () => { initialValues={{ device: [ { + survey_critter_id: null, deployments: [ { deployment_id: '', @@ -71,6 +76,8 @@ const ManualTelemetryList = () => { validateOnBlur={false} validateOnChange={true} onSubmit={async (values, actions) => { + console.log(values); + console.log(actions); console.log('THINGS ARE BEING SUBMITTED'); setIsLoading(true); // handleCritterSave @@ -86,15 +93,21 @@ const ManualTelemetryList = () => { // } // setFormMode(ANIMAL_FORM_MODE.EDIT); }}> - Butts + Critter Deployments <> - + Critter - { + setCritterId(Number(e.target.value)); + }}> + {critters?.map((item) => { + return {item.taxon}; + })} @@ -115,6 +128,8 @@ const ManualTelemetryList = () => { variant="outlined" onClick={() => { setShowDialog(false); + formikProps.resetForm(); + setCritterId(null); }}> Cancel diff --git a/app/src/hooks/useTelemetryApi.ts b/app/src/hooks/useTelemetryApi.ts index 212eac6ce9..7ed5536c02 100644 --- a/app/src/hooks/useTelemetryApi.ts +++ b/app/src/hooks/useTelemetryApi.ts @@ -1,4 +1,3 @@ -import axios from 'axios'; import { ConfigContext } from 'contexts/configContext'; import { useContext } from 'react'; import { v4 } from 'uuid'; @@ -19,7 +18,7 @@ export interface ICritterDeploymentResponse { export const useTelemetryApi = () => { const config = useContext(ConfigContext); const apiAxios = useAxios(config?.API_HOST); - const devices = useDeviceApi(axios); + const devices = useDeviceApi(apiAxios); const getCritterAndDeployments = (projectId: number, surveyId: number): Promise => { // This endpoint will fetch a list of critters and their deployments From c8f736b96af286de8a360adceea0dcba3554e9c0 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 17 Nov 2023 16:41:12 -0800 Subject: [PATCH 14/88] clean up --- .../telemetry-device/TelemetryDeviceFormContent.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx b/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx index 4884b0aaa8..20d6fc765d 100644 --- a/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx +++ b/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx @@ -19,8 +19,7 @@ const TelemetryDeviceFormContent = (props: TelemetryDeviceFormContentProps) => { const { index, mode } = props; const telemetryApi = useTelemetryApi(); - const { values, validateField, errors } = useFormikContext(); - console.log(errors); + const { values, validateField } = useFormikContext(); const device = values.device?.[index]; console.log(values.device); From 69a8b75e8a35e07f45c37f1fc7344f209eef3240 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 17 Nov 2023 16:41:32 -0800 Subject: [PATCH 15/88] added formik validation and stubbing out more work flow changes --- .../surveys/telemetry/ManualTelemetryList.tsx | 57 ++++++++++++++++--- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx index d5f8b16769..44a9f831a4 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx @@ -10,12 +10,14 @@ import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; import InputLabel from '@mui/material/InputLabel'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import ListFader from 'components/loading/SkeletonList'; import { SurveyContext } from 'contexts/surveyContext'; import { Formik } from 'formik'; +import { get } from 'lodash-es'; import { useContext, useEffect, useMemo, useState } from 'react'; import yup from 'utils/YupSchema'; import { ANIMAL_FORM_MODE } from '../view/survey-animals/animal'; @@ -26,7 +28,14 @@ import ManualTelemetryCard from './ManualTelemetryCard'; // export interface ManualTelemetryListProps { const AnimalDeploymentSchema = yup.object().shape({ - device: yup.array().of(AnimalTelemetryDeviceSchema).required() + device: yup + .array() + .of( + AnimalTelemetryDeviceSchema.shape({ + survey_critter_id: yup.number().required('An animal selection is required') // add survey critter id to form + }) + ) + .required() }); const ManualTelemetryList = () => { @@ -36,7 +45,8 @@ const ManualTelemetryList = () => { const [showDialog, setShowDialog] = useState(false); const [isLoading, setIsLoading] = useState(false); - const [critterId, setCritterId] = useState(null); + const [critterId, setCritterId] = useState(''); + const [deviceIndex, setDeviceIndex] = useState(0); useEffect(() => { surveyContext.critterDeploymentDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); @@ -48,14 +58,28 @@ const ManualTelemetryList = () => { [surveyContext.critterDeploymentDataLoader.data] ); const critters = useMemo(() => surveyContext.critterDataLoader.data, [surveyContext.critterDataLoader.data]); - + const blankDevice = { + survey_critter_id: '', + deployments: [ + { + deployment_id: '', + attachment_start: '', + attachment_end: undefined + } + ], + device_id: '', + device_make: '', + device_model: '', + frequency: '', + frequency_unit: '' + }; return ( <> { onSubmit={async (values, actions) => { console.log(values); console.log(actions); - console.log('THINGS ARE BEING SUBMITTED'); + setIsLoading(true); // handleCritterSave }}> @@ -104,13 +128,25 @@ const ManualTelemetryList = () => { value={critterId} onChange={(e) => { setCritterId(Number(e.target.value)); + formikProps.setFieldValue(`device[${deviceIndex}].survey_critter_id`, Number(e.target.value)); }}> {critters?.map((item) => { return {item.taxon}; })} + + + {get(formikProps.errors.device?.[deviceIndex], 'survey_critter_id')} + + - + @@ -127,9 +163,11 @@ const ManualTelemetryList = () => { color="primary" variant="outlined" onClick={() => { - setShowDialog(false); - formikProps.resetForm(); - setCritterId(null); + console.log('________'); + console.log(formikProps.errors); + // setShowDialog(false); + // formikProps.resetForm(); + // setCritterId(''); }}> Cancel @@ -162,6 +200,7 @@ const ManualTelemetryList = () => { startIcon={} onClick={() => { setShowDialog(true); + // AddEditAnimal: Line 244 }}> Add From 33adcfa191641122f45026ddd6ba16e4ae0660d8 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Mon, 20 Nov 2023 10:20:28 -0800 Subject: [PATCH 16/88] stubbing in menu actions --- .../surveys/telemetry/ManualTelemetryCard.tsx | 8 ++- .../surveys/telemetry/ManualTelemetryList.tsx | 61 ++++++++++++++++--- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/app/src/features/surveys/telemetry/ManualTelemetryCard.tsx b/app/src/features/surveys/telemetry/ManualTelemetryCard.tsx index 0d0b0d98b7..6ba72fd192 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryCard.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryCard.tsx @@ -7,9 +7,15 @@ import AccordionSummary from '@mui/material/AccordionSummary'; import Typography from '@mui/material/Typography'; export interface ManualTelemetryCardProps { + device_id: number; name: string; details: string; + // openMenu: () => void; + // onEdit: () => void; + // onDelete: () => void; + onMenu: (e: React.MouseEvent, id: number) => void; } + const ManualTelemetryCard = (props: ManualTelemetryCardProps) => { return ( { ) => console.log('Menu has been clicked')} + onClick={(event: React.MouseEvent) => props.onMenu(event, props.device_id)} aria-label="settings"> diff --git a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx index 44a9f831a4..3623970e3d 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx @@ -1,7 +1,7 @@ -import { mdiPlus } from '@mdi/js'; +import { mdiPencilOutline, mdiPlus, mdiTrashCanOutline } from '@mdi/js'; import Icon from '@mdi/react'; import { LoadingButton } from '@mui/lab'; -import { MenuItem, Select, useMediaQuery, useTheme } from '@mui/material'; +import { ListItemIcon, Menu, MenuItem, Select, useMediaQuery, useTheme } from '@mui/material'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import { grey } from '@mui/material/colors'; @@ -12,6 +12,7 @@ import DialogTitle from '@mui/material/DialogTitle'; import FormControl from '@mui/material/FormControl'; import FormHelperText from '@mui/material/FormHelperText'; import InputLabel from '@mui/material/InputLabel'; +import { MenuProps } from '@mui/material/Menu'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import ListFader from 'components/loading/SkeletonList'; @@ -43,6 +44,8 @@ const ManualTelemetryList = () => { const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); const surveyContext = useContext(SurveyContext); + const [anchorEl, setAnchorEl] = useState(null); + const [showDialog, setShowDialog] = useState(false); const [isLoading, setIsLoading] = useState(false); const [critterId, setCritterId] = useState(''); @@ -73,8 +76,43 @@ const ManualTelemetryList = () => { frequency: '', frequency_unit: '' }; + + const handleMenuOpen = (event: React.MouseEvent, device_id: number) => { + setAnchorEl(event.currentTarget); + }; + + const handleSubmit = () => { + console.log('HANDLE SUBMIT NEEDS TO DO STUFF'); + }; return ( <> + setAnchorEl(null)} + anchorEl={anchorEl} + anchorOrigin={{ + vertical: 'top', + horizontal: 'right' + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'right' + }}> + { + console.log("Edit clicked") + }}> + + + + Edit Details + + + + + + Delete + + { console.log(actions); setIsLoading(true); + handleSubmit(); // handleCritterSave }}> {(formikProps) => ( @@ -155,6 +194,7 @@ const ManualTelemetryList = () => { variant="contained" loading={isLoading} onClick={() => { + console.log(formikProps.values); formikProps.submitForm(); }}> Save @@ -163,11 +203,9 @@ const ManualTelemetryList = () => { color="primary" variant="outlined" onClick={() => { - console.log('________'); - console.log(formikProps.errors); - // setShowDialog(false); - // formikProps.resetForm(); - // setCritterId(''); + setShowDialog(false); + formikProps.resetForm(); + setCritterId(''); }}> Cancel @@ -218,7 +256,14 @@ const ManualTelemetryList = () => { background: grey[100] }}> {deployments?.map((item) => ( - + { + handleMenuOpen(event, id); + }} + /> ))} From b553c6f6f9ef939ca0a88f33c59c68091c871cd2 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Mon, 20 Nov 2023 15:47:02 -0800 Subject: [PATCH 17/88] got add deployment working --- .../surveys/telemetry/ManualTelemetryList.tsx | 54 +++++++++++++++---- .../TelemetryDeviceFormContent.tsx | 2 +- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx index 3623970e3d..65083f9fb7 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx @@ -16,8 +16,10 @@ import { MenuProps } from '@mui/material/Menu'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import ListFader from 'components/loading/SkeletonList'; +import { AttachmentType } from 'constants/attachments'; import { SurveyContext } from 'contexts/surveyContext'; import { Formik } from 'formik'; +import { useBiohubApi } from 'hooks/useBioHubApi'; import { get } from 'lodash-es'; import { useContext, useEffect, useMemo, useState } from 'react'; import yup from 'utils/YupSchema'; @@ -27,7 +29,10 @@ import TelemetryDeviceFormContent from '../view/survey-animals/telemetry-device/ import ManualTelemetryCard from './ManualTelemetryCard'; // export interface ManualTelemetryListProps { - +/* +1. Create new type to handle device that flattens the deployment? (no we might want to setup multiple later) +2. Create +*/ const AnimalDeploymentSchema = yup.object().shape({ device: yup .array() @@ -43,6 +48,7 @@ const ManualTelemetryList = () => { const theme = useTheme(); const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); const surveyContext = useContext(SurveyContext); + const biohubApi = useBiohubApi(); const [anchorEl, setAnchorEl] = useState(null); @@ -79,11 +85,36 @@ const ManualTelemetryList = () => { const handleMenuOpen = (event: React.MouseEvent, device_id: number) => { setAnchorEl(event.currentTarget); + setDeviceIndex(Number(deployments?.findIndex((item) => item.device_id === device_id))); }; - const handleSubmit = () => { + const handleSubmit = async (survey_critter_id: number, data: any) => { console.log('HANDLE SUBMIT NEEDS TO DO STUFF'); + + await handleAddTelemetry(survey_critter_id, data); + + if (data[deviceIndex].attachmentFile) { + await handleUploadFile(data[deviceIndex].attachmentFile, data[deviceIndex].attachmentType); + } + // ADD + }; + + const handleAddTelemetry = async (survey_critter_id: number, data: any) => { + const critter = critters?.find((a) => a.survey_critter_id === survey_critter_id); + if (!critter) console.log('Did not find critter in addTelemetry!'); + + const device = data[deviceIndex]; + device.critter_id = critter?.critter_id; + try { + await biohubApi.survey.addDeployment(surveyContext.projectId, surveyContext.surveyId, survey_critter_id, device); + surveyContext.critterDeploymentDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); + // success snack bar + } catch (error) { + // error snack bar + } }; + + const handleUploadFile = async (file?: File, attachmentType?: AttachmentType) => {}; return ( <> { vertical: 'top', horizontal: 'right' }}> - { - console.log("Edit clicked") - }}> + { + console.log('Edit clicked'); + }}> @@ -138,12 +170,12 @@ const ManualTelemetryList = () => { validateOnBlur={false} validateOnChange={true} onSubmit={async (values, actions) => { - console.log(values); - console.log(actions); - + console.log('ON SUBMIT'); setIsLoading(true); - handleSubmit(); - // handleCritterSave + await handleSubmit(Number(values.device[deviceIndex].survey_critter_id), values.device); + setIsLoading(false); + setShowDialog(false); + actions.resetForm(); }}> {(formikProps) => ( { variant="contained" loading={isLoading} onClick={() => { - console.log(formikProps.values); formikProps.submitForm(); }}> Save @@ -237,6 +268,7 @@ const ManualTelemetryList = () => { color="primary" startIcon={} onClick={() => { + setDeviceIndex(Number(deployments?.length)); setShowDialog(true); // AddEditAnimal: Line 244 }}> diff --git a/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx b/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx index 20d6fc765d..aef4f3acea 100644 --- a/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx +++ b/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx @@ -15,6 +15,7 @@ interface TelemetryDeviceFormContentProps { index: number; mode: ANIMAL_FORM_MODE; } +//TODO: Is this component needed anymore? should all telemetry now be handled by the page/ table combo const TelemetryDeviceFormContent = (props: TelemetryDeviceFormContentProps) => { const { index, mode } = props; @@ -22,7 +23,6 @@ const TelemetryDeviceFormContent = (props: TelemetryDeviceFormContentProps) => { const { values, validateField } = useFormikContext(); const device = values.device?.[index]; - console.log(values.device); const { data: deviceDetails, refresh } = useDataLoader(() => telemetryApi.devices.getDeviceDetails(device.device_id)); From 09b59854cd80d76d612c826893e4e6366e7e96c5 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 21 Nov 2023 10:48:00 -0800 Subject: [PATCH 18/88] refactored form --- .../surveys/telemetry/ManualTelemetryList.tsx | 312 +++++++++--------- .../telemetry-device/DeploymentForm.tsx | 147 +++++++++ .../telemetry-device/TelemetryDeviceForm.tsx | 162 +++++++++ .../TelemetryDeviceFormContent.tsx | 17 +- .../survey-animals/telemetry-device/device.ts | 5 + 5 files changed, 479 insertions(+), 164 deletions(-) create mode 100644 app/src/features/surveys/view/survey-animals/telemetry-device/DeploymentForm.tsx create mode 100644 app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceForm.tsx diff --git a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx index 65083f9fb7..4e80341675 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx @@ -25,7 +25,7 @@ import { useContext, useEffect, useMemo, useState } from 'react'; import yup from 'utils/YupSchema'; import { ANIMAL_FORM_MODE } from '../view/survey-animals/animal'; import { AnimalTelemetryDeviceSchema } from '../view/survey-animals/telemetry-device/device'; -import TelemetryDeviceFormContent from '../view/survey-animals/telemetry-device/TelemetryDeviceFormContent'; +import TelemetryDeviceForm from '../view/survey-animals/telemetry-device/TelemetryDeviceForm'; import ManualTelemetryCard from './ManualTelemetryCard'; // export interface ManualTelemetryListProps { @@ -33,15 +33,9 @@ import ManualTelemetryCard from './ManualTelemetryCard'; 1. Create new type to handle device that flattens the deployment? (no we might want to setup multiple later) 2. Create */ -const AnimalDeploymentSchema = yup.object().shape({ - device: yup - .array() - .of( - AnimalTelemetryDeviceSchema.shape({ - survey_critter_id: yup.number().required('An animal selection is required') // add survey critter id to form - }) - ) - .required() +export const AnimalDeploymentSchema = AnimalTelemetryDeviceSchema.shape({ + survey_critter_id: yup.number().required('An animal selection is required'), // add survey critter id to form + critter_id: yup.number().required() }); const ManualTelemetryList = () => { @@ -67,21 +61,6 @@ const ManualTelemetryList = () => { [surveyContext.critterDeploymentDataLoader.data] ); const critters = useMemo(() => surveyContext.critterDataLoader.data, [surveyContext.critterDataLoader.data]); - const blankDevice = { - survey_critter_id: '', - deployments: [ - { - deployment_id: '', - attachment_start: '', - attachment_end: undefined - } - ], - device_id: '', - device_make: '', - device_model: '', - frequency: '', - frequency_unit: '' - }; const handleMenuOpen = (event: React.MouseEvent, device_id: number) => { setAnchorEl(event.currentTarget); @@ -89,14 +68,16 @@ const ManualTelemetryList = () => { }; const handleSubmit = async (survey_critter_id: number, data: any) => { - console.log('HANDLE SUBMIT NEEDS TO DO STUFF'); - + // ADD NEW TELEMETRY await handleAddTelemetry(survey_critter_id, data); + // EDIT TELEMETRY + // TODO: add this + + // UPLOAD ANY FILES if (data[deviceIndex].attachmentFile) { await handleUploadFile(data[deviceIndex].attachmentFile, data[deviceIndex].attachmentType); } - // ADD }; const handleAddTelemetry = async (survey_critter_id: number, data: any) => { @@ -147,23 +128,19 @@ const ManualTelemetryList = () => { { onSubmit={async (values, actions) => { console.log('ON SUBMIT'); setIsLoading(true); - await handleSubmit(Number(values.device[deviceIndex].survey_critter_id), values.device); + await handleSubmit(Number(values.survey_critter_id), values); setIsLoading(false); setShowDialog(false); actions.resetForm(); }}> {(formikProps) => ( - { - // if (formMode === ANIMAL_FORM_MODE.ADD) { - // formikArrayHelpers.remove(selectedIndex); - // } - // setFormMode(ANIMAL_FORM_MODE.EDIT); - }}> - Critter Deployments - - <> - - Critter - - - + { + // if (formMode === ANIMAL_FORM_MODE.ADD) { + // formikArrayHelpers.remove(selectedIndex); + // } + // setFormMode(ANIMAL_FORM_MODE.EDIT); + }}> + Critter Deployments + + <> + + Critter + + + + {get(formikProps.errors, 'survey_critter_id')} + + + + + + + + { + formikProps.submitForm(); + }}> + Save + + + + + + + - Cancel - - - + + Deployments ‌ + + ({deployments?.length ?? 0}) + + + + + + {/* Display list of skeleton components while waiting for a response */} + + + {deployments?.map((item) => ( + { + handleMenuOpen(event, id); + }} + /> + ))} + + + + )} - - - - Deployments ‌ - - ({deployments?.length ?? 0}) - - - - - - {/* Display list of skeleton components while waiting for a response */} - - - {deployments?.map((item) => ( - { - handleMenuOpen(event, id); - }} - /> - ))} - - - ); }; diff --git a/app/src/features/surveys/view/survey-animals/telemetry-device/DeploymentForm.tsx b/app/src/features/surveys/view/survey-animals/telemetry-device/DeploymentForm.tsx new file mode 100644 index 0000000000..247b0edcbe --- /dev/null +++ b/app/src/features/surveys/view/survey-animals/telemetry-device/DeploymentForm.tsx @@ -0,0 +1,147 @@ +import { mdiTrashCanOutline } from '@mdi/js'; +import Icon from '@mdi/react'; +import { IconButton } from '@mui/material'; +import Grid from '@mui/material/Grid'; +import YesNoDialog from 'components/dialog/YesNoDialog'; +import SingleDateField from 'components/fields/SingleDateField'; +import { DialogContext } from 'contexts/dialogContext'; +import { SurveyContext } from 'contexts/surveyContext'; +import { Field, useFormikContext } from 'formik'; +import { IGetDeviceDetailsResponse } from 'hooks/telemetry/useDeviceApi'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import { useQuery } from 'hooks/useQuery'; +import { Fragment, useContext, useState } from 'react'; +import { dateRangesOverlap, setMessageSnackbar } from 'utils/Utils'; +import { ANIMAL_FORM_MODE } from '../animal'; +import { IAnimalTelemetryDevice, IDeploymentTimespan } from './device'; + +interface DeploymentFormSectionProps { + index: number; + mode: ANIMAL_FORM_MODE; + deviceDetails?: IGetDeviceDetailsResponse; +} + +export const DeploymentForm = (props: DeploymentFormSectionProps): JSX.Element => { + const { index, mode, deviceDetails } = props; + + const bhApi = useBiohubApi(); + const { cid: survey_critter_id } = useQuery(); + const { values, validateField } = useFormikContext(); + const { surveyId, projectId } = useContext(SurveyContext); + const dialogContext = useContext(DialogContext); + + const [deploymentIDToDelete, setDeploymentIDToDelete] = useState(null); + + const device = values; + const deployments = device.deployments; + + const handleRemoveDeployment = async (deployment_id: string) => { + try { + if (survey_critter_id === undefined) { + setMessageSnackbar('No critter set!', dialogContext); + } + await bhApi.survey.removeDeployment(projectId, surveyId, Number(survey_critter_id), deployment_id); + const indexOfDeployment = deployments?.findIndex((deployment) => deployment.deployment_id === deployment_id); + if (indexOfDeployment !== undefined) { + deployments?.splice(indexOfDeployment); + } + setMessageSnackbar('Deployment deleted', dialogContext); + } catch (e) { + setMessageSnackbar('Failed to delete deployment.', dialogContext); + } + }; + + const deploymentOverlapTest = (deployment: IDeploymentTimespan) => { + if (index === undefined) { + return; + } + if (!deviceDetails) { + return; + } + + if (!deployment.attachment_start) { + return; + } + const existingDeployment = deviceDetails.deployments.find( + (existingDeployment) => + deployment.deployment_id !== existingDeployment.deployment_id && + dateRangesOverlap( + deployment.attachment_start, + deployment.attachment_end, + existingDeployment.attachment_start, + existingDeployment.attachment_end + ) + ); + if (!existingDeployment) { + return; + } + return `This will conflict with an existing deployment for the device running from ${ + existingDeployment.attachment_start + } until ${existingDeployment.attachment_end ?? 'indefinite.'}`; + }; + + return ( + <> + + {deployments?.map((deploy, i) => { + return ( + + + validateField(`deployments.${i}.attachment_start`) }} + validate={() => deploymentOverlapTest(deploy)} + /> + + + validateField(`deployments.${i}.attachment_end`) }} + validate={() => deploymentOverlapTest(deploy)} + /> + + {mode === ANIMAL_FORM_MODE.EDIT && ( + + { + setDeploymentIDToDelete(String(deploy.deployment_id)); + }}> + + + + )} + + ); + })} + + + {/* Delete Dialog */} + setDeploymentIDToDelete(null)} + onNo={() => setDeploymentIDToDelete(null)} + onYes={async () => { + if (deploymentIDToDelete) { + await handleRemoveDeployment(deploymentIDToDelete); + } + setDeploymentIDToDelete(null); + }} + /> + + ); +}; diff --git a/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceForm.tsx b/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceForm.tsx new file mode 100644 index 0000000000..ff57e27359 --- /dev/null +++ b/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceForm.tsx @@ -0,0 +1,162 @@ +import { Box, Grid, Typography } from '@mui/material'; +import CustomTextField from 'components/fields/CustomTextField'; +import TelemetrySelectField from 'components/fields/TelemetrySelectField'; +import FormikDevDebugger from 'components/formik/FormikDevDebugger'; +import { AttachmentType } from 'constants/attachments'; +import { Field, useFormikContext } from 'formik'; +import useDataLoader from 'hooks/useDataLoader'; +import { useTelemetryApi } from 'hooks/useTelemetryApi'; +import { useEffect } from 'react'; +import { ANIMAL_FORM_MODE } from '../animal'; +import { DeploymentForm } from './DeploymentForm'; +import { IAnimalTelemetryDevice } from './device'; +import TelemetryFileUpload from './TelemetryFileUpload'; + +export interface ITelemetryDeviceFormProps { + index: number; + mode: ANIMAL_FORM_MODE; +} +// ok so this just needs edit and manage a single thing +// the index on the other form component will + +const TelemetryDeviceForm = (props: ITelemetryDeviceFormProps) => { + const { index, mode } = props; + + const telemetryApi = useTelemetryApi(); + const { values, validateField } = useFormikContext(); + + const device = values; + + const { data: deviceDetails, refresh } = useDataLoader(() => + telemetryApi.devices.getDeviceDetails(Number(device.device_id)) + ); + + const validateDeviceMake = async (value: number | '') => { + const deviceMake = deviceDetails?.device?.device_make; + if (device.device_id && deviceMake && deviceMake !== value && mode === ANIMAL_FORM_MODE.ADD) { + return `The current make for this device is ${deviceMake}`; + } + }; + + useEffect(() => { + if (!device.device_id || !device.device_make) { + return; + } + refresh(); + validateField(`device_make`); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [device.device_id, device.device_make, deviceDetails?.device?.device_make, index]); + + if (!device) { + return <>; + } + + return ( + <> + + + Device Metadata + + + + + + + + + + + + { + const codeVals = await telemetryApi.devices.getCodeValues('frequency_unit'); + return codeVals.map((a) => a.description); + }} + /> + + + + + + + + + + + + {((device.device_make === 'Vectronic' && !deviceDetails?.keyXStatus) || device.device_make === 'Lotek') && ( + + + Upload Attachment + + {device.device_make === 'Vectronic' && ( + <> + {`Vectronic KeyX File (Optional)`} + + + )} + {device.device_make === 'Lotek' && ( + <> + {`Lotek Config File (Optional)`} + + + )} + + )} + + + Deployments + + + + + + ); +}; + +export default TelemetryDeviceForm; diff --git a/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx b/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx index aef4f3acea..9f96eb085b 100644 --- a/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx +++ b/app/src/features/surveys/view/survey-animals/telemetry-device/TelemetryDeviceFormContent.tsx @@ -21,8 +21,21 @@ const TelemetryDeviceFormContent = (props: TelemetryDeviceFormContentProps) => { const telemetryApi = useTelemetryApi(); const { values, validateField } = useFormikContext(); - - const device = values.device?.[index]; + let device: any; + if (values.device?.[index]) { + device = values.device?.[index]; + } else { + device = { + survey_critter_id: '', + deployments: [], + device_id: '', + device_make: '', + device_model: '', + frequency: '', + frequency_unit: '' + }; + } + console.log(device); const { data: deviceDetails, refresh } = useDataLoader(() => telemetryApi.devices.getDeviceDetails(device.device_id)); diff --git a/app/src/features/surveys/view/survey-animals/telemetry-device/device.ts b/app/src/features/surveys/view/survey-animals/telemetry-device/device.ts index 00fd395456..64cc22678b 100644 --- a/app/src/features/surveys/view/survey-animals/telemetry-device/device.ts +++ b/app/src/features/surveys/view/survey-animals/telemetry-device/device.ts @@ -53,6 +53,11 @@ export interface IAnimalTelemetryDeviceFile extends IAnimalTelemetryDevice { attachmentType?: AttachmentType; } +// An interface used for creating a new Telemetry Device Deployment +export interface IAnimalTelemetryDeviceFilePost extends IAnimalTelemetryDeviceFile { + critter_id?: string; +} + export class Device implements Omit { device_id: number; device_make: string; From fceafba6dd612530b2ab9f3f10e1fc4a3399352b Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 21 Nov 2023 11:19:33 -0800 Subject: [PATCH 19/88] updated validation ui --- .../surveys/telemetry/ManualTelemetryList.tsx | 19 ++++++++++++------- .../telemetry-device/TelemetryDeviceForm.tsx | 2 -- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx index 4e80341675..891e79e5ea 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx @@ -34,8 +34,7 @@ import ManualTelemetryCard from './ManualTelemetryCard'; 2. Create */ export const AnimalDeploymentSchema = AnimalTelemetryDeviceSchema.shape({ - survey_critter_id: yup.number().required('An animal selection is required'), // add survey critter id to form - critter_id: yup.number().required() + survey_critter_id: yup.number().required('An animal selection is required') // add survey critter id to form }); const ManualTelemetryList = () => { @@ -75,7 +74,7 @@ const ManualTelemetryList = () => { // TODO: add this // UPLOAD ANY FILES - if (data[deviceIndex].attachmentFile) { + if (data.attachmentFile) { await handleUploadFile(data[deviceIndex].attachmentFile, data[deviceIndex].attachmentType); } }; @@ -84,10 +83,9 @@ const ManualTelemetryList = () => { const critter = critters?.find((a) => a.survey_critter_id === survey_critter_id); if (!critter) console.log('Did not find critter in addTelemetry!'); - const device = data[deviceIndex]; - device.critter_id = critter?.critter_id; + data.critter_id = critter?.critter_id; try { - await biohubApi.survey.addDeployment(surveyContext.projectId, surveyContext.surveyId, survey_critter_id, device); + await biohubApi.survey.addDeployment(surveyContext.projectId, surveyContext.surveyId, survey_critter_id, data); surveyContext.critterDeploymentDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); // success snack bar } catch (error) { @@ -149,10 +147,12 @@ const ManualTelemetryList = () => { onSubmit={async (values, actions) => { console.log('ON SUBMIT'); setIsLoading(true); + console.log(values); await handleSubmit(Number(values.survey_critter_id), values); setIsLoading(false); setShowDialog(false); actions.resetForm(); + setCritterId(''); }}> {(formikProps) => ( <> @@ -169,12 +169,17 @@ const ManualTelemetryList = () => { Critter Deployments <> - + Critter diff --git a/app/src/hooks/useTelemetryApi.ts b/app/src/hooks/useTelemetryApi.ts index 051a8e53b1..5fe7004f62 100644 --- a/app/src/hooks/useTelemetryApi.ts +++ b/app/src/hooks/useTelemetryApi.ts @@ -19,7 +19,16 @@ export const useTelemetryApi = () => { const apiAxios = useAxios(config?.API_HOST); const devices = useDeviceApi(apiAxios); - return { devices }; + const getManualTelemetry = async () => { + try { + const { data } = await apiAxios.get('/api/telemetry'); + return data; + } catch (e) { + console.log(e); + } + }; + + return { devices, getManualTelemetry }; }; type TelemetryApiReturnType = ReturnType; From 0e6ec5527341b35da5f9cffe0c5fc8dbea418c77 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Mon, 27 Nov 2023 16:39:52 -0700 Subject: [PATCH 36/88] stubbing out context for telemetry --- app/src/contexts/telemetryContext.tsx | 26 +++++++++++++++++++ .../telemetry/ManualTelemetryComponent.tsx | 10 +++++++ app/src/hooks/useTelemetryApi.ts | 22 +++++++++++----- 3 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 app/src/contexts/telemetryContext.tsx diff --git a/app/src/contexts/telemetryContext.tsx b/app/src/contexts/telemetryContext.tsx new file mode 100644 index 0000000000..423a5fb374 --- /dev/null +++ b/app/src/contexts/telemetryContext.tsx @@ -0,0 +1,26 @@ +import useDataLoader, { DataLoader } from 'hooks/useDataLoader'; +import { useTelemetryApi } from 'hooks/useTelemetryApi'; +import { createContext, PropsWithChildren } from 'react'; + +export type ITelemetryContext = { + telemetryDataLoader: DataLoader<[], {}, unknown>; +}; + +export const TelemetryContext = createContext({ + telemetryDataLoader: {} as DataLoader +}); + +export const TelemetryContextProvider = (props: PropsWithChildren>) => { + // const { projectId, surveyId } = useContext(SurveyContext); + const telemetryApi = useTelemetryApi(); + + const telemetryDataLoader = useDataLoader(() => telemetryApi.getManualTelemetry()); + + telemetryDataLoader.load(); + + const telemetryContext: ITelemetryContext = { + telemetryDataLoader + }; + + return {props.children}; +}; diff --git a/app/src/features/surveys/telemetry/ManualTelemetryComponent.tsx b/app/src/features/surveys/telemetry/ManualTelemetryComponent.tsx index 9fd9d960f1..c3adfab99f 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryComponent.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryComponent.tsx @@ -1,9 +1,19 @@ import Box from '@mui/material/Box'; import { grey } from '@mui/material/colors'; import Toolbar from '@mui/material/Toolbar'; +import { useTelemetryApi } from 'hooks/useTelemetryApi'; +import { useEffect } from 'react'; import ManualTelemetryTable from './ManualTelemetryTable'; const ManualTelemetryComponent = () => { + const api = useTelemetryApi(); + useEffect(() => { + const fetchData = async () => { + const temp = await api.getManualTelemetry(); + console.log(temp); + }; + fetchData(); + }, []); return ( <> { const config = useContext(ConfigContext); const apiAxios = useAxios(config?.API_HOST); const devices = useDeviceApi(apiAxios); - const getManualTelemetry = async () => { - try { - const { data } = await apiAxios.get('/api/telemetry'); - return data; - } catch (e) { - console.log(e); - } + const getManualTelemetry = async (): Promise => { + const { data } = await apiAxios.get('/api/telemetry'); + return data; + }; + + const createManualTelemetry = async (postData: IManualTelemetry): Promise => { + const { data } = await apiAxios.post('/api/telemetry'); + return data; }; return { devices, getManualTelemetry }; From fa11956b2d614a12d749cea1809fce2b338f04a7 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 29 Nov 2023 09:07:13 -0700 Subject: [PATCH 37/88] stubbed out more api changes --- app/src/contexts/telemetryContext.tsx | 6 +++--- app/src/hooks/useTelemetryApi.ts | 22 ++++++++++++++++++---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/app/src/contexts/telemetryContext.tsx b/app/src/contexts/telemetryContext.tsx index 423a5fb374..2194098de7 100644 --- a/app/src/contexts/telemetryContext.tsx +++ b/app/src/contexts/telemetryContext.tsx @@ -1,13 +1,13 @@ import useDataLoader, { DataLoader } from 'hooks/useDataLoader'; -import { useTelemetryApi } from 'hooks/useTelemetryApi'; +import { IManualTelemetry, useTelemetryApi } from 'hooks/useTelemetryApi'; import { createContext, PropsWithChildren } from 'react'; export type ITelemetryContext = { - telemetryDataLoader: DataLoader<[], {}, unknown>; + telemetryDataLoader: DataLoader<[], IManualTelemetry[], unknown>; }; export const TelemetryContext = createContext({ - telemetryDataLoader: {} as DataLoader + telemetryDataLoader: {} as DataLoader }); export const TelemetryContextProvider = (props: PropsWithChildren>) => { diff --git a/app/src/hooks/useTelemetryApi.ts b/app/src/hooks/useTelemetryApi.ts index e7bf00495f..ba5e500d41 100644 --- a/app/src/hooks/useTelemetryApi.ts +++ b/app/src/hooks/useTelemetryApi.ts @@ -14,29 +14,43 @@ export interface ICritterDeploymentResponse { taxon: string; } -export interface IManualTelemetry { +export interface ICreateManualTelemetry { deployment_id: string; latitude: number; longitude: number; date: string; } +export interface IManualTelemetry extends ICreateManualTelemetry { + telemetry_manual_id: string; +} + export const useTelemetryApi = () => { const config = useContext(ConfigContext); const apiAxios = useAxios(config?.API_HOST); const devices = useDeviceApi(apiAxios); + const getVendorTelemetry = async (): Promise => { + // const { data } = await apiAxios.get('/api/telemetry'); + return []; + }; + const getManualTelemetry = async (): Promise => { const { data } = await apiAxios.get('/api/telemetry'); return data; }; - const createManualTelemetry = async (postData: IManualTelemetry): Promise => { - const { data } = await apiAxios.post('/api/telemetry'); + const createManualTelemetry = async (postData: ICreateManualTelemetry[]): Promise => { + const { data } = await apiAxios.post('/api/telemetry', postData); + return data; + }; + + const updateManualTelemetry = async (updateData: IManualTelemetry[]) => { + const { data } = await apiAxios.patch('/api/telemetry', updateData); return data; }; - return { devices, getManualTelemetry }; + return { devices, getManualTelemetry, createManualTelemetry, updateManualTelemetry, getVendorTelemetry }; }; type TelemetryApiReturnType = ReturnType; From 29c07edf57af84ca3a70b569bca703bbafc4e8bc Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 29 Nov 2023 11:12:31 -0700 Subject: [PATCH 38/88] copying over table changes --- app/src/contexts/telemetryTableContext.tsx | 183 ++++++++++++++++++ .../telemetry/ManualTelemetryComponent.tsx | 9 + .../surveys/telemetry/ManualTelemetryPage.tsx | 5 +- .../telemetry/ManualTelemetryTable.tsx | 16 +- 4 files changed, 198 insertions(+), 15 deletions(-) create mode 100644 app/src/contexts/telemetryTableContext.tsx diff --git a/app/src/contexts/telemetryTableContext.tsx b/app/src/contexts/telemetryTableContext.tsx new file mode 100644 index 0000000000..e7b3c985df --- /dev/null +++ b/app/src/contexts/telemetryTableContext.tsx @@ -0,0 +1,183 @@ +import { GridRowId, GridRowSelectionModel, useGridApiRef } from '@mui/x-data-grid'; +import { GridApiCommunity } from '@mui/x-data-grid/internals'; +import { DialogContext } from 'contexts/dialogContext'; +import { createContext, PropsWithChildren, useContext, useState } from 'react'; +import { RowValidationError, TableValidationModel } from '../components/data-grid/DataGridValidationAlert'; +import { SurveyContext } from './surveyContext'; +import { TelemetryContext } from './telemetryContext'; + +export interface IManualTelemetryRecord { + alias: string; + device_id: number; + latitude: number; + longitude: number; + date: string; + time: string; +} + +export interface IManualTelemetryTableRow extends Partial { + id: GridRowId; +} + +export type TelemetryTableValidationModel = TableValidationModel; +export type TelemetryRowValidationError = RowValidationError; + +export type ITelemetryTableContext = { + /** + * API ref used to interface with an MUI DataGrid representing the telemetry records + */ + _muiDataGridApiRef: React.MutableRefObject; + /** + * The rows the data grid should render. + */ + rows: IManualTelemetryTableRow[]; + /** + * A setState setter for the `rows` + */ + setRows: React.Dispatch>; + /** + * Appends a new blank record to the telemetry rows + */ + addRecord: () => void; + /** + * Transitions all rows in edit mode to view mode and triggers a commit of all telemetry rows to the database. + */ + saveRecords: () => void; + /** + * Deletes all of the given records and removes them from the Observation table. + */ + deleteRecords: (observationRecords: IManualTelemetryTableRow[]) => void; + /** + * Deletes all of the currently selected records and removes them from the Observation table. + */ + deleteSelectedRecords: () => void; + /** + * Reverts all changes made to observation records within the Observation Table + */ + revertRecords: () => void; + /** + * Refreshes the Observation Table with already existing records + */ + refreshRecords: () => Promise; + /** + * Returns all of the observation table records that have been selected + */ + getSelectedRecords: () => IManualTelemetryTableRow[]; + /** + * Indicates whether the observation table has any unsaved changes + */ + hasUnsavedChanges: boolean; + /** + * Callback that should be called when a row enters edit mode. + */ + onRowEditStart: (id: GridRowId) => void; + /** + * The IDs of the selected observation table rows + */ + rowSelectionModel: GridRowSelectionModel; + /** + * Sets the IDs of the selected observation table rows + */ + onRowSelectionModelChange: (rowSelectionModel: GridRowSelectionModel) => void; + /** + * Indicates if the data is in the process of being persisted to the server. + */ + isSaving: boolean; + /** + * Indicates whether or not content in the telemetry table is loading. + */ + isLoading: boolean; + /** + * The state of the validation model + */ + validationModel: any; + /** + * Reflects the total count of telemetry records for the survey + */ + recordCount: number; + /** + * Updates the total observation count for the survey + */ + setRecordCount: (count: number) => void; +}; + +export const TelemetryTableContext = createContext({ + _muiDataGridApiRef: null as unknown as React.MutableRefObject, + rows: [], + setRows: () => {}, + addRecord: () => {}, + saveRecords: () => {}, + deleteRecords: () => undefined, + deleteSelectedRecords: () => undefined, + revertRecords: () => undefined, + refreshRecords: () => Promise.resolve(), + getSelectedRecords: () => [], + hasUnsavedChanges: false, + onRowEditStart: () => {}, + rowSelectionModel: [], + onRowSelectionModelChange: () => {}, + isSaving: false, + isLoading: false, + validationModel: {}, + recordCount: 0, + setRecordCount: () => undefined +}); + +export const TelemetryTableContextProvider = (props: PropsWithChildren>) => { + const _muiDataGridApiRef = useGridApiRef(); + const surveyContext = useContext(SurveyContext); + const telemetryContext = useContext(TelemetryContext); + const dialogContext = useContext(DialogContext); + + // The data grid rows + const [rows, setRows] = useState([]); + // Stores the currently selected row ids + const [rowSelectionModel, setRowSelectionModel] = useState([]); + // Existing rows that are in edit mode + const [modifiedRowIds, setModifiedRowIds] = useState([]); + // New rows (regardless of mode) + const [addedRowIds, setAddedRowIds] = useState([]); + // True if the rows are in the process of transitioning from edit to view mode + const [_isStoppingEdit, _setIsStoppingEdit] = useState(false); + // True if the taxonomy cache has been initialized + const [hasInitializedTaxonomyCache, setHasInitializedTaxonomyCache] = useState(false); + // True if the records are in the process of being saved to the server + const [_isSaving, _setIsSaving] = useState(false); + // Stores the current count of observations for this survey + const [observationCount, setObservationCount] = useState(0); + // Stores the current validation state of the table + const [validationModel, setValidationModel] = useState({}); + + /** + * Gets all rows from the table, including values that have been edited in the table. + */ + const _getRowsWithEditedValues = (): IManualTelemetryTableRow[] => { + const rowValues = Array.from(_muiDataGridApiRef.current.getRowModels?.()?.values()) as IManualTelemetryTableRow[]; + + return rowValues.map((row) => { + const editRow = _muiDataGridApiRef.current.state.editRows[row.id]; + if (!editRow) { + return row; + } + + return Object.entries(editRow).reduce( + (newRow, entry) => ({ ...row, ...newRow, _isModified: true, [entry[0]]: entry[1].value }), + {} + ); + }) as IManualTelemetryTableRow[]; + }; + + const _validateRows = (): TelemetryTableValidationModel | null => { + const rowValues = _getRowsWithEditedValues(); + const tableColumns = _muiDataGridApiRef.current.getAllColumns(); + + const requiredColumns: (keyof IManualTelemetryTableRow)[] = [ + 'alias', + 'device_id', + 'latitude', + 'longitude', + 'date', + 'time' + ]; + }; +}; diff --git a/app/src/features/surveys/telemetry/ManualTelemetryComponent.tsx b/app/src/features/surveys/telemetry/ManualTelemetryComponent.tsx index c3adfab99f..a15d194081 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryComponent.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryComponent.tsx @@ -1,5 +1,6 @@ import Box from '@mui/material/Box'; import { grey } from '@mui/material/colors'; +import Paper from '@mui/material/Paper'; import Toolbar from '@mui/material/Toolbar'; import { useTelemetryApi } from 'hooks/useTelemetryApi'; import { useEffect } from 'react'; @@ -24,6 +25,14 @@ const ManualTelemetryComponent = () => { sx={{ overflow: 'hidden' }}> + { - + + + diff --git a/app/src/features/surveys/telemetry/ManualTelemetryTable.tsx b/app/src/features/surveys/telemetry/ManualTelemetryTable.tsx index 32ac9bcd7a..339658d7c5 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryTable.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryTable.tsx @@ -1,21 +1,9 @@ import { cyan, grey } from '@mui/material/colors'; -import { DataGrid, GridColDef, GridRowId, useGridApiRef } from '@mui/x-data-grid'; +import { DataGrid, GridColDef, useGridApiRef } from '@mui/x-data-grid'; import { GridTableRowSkeleton } from 'components/loading/SkeletonLoaders'; +import { IManualTelemetryTableRow } from 'contexts/telemetryTableContext'; import { v4 as uuidv4 } from 'uuid'; -export interface IManualTelemetryRecord { - alias: string; - device_id: number; - latitude: number; - longitude: number; - date: string; - time: string; -} - -export interface IManualTelemetryTableRow extends Partial { - id: GridRowId; -} - const ManualTelemetryTable = () => { const tableColumns: GridColDef[] = [ { From 36e59e2b7e8fa41dc7d1305aa3f8766609da4dcf Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 29 Nov 2023 11:27:04 -0700 Subject: [PATCH 39/88] renamed file --- .../contexts/{telemetryContext.tsx => telemetryDataContext.tsx} | 0 app/src/contexts/telemetryTableContext.tsx | 2 +- app/src/features/surveys/telemetry/ManualTelemetryPage.tsx | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename app/src/contexts/{telemetryContext.tsx => telemetryDataContext.tsx} (100%) diff --git a/app/src/contexts/telemetryContext.tsx b/app/src/contexts/telemetryDataContext.tsx similarity index 100% rename from app/src/contexts/telemetryContext.tsx rename to app/src/contexts/telemetryDataContext.tsx diff --git a/app/src/contexts/telemetryTableContext.tsx b/app/src/contexts/telemetryTableContext.tsx index e7b3c985df..0f8cb4f40e 100644 --- a/app/src/contexts/telemetryTableContext.tsx +++ b/app/src/contexts/telemetryTableContext.tsx @@ -4,7 +4,7 @@ import { DialogContext } from 'contexts/dialogContext'; import { createContext, PropsWithChildren, useContext, useState } from 'react'; import { RowValidationError, TableValidationModel } from '../components/data-grid/DataGridValidationAlert'; import { SurveyContext } from './surveyContext'; -import { TelemetryContext } from './telemetryContext'; +import { TelemetryContext } from './telemetryDataContext'; export interface IManualTelemetryRecord { alias: string; diff --git a/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx b/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx index 9ba4112af9..144bb5a951 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx @@ -3,7 +3,7 @@ import CircularProgress from '@mui/material/CircularProgress'; import { grey } from '@mui/material/colors'; import Paper from '@mui/material/Paper'; import { SurveyContext } from 'contexts/surveyContext'; -import { TelemetryContextProvider } from 'contexts/telemetryContext'; +import { TelemetryContextProvider } from 'contexts/telemetryDataContext'; import { useContext } from 'react'; import SurveyObservationHeader from '../observations/SurveyObservationHeader'; import ManualTelemetryComponent from './ManualTelemetryComponent'; From ec0ce55b3b05ab47279d3cbaf5fe60fe41277fd7 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 29 Nov 2023 11:34:14 -0700 Subject: [PATCH 40/88] oops --- app/src/contexts/telemetryDataContext.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/contexts/telemetryDataContext.tsx b/app/src/contexts/telemetryDataContext.tsx index 2194098de7..d1cb5ab647 100644 --- a/app/src/contexts/telemetryDataContext.tsx +++ b/app/src/contexts/telemetryDataContext.tsx @@ -4,10 +4,12 @@ import { createContext, PropsWithChildren } from 'react'; export type ITelemetryContext = { telemetryDataLoader: DataLoader<[], IManualTelemetry[], unknown>; + vendorTelemetryDataLoader: DataLoader<[], IManualTelemetry[], unknown>; }; -export const TelemetryContext = createContext({ - telemetryDataLoader: {} as DataLoader +export const TelemetryDataContext = createContext({ + telemetryDataLoader: {} as DataLoader, + vendorTelemetryDataLoader: {} as DataLoader }); export const TelemetryContextProvider = (props: PropsWithChildren>) => { @@ -15,12 +17,14 @@ export const TelemetryContextProvider = (props: PropsWithChildren telemetryApi.getManualTelemetry()); + const vendorTelemetryDataLoader = useDataLoader(() => telemetryApi.getVendorTelemetry()); telemetryDataLoader.load(); const telemetryContext: ITelemetryContext = { - telemetryDataLoader + telemetryDataLoader, + vendorTelemetryDataLoader }; - return {props.children}; + return {props.children}; }; From 0a77b37eb0de3c8fc198035c172667f01a44fc10 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 29 Nov 2023 11:35:27 -0700 Subject: [PATCH 41/88] rename continues --- app/src/contexts/telemetryDataContext.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/contexts/telemetryDataContext.tsx b/app/src/contexts/telemetryDataContext.tsx index d1cb5ab647..ad542fa6bc 100644 --- a/app/src/contexts/telemetryDataContext.tsx +++ b/app/src/contexts/telemetryDataContext.tsx @@ -2,17 +2,17 @@ import useDataLoader, { DataLoader } from 'hooks/useDataLoader'; import { IManualTelemetry, useTelemetryApi } from 'hooks/useTelemetryApi'; import { createContext, PropsWithChildren } from 'react'; -export type ITelemetryContext = { +export type ITelemetryDataContext = { telemetryDataLoader: DataLoader<[], IManualTelemetry[], unknown>; vendorTelemetryDataLoader: DataLoader<[], IManualTelemetry[], unknown>; }; -export const TelemetryDataContext = createContext({ +export const TelemetryDataContext = createContext({ telemetryDataLoader: {} as DataLoader, vendorTelemetryDataLoader: {} as DataLoader }); -export const TelemetryContextProvider = (props: PropsWithChildren>) => { +export const TelemetryDataContextProvider = (props: PropsWithChildren>) => { // const { projectId, surveyId } = useContext(SurveyContext); const telemetryApi = useTelemetryApi(); @@ -21,10 +21,10 @@ export const TelemetryContextProvider = (props: PropsWithChildren{props.children}; + return {props.children}; }; From 2d2728d9e5cac7fabe154bfdd8210b13a6e7d7b3 Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Wed, 29 Nov 2023 10:49:29 -0800 Subject: [PATCH 42/88] SIMSBIOHUB-333: Added i18n consts --- app/src/constants/i18n.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/app/src/constants/i18n.ts b/app/src/constants/i18n.ts index 577a65729e..6930716a56 100644 --- a/app/src/constants/i18n.ts +++ b/app/src/constants/i18n.ts @@ -1,3 +1,5 @@ +import { pluralize as p } from "utils/Utils"; + export const CreateProjectI18N = { cancelTitle: 'Discard changes and exit?', cancelText: 'Any changes you have made will not be saved. Do you want to proceed?', @@ -403,10 +405,30 @@ export const ObservationsTableI18N = { submitRecordsErrorDialogTitle: 'Error Updating Observation Records', submitRecordsErrorDialogText: 'An error has occurred while attempting to update the observation records for this survey. Please try again. If the error persists, please contact your system administrator.', - removeRecordsErrorDialogTitle: 'Error Deleting Observation Record', + removeRecordsErrorDialogTitle: 'Error Deleting Observation Records', removeRecordsErrorDialogText: 'An error has occurred while attempting to delete observation records for this survey. Please try again. If the error persists, please contact your system administrator.', saveRecordsSuccessSnackbarMessage: 'Observations updated successfully.', deleteSingleRecordSuccessSnackbarMessage: 'Deleted observation record successfully.', - deleteMultipleRecordSuccessSnackbarMessage: (count: number) => `Deleted ${count} observation records successfully.` + deleteMultipleRecordSuccessSnackbarMessage: (count: number) => `Deleted ${count} ${p(count, 'observation record')} successfully.` +}; + +export const TelemetryTableI18N = { + removeAllDialogTitle: 'Discard changes?', + removeAllDialogText: 'Are you sure you want to discard all your changes? This action cannot be undone.', + removeSingleRecordDialogTitle: 'Delete record?', + removeSingleRecordDialogText: 'Are you sure you want to delete this record? This action cannot be undone.', + removeSingleRecordButtonText: 'Delete Record', + removeMultipleRecordsDialogTitle: (count: number) => `Delete ${count} records?`, + removeMultipleRecordsDialogText: 'Are you sure you want to delete these records? This action cannot be undone.', + removeMultipleRecordsButtonText: 'Delete Records', + submitRecordsErrorDialogTitle: 'Error Updating Telemetry Records', + submitRecordsErrorDialogText: + 'An error has occurred while attempting to update the telemetry records for this survey. Please try again. If the error persists, please contact your system administrator.', + removeRecordsErrorDialogTitle: 'Error Deleting Telemetry Records', + removeRecordsErrorDialogText: + 'An error has occurred while attempting to delete telemetry records for this survey. Please try again. If the error persists, please contact your system administrator.', + saveRecordsSuccessSnackbarMessage: 'Telemetry updated successfully.', + deleteSingleRecordSuccessSnackbarMessage: 'Deleted telemetry record successfully.', + deleteMultipleRecordSuccessSnackbarMessage: (count: number) => `Deleted ${count} ${p(count, 'telemetry record')} successfully.` }; From fdf9c3004260d0527cf3a77d27f2bb771606213b Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Wed, 29 Nov 2023 11:06:25 -0800 Subject: [PATCH 43/88] SIMSBIOHUB-333: Added functionality to telemetryTableContext (WIP) --- app/src/contexts/telemetryTableContext.tsx | 484 ++++++++++++++++++++- 1 file changed, 465 insertions(+), 19 deletions(-) diff --git a/app/src/contexts/telemetryTableContext.tsx b/app/src/contexts/telemetryTableContext.tsx index 0f8cb4f40e..8acb165687 100644 --- a/app/src/contexts/telemetryTableContext.tsx +++ b/app/src/contexts/telemetryTableContext.tsx @@ -1,10 +1,17 @@ -import { GridRowId, GridRowSelectionModel, useGridApiRef } from '@mui/x-data-grid'; +import { GridRowId, GridRowSelectionModel, GridValidRowModel, useGridApiRef } from '@mui/x-data-grid'; import { GridApiCommunity } from '@mui/x-data-grid/internals'; import { DialogContext } from 'contexts/dialogContext'; import { createContext, PropsWithChildren, useContext, useState } from 'react'; import { RowValidationError, TableValidationModel } from '../components/data-grid/DataGridValidationAlert'; import { SurveyContext } from './surveyContext'; -import { TelemetryContext } from './telemetryDataContext'; +import { TelemetryContext as TelemetryDataContext } from './telemetryDataContext'; +import Typography from '@mui/material/Typography'; +import moment from 'moment'; +import { useCallback, useEffect, useMemo } from 'react'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import { TelemetryTableI18N } from 'constants/i18n'; +import { APIError } from 'hooks/api/useAxios'; +import { v4 as uuidv4 } from 'uuid'; export interface IManualTelemetryRecord { alias: string; @@ -44,27 +51,27 @@ export type ITelemetryTableContext = { */ saveRecords: () => void; /** - * Deletes all of the given records and removes them from the Observation table. + * Deletes all of the given records and removes them from the Telemetry table. */ - deleteRecords: (observationRecords: IManualTelemetryTableRow[]) => void; + deleteRecords: (telemetryRecords: IManualTelemetryTableRow[]) => void; /** - * Deletes all of the currently selected records and removes them from the Observation table. + * Deletes all of the currently selected records and removes them from the Telemetry table. */ deleteSelectedRecords: () => void; /** - * Reverts all changes made to observation records within the Observation Table + * Reverts all changes made to telemetry records within the Telemetry Table */ revertRecords: () => void; /** - * Refreshes the Observation Table with already existing records + * Refreshes the Telemetry Table with already existing records */ refreshRecords: () => Promise; /** - * Returns all of the observation table records that have been selected + * Returns all of the telemetry table records that have been selected */ getSelectedRecords: () => IManualTelemetryTableRow[]; /** - * Indicates whether the observation table has any unsaved changes + * Indicates whether the telemetry table has any unsaved changes */ hasUnsavedChanges: boolean; /** @@ -72,11 +79,11 @@ export type ITelemetryTableContext = { */ onRowEditStart: (id: GridRowId) => void; /** - * The IDs of the selected observation table rows + * The IDs of the selected telemetry table rows */ rowSelectionModel: GridRowSelectionModel; /** - * Sets the IDs of the selected observation table rows + * Sets the IDs of the selected telemetry table rows */ onRowSelectionModelChange: (rowSelectionModel: GridRowSelectionModel) => void; /** @@ -96,7 +103,7 @@ export type ITelemetryTableContext = { */ recordCount: number; /** - * Updates the total observation count for the survey + * Updates the total telemetry count for the survey */ setRecordCount: (count: number) => void; }; @@ -125,8 +132,11 @@ export const TelemetryTableContext = createContext({ export const TelemetryTableContextProvider = (props: PropsWithChildren>) => { const _muiDataGridApiRef = useGridApiRef(); + + const biohubApi = useBiohubApi(); const surveyContext = useContext(SurveyContext); - const telemetryContext = useContext(TelemetryContext); + + const telemetryDataContext = useContext(TelemetryDataContext); const dialogContext = useContext(DialogContext); // The data grid rows @@ -139,12 +149,10 @@ export const TelemetryTableContextProvider = (props: PropsWithChildren([]); // True if the rows are in the process of transitioning from edit to view mode const [_isStoppingEdit, _setIsStoppingEdit] = useState(false); - // True if the taxonomy cache has been initialized - const [hasInitializedTaxonomyCache, setHasInitializedTaxonomyCache] = useState(false); // True if the records are in the process of being saved to the server const [_isSaving, _setIsSaving] = useState(false); - // Stores the current count of observations for this survey - const [observationCount, setObservationCount] = useState(0); + // Stores the current count of telemetry records for this survey + const [recordCount, setRecordCount] = useState(0); // Stores the current validation state of the table const [validationModel, setValidationModel] = useState({}); @@ -167,17 +175,455 @@ export const TelemetryTableContextProvider = (props: PropsWithChildren { const rowValues = _getRowsWithEditedValues(); const tableColumns = _muiDataGridApiRef.current.getAllColumns(); const requiredColumns: (keyof IManualTelemetryTableRow)[] = [ 'alias', - 'device_id', 'latitude', 'longitude', 'date', - 'time' + 'time', + 'device_id' ]; + + const validation = rowValues.reduce((tableModel: TelemetryTableValidationModel, row: IManualTelemetryTableRow) => { + const rowErrors: TelemetryRowValidationError[] = []; + + // Validate missing columns + const missingColumns: Set = new Set(requiredColumns.filter((column) => !row[column])); + + Array.from(missingColumns).forEach((field: keyof IManualTelemetryTableRow) => { + const columnName = tableColumns.find((column) => column.field === field)?.headerName ?? field; + rowErrors.push({ field, message: `Missing column: ${columnName}` }); + }); + + // Validate date value + if (row.date && !moment(row.date).isValid()) { + rowErrors.push({ field: 'date', message: 'Invalid date' }); + } + + // Validate time value + if (row.time === 'Invalid date') { + rowErrors.push({ field: 'time', message: 'Invalid time' }); + } + + if (rowErrors.length > 0) { + tableModel[row.id] = rowErrors; + } + + return tableModel; + }, {}); + + setValidationModel(validation); + + return Object.keys(validation).length > 0 ? validation : null; }; + + const _commitDeleteRecords = useCallback( + async (telemetryRecords: IManualTelemetryTableRow[]): Promise => { + if (!telemetryRecords.length) { + return; + } + + const allRowIdsToDelete = telemetryRecords.map((item) => String(item.id)); + + // Get all row ids that are new, which only need to be removed from local state + const addedRowIdsToDelete = allRowIdsToDelete.filter((id) => addedRowIds.includes(id)); + + // Get all row ids that are not new, which need to be deleted from the server + const modifiedRowIdsToDelete = allRowIdsToDelete.filter((id) => !addedRowIds.includes(id)); + + try { + if (modifiedRowIdsToDelete.length) { + + /** TODO + const response = await biohubApi.observation.deleteObservationRecords( + projectId, + surveyId, + modifiedRowIdsToDelete + ); + + setTelemetryCount(response.supplementaryObservationData.observationCount); + */ + } + + // Remove row IDs from validation model + setValidationModel((prevValidationModel) => + allRowIdsToDelete.reduce((newValidationModel, rowId) => { + delete newValidationModel[rowId]; + return newValidationModel; + }, prevValidationModel) + ); + + // Update all rows, removing deleted rows + setRows((current) => current.filter((item) => !allRowIdsToDelete.includes(String(item.id)))); + + // Update added rows, removing deleted rows + setAddedRowIds((current) => current.filter((id) => !addedRowIdsToDelete.includes(id))); + + // Updated editing rows, removing deleted rows + setModifiedRowIds((current) => current.filter((id) => !allRowIdsToDelete.includes(id))); + + // Close yes-no dialog + dialogContext.setYesNoDialog({ open: false }); + + // Show snackbar for successful deletion + dialogContext.setSnackbar({ + snackbarMessage: ( + + {telemetryRecords.length === 1 + ? TelemetryTableI18N.deleteSingleRecordSuccessSnackbarMessage + : TelemetryTableI18N.deleteMultipleRecordSuccessSnackbarMessage(telemetryRecords.length)} + + ), + open: true + }); + } catch { + // Close yes-no dialog + dialogContext.setYesNoDialog({ open: false }); + + // Show error dialog + dialogContext.setErrorDialog({ + onOk: () => dialogContext.setErrorDialog({ open: false }), + onClose: () => dialogContext.setErrorDialog({ open: false }), + dialogTitle: TelemetryTableI18N.removeRecordsErrorDialogTitle, + dialogText: TelemetryTableI18N.removeRecordsErrorDialogText, + open: true + }); + } + }, + [addedRowIds, dialogContext, /* biohubApi.observation, projectId, surveyId */] // TODO + ); + + const getSelectedRecords: () => IManualTelemetryTableRow[] = useCallback(() => { + if (!_muiDataGridApiRef?.current?.getRowModels) { + // Data grid is not fully initialized + return []; + } + + const rowValues = Array.from(_muiDataGridApiRef.current.getRowModels(), ([_, value]) => value); + return rowValues.filter((row): row is IManualTelemetryTableRow => + rowSelectionModel.includes((row as IManualTelemetryTableRow).id) + ); + }, [_muiDataGridApiRef, rowSelectionModel]); + + const deleteRecords = useCallback( + (telemetryRecords: IManualTelemetryTableRow[]) => { + if (!telemetryRecords.length) { + return; + } + + dialogContext.setYesNoDialog({ + dialogTitle: + telemetryRecords.length === 1 + ? TelemetryTableI18N.removeSingleRecordDialogTitle + : TelemetryTableI18N.removeMultipleRecordsDialogTitle(telemetryRecords.length), + dialogText: + telemetryRecords.length === 1 + ? TelemetryTableI18N.removeSingleRecordDialogText + : TelemetryTableI18N.removeMultipleRecordsDialogText, + yesButtonProps: { + color: 'error', + loading: false + }, + yesButtonLabel: + telemetryRecords.length === 1 + ? TelemetryTableI18N.removeSingleRecordButtonText + : TelemetryTableI18N.removeMultipleRecordsButtonText, + noButtonProps: { color: 'primary', variant: 'outlined', disabled: false }, + noButtonLabel: 'Cancel', + open: true, + onYes: () => _commitDeleteRecords(telemetryRecords), + onClose: () => dialogContext.setYesNoDialog({ open: false }), + onNo: () => dialogContext.setYesNoDialog({ open: false }) + }); + }, + [_commitDeleteRecords, dialogContext] + ); + + const deleteSelectedRecords = useCallback(() => { + const selectedRecords = getSelectedRecords(); + if (!selectedRecords.length) { + return; + } + + deleteRecords(selectedRecords); + }, [deleteRecords, getSelectedRecords]); + + const onRowEditStart = (id: GridRowId) => { + setModifiedRowIds((current) => Array.from(new Set([...current, String(id)]))); + }; + + /** + * Add a new empty record to the data grid. + */ + const addRecord = useCallback(() => { + const id = uuidv4(); + + const newRecord: IManualTelemetryTableRow = { + id, + alias: '', + device_id: null as unknown as number, + latitude: null as unknown as number, + longitude: null as unknown as number, + date: '', + time: '' + }; + + // Append new record to initial rows + setRows([...rows, newRecord]); + + setAddedRowIds((current) => [...current, id]); + + // Set edit mode for the new row + _muiDataGridApiRef.current.startRowEditMode({ id, fieldToFocus: 'wldtaxonomic_units' }); + }, [_muiDataGridApiRef, rows]); + + /** + * Transition all editable rows from edit mode to view mode. + */ + const saveRecords = useCallback(() => { + if (_isStoppingEdit) { + // Stop edit mode already in progress + return; + } + + // Validate rows + const validationErrors = _validateRows(); + + if (validationErrors) { + return; + } + + _setIsStoppingEdit(true); + + // Collect the ids of all rows in edit mode + const allEditingIds = Object.keys(_muiDataGridApiRef.current.state.editRows); + + // Remove any row ids that the data grid might still be tracking, but which have been removed from local state + const editingIdsToSave = allEditingIds.filter((id) => rows.find((row) => String(row.id) === id)); + + if (!editingIdsToSave.length) { + // No rows in edit mode, nothing to stop or save + _setIsStoppingEdit(false); + return; + } + + // Transition all rows in edit mode to view mode + for (const id of editingIdsToSave) { + _muiDataGridApiRef.current.stopRowEditMode({ id }); + } + + // Store ids of rows that were in edit mode + setModifiedRowIds(editingIdsToSave); + }, [_muiDataGridApiRef, _isStoppingEdit, rows]); + + /** + * Transition all rows tracked by `modifiedRowIds` to view mode. + */ + const _revertAllRowsEditMode = useCallback(() => { + modifiedRowIds.forEach((id) => _muiDataGridApiRef.current.startRowEditMode({ id })); + }, [_muiDataGridApiRef, modifiedRowIds]); + + const revertRecords = useCallback(() => { + // Mark all rows as saved + setModifiedRowIds([]); + setAddedRowIds([]); + + // Revert any current edits + const editingIds = Object.keys(_muiDataGridApiRef.current.state.editRows); + editingIds.forEach((id) => _muiDataGridApiRef.current.stopRowEditMode({ id, ignoreModifications: true })); + + // Remove any rows that are newly created + setRows(rows.filter((row) => !addedRowIds.includes(String(row.id)))); + }, [_muiDataGridApiRef, addedRowIds, rows]); + + const refreshRecords = useCallback(async () => { + return telemetryDataContext.telemetryDataLoader.refresh(); + }, [ telemetryDataContext.telemetryDataLoader ]); + + // True if the data grid contains at least 1 unsaved record + const hasUnsavedChanges = modifiedRowIds.length > 0 || addedRowIds.length > 0; + + /** + * Send all telemetry rows to the backend. + */ + const _saveRecords = useCallback( + async (rowsToSave: GridValidRowModel[]) => { + try { + + /** TODO + await biohubApi.observation.insertUpdateObservationRecords( + projectId, + surveyId, + rowsToSave as IObservationTableRow[] + ); + */ + + setModifiedRowIds([]); + setAddedRowIds([]); + + dialogContext.setSnackbar({ + snackbarMessage: ( + + {TelemetryTableI18N.saveRecordsSuccessSnackbarMessage} + + ), + open: true + }); + + return refreshRecords(); + } catch (error) { + _revertAllRowsEditMode(); + const apiError = error as APIError; + dialogContext.setErrorDialog({ + onOk: () => dialogContext.setErrorDialog({ open: false }), + onClose: () => dialogContext.setErrorDialog({ open: false }), + dialogTitle: TelemetryTableI18N.submitRecordsErrorDialogTitle, + dialogText: TelemetryTableI18N.submitRecordsErrorDialogText, + dialogErrorDetails: apiError.errors, + open: true + }); + } finally { + _setIsSaving(false); + } + }, + [/* biohubApi.observation, projectId, surveyId,*/ dialogContext, refreshRecords, _revertAllRowsEditMode] // TODO + ); + + const isLoading: boolean = useMemo(() => { + return telemetryDataContext.telemetryDataLoader.isLoading; + }, [telemetryDataContext.telemetryDataLoader.isLoading]); + + const isSaving: boolean = useMemo(() => { + return _isSaving || _isStoppingEdit; + }, [_isSaving, _isStoppingEdit]); + + /** + * Runs when telemetry context data has changed. This does not occur when records are + * deleted; Only on initial page load, and whenever records are saved. + */ + useEffect(() => { + if (!telemetryDataContext.telemetryDataLoader.hasLoaded) { + // Existing telemetry records have not yet loaded + return; + } + + if (!telemetryDataContext.telemetryDataLoader.data) { + // Existing telemetry data doesn't exist + return; + } + + // Collect rows from the telemetry data loader + + /** TODO + const rows: IManualTelemetryTableRow[] = telemetryDataContext.telemetryDataLoader.data.surveyObservations.map( + (row: IObservationRecord) => ({ ...row, id: String(row.survey_observation_id) }) + ); + */ + + const rows: IManualTelemetryTableRow[] = [] // TODO placeholder; see above + + // Set initial rows for the table context + setRows(rows); + + // Set initial record count + // setRecordCount(observationsContext.observationsDataLoader.data.supplementaryObservationData.observationCount); // TODO + + }, [telemetryDataContext.telemetryDataLoader.data, telemetryDataContext.telemetryDataLoader.hasLoaded]); + + /** + * Runs when row records are being saved and transitioned from Edit mode to View mode. + */ + useEffect(() => { + if (!_muiDataGridApiRef?.current?.getRowModels) { + // Data grid is not fully initialized + return; + } + + if (!_isStoppingEdit) { + // Stop edit mode not in progress, cannot save yet + return; + } + + if (!modifiedRowIds.length) { + // No rows to save + return; + } + + if (_isSaving) { + // Saving already in progress + return; + } + + if (modifiedRowIds.some((id) => _muiDataGridApiRef.current.getRowMode(id) === 'edit')) { + // Not all rows have transitioned to view mode, cannot save yet + return; + } + + // All rows have transitioned to view mode + _setIsStoppingEdit(false); + + // Start saving records + _setIsSaving(true); + + const rowModels = _muiDataGridApiRef.current.getRowModels(); + const rowValues = Array.from(rowModels, ([_, value]) => value); + + _saveRecords(rowValues); + }, [_muiDataGridApiRef, _saveRecords, _isSaving, _isStoppingEdit, modifiedRowIds]); + + const telemetryTableContext: ITelemetryTableContext = useMemo( + () => ({ + _muiDataGridApiRef, + rows, + setRows, + addRecord, + saveRecords, + deleteRecords, + deleteSelectedRecords, + revertRecords, + refreshRecords, + getSelectedRecords, + hasUnsavedChanges, + onRowEditStart, + rowSelectionModel, + onRowSelectionModelChange: setRowSelectionModel, + isLoading, + isSaving, + validationModel, + recordCount, + setRecordCount + }), + [ + _muiDataGridApiRef, + rows, + addRecord, + saveRecords, + deleteRecords, + deleteSelectedRecords, + revertRecords, + refreshRecords, + getSelectedRecords, + hasUnsavedChanges, + rowSelectionModel, + isLoading, + validationModel, + isSaving, + recordCount + ] + ); + + return ( + + {props.children} + + ); }; From 8af099ee37bcf64b7790d3024c70753c08e13457 Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Wed, 29 Nov 2023 11:06:57 -0800 Subject: [PATCH 44/88] SIMSBIOHUB-333: Fix import --- app/src/contexts/telemetryTableContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/contexts/telemetryTableContext.tsx b/app/src/contexts/telemetryTableContext.tsx index 8acb165687..fb7a40724e 100644 --- a/app/src/contexts/telemetryTableContext.tsx +++ b/app/src/contexts/telemetryTableContext.tsx @@ -4,7 +4,7 @@ import { DialogContext } from 'contexts/dialogContext'; import { createContext, PropsWithChildren, useContext, useState } from 'react'; import { RowValidationError, TableValidationModel } from '../components/data-grid/DataGridValidationAlert'; import { SurveyContext } from './surveyContext'; -import { TelemetryContext as TelemetryDataContext } from './telemetryDataContext'; +import { TelemetryDataContext } from './telemetryDataContext'; import Typography from '@mui/material/Typography'; import moment from 'moment'; import { useCallback, useEffect, useMemo } from 'react'; From 57d13deca130e6662593dd295c0be0389eb2788b Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Wed, 29 Nov 2023 15:14:12 -0800 Subject: [PATCH 45/88] refactored manual telemetry path format and updated openApi schemas --- .../telemetry/{ => manual}/delete.test.ts | 4 +- .../paths/telemetry/{ => manual}/delete.ts | 28 ++--- api/src/paths/telemetry/manual/deployments.ts | 64 +++++++++++ .../telemetry/{ => manual}/index.test.ts | 4 +- api/src/paths/telemetry/{ => manual}/index.ts | 16 +-- api/src/paths/telemetry/vendor/deployments.ts | 106 ++++++++++++++++++ api/src/services/bctw-service.test.ts | 27 ++++- api/src/services/bctw-service.ts | 25 +++++ 8 files changed, 244 insertions(+), 30 deletions(-) rename api/src/paths/telemetry/{ => manual}/delete.test.ts (90%) rename api/src/paths/telemetry/{ => manual}/delete.ts (64%) create mode 100644 api/src/paths/telemetry/manual/deployments.ts rename api/src/paths/telemetry/{ => manual}/index.test.ts (96%) rename api/src/paths/telemetry/{ => manual}/index.ts (92%) create mode 100644 api/src/paths/telemetry/vendor/deployments.ts diff --git a/api/src/paths/telemetry/delete.test.ts b/api/src/paths/telemetry/manual/delete.test.ts similarity index 90% rename from api/src/paths/telemetry/delete.test.ts rename to api/src/paths/telemetry/manual/delete.test.ts index 7fd0c305c7..cd2a0ba632 100644 --- a/api/src/paths/telemetry/delete.test.ts +++ b/api/src/paths/telemetry/manual/delete.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import sinon from 'sinon'; -import { BctwService, IManualTelemetry } from '../../services/bctw-service'; -import { getRequestHandlerMocks } from '../../__mocks__/db'; +import { BctwService, IManualTelemetry } from '../../../services/bctw-service'; +import { getRequestHandlerMocks } from '../../../__mocks__/db'; import { deleteManualTelemetry } from './delete'; const mockTelemetry = ([ diff --git a/api/src/paths/telemetry/delete.ts b/api/src/paths/telemetry/manual/delete.ts similarity index 64% rename from api/src/paths/telemetry/delete.ts rename to api/src/paths/telemetry/manual/delete.ts index 6f529a1690..2fd6b68173 100644 --- a/api/src/paths/telemetry/delete.ts +++ b/api/src/paths/telemetry/manual/delete.ts @@ -1,10 +1,10 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; import { manual_telemetry_responses } from '.'; -import { authorizeRequestHandler } from '../../request-handlers/security/authorization'; -import { BctwService, IBctwUser } from '../../services/bctw-service'; -import { getLogger } from '../../utils/logger'; -const defaultLog = getLogger('paths/telemetry/delete'); +import { authorizeRequestHandler } from '../../../request-handlers/security/authorization'; +import { BctwService, IBctwUser } from '../../../services/bctw-service'; +import { getLogger } from '../../../utils/logger'; +const defaultLog = getLogger('paths/telemetry/manual/delete'); export const POST: Operation = [ authorizeRequestHandler(() => { @@ -33,19 +33,13 @@ POST.apiDoc = { content: { 'application/json': { schema: { - title: 'Manual Telemetry delete request object', - type: 'object', - required: ['telemetry_manual_ids'], - properties: { - telemetry_manual_ids: { - type: 'array', - minItems: 1, - items: { - title: 'telemetry_manual_ids', - type: 'string', - format: 'uuid' - } - } + title: 'Manual Telemetry ids to delete', + type: 'array', + minItems: 1, + items: { + title: 'telemetry manual ids', + type: 'string', + format: 'uuid' } } } diff --git a/api/src/paths/telemetry/manual/deployments.ts b/api/src/paths/telemetry/manual/deployments.ts new file mode 100644 index 0000000000..8b23fec263 --- /dev/null +++ b/api/src/paths/telemetry/manual/deployments.ts @@ -0,0 +1,64 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { manual_telemetry_responses } from '.'; +import { authorizeRequestHandler } from '../../../request-handlers/security/authorization'; +import { BctwService, getBctwUser } from '../../../services/bctw-service'; +import { getLogger } from '../../../utils/logger'; + +const defaultLog = getLogger('paths/telemetry/manual'); + +export const POST: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + discriminator: 'SystemUser' + } + ] + }; + }), + getManualTelemetryByDeploymentIds() +]; + +POST.apiDoc = { + description: 'Get a list of manually created telemetry by deployment ids', + tags: ['telemetry'], + security: [ + { + Bearer: [] + } + ], + responses: manual_telemetry_responses, + requestBody: { + description: 'Request body', + required: true, + content: { + 'application/json': { + schema: { + title: 'Manual Telemetry deployment ids', + type: 'array', + minItems: 1, + items: { + title: 'Manual telemetry deployment ids', + type: 'string', + format: 'uuid' + } + } + } + } + } +}; + +export function getManualTelemetryByDeploymentIds(): RequestHandler { + return async (req, res) => { + const user = getBctwUser(req); + const bctwService = new BctwService(user); + try { + const result = await bctwService.getManualTelemetryByDeploymentIds(req.body); + return res.status(200).json(result); + } catch (error) { + defaultLog.error({ label: 'getManualTelemetryByDeploymentIds', message: 'error', error }); + throw error; + } + }; +} diff --git a/api/src/paths/telemetry/index.test.ts b/api/src/paths/telemetry/manual/index.test.ts similarity index 96% rename from api/src/paths/telemetry/index.test.ts rename to api/src/paths/telemetry/manual/index.test.ts index 4a7065f3fb..2d9774b5bc 100644 --- a/api/src/paths/telemetry/index.test.ts +++ b/api/src/paths/telemetry/manual/index.test.ts @@ -2,8 +2,8 @@ import Ajv from 'ajv'; import { expect } from 'chai'; import sinon from 'sinon'; import { createManualTelemetry, GET, getManualTelemetry, PATCH, POST, updateManualTelemetry } from '.'; -import { BctwService, IManualTelemetry } from '../../services/bctw-service'; -import { getRequestHandlerMocks } from '../../__mocks__/db'; +import { BctwService, IManualTelemetry } from '../../../services/bctw-service'; +import { getRequestHandlerMocks } from '../../../__mocks__/db'; const mockTelemetry = ([ { diff --git a/api/src/paths/telemetry/index.ts b/api/src/paths/telemetry/manual/index.ts similarity index 92% rename from api/src/paths/telemetry/index.ts rename to api/src/paths/telemetry/manual/index.ts index b88f9f817e..6cd181a383 100644 --- a/api/src/paths/telemetry/index.ts +++ b/api/src/paths/telemetry/manual/index.ts @@ -1,10 +1,10 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { authorizeRequestHandler } from '../../request-handlers/security/authorization'; -import { BctwService, getBctwUser } from '../../services/bctw-service'; -import { getLogger } from '../../utils/logger'; +import { authorizeRequestHandler } from '../../../request-handlers/security/authorization'; +import { BctwService, getBctwUser } from '../../../services/bctw-service'; +import { getLogger } from '../../../utils/logger'; -const defaultLog = getLogger('paths/telemetry'); +const defaultLog = getLogger('paths/telemetry/manual'); export const manual_telemetry_responses = { 200: { @@ -20,7 +20,7 @@ export const manual_telemetry_responses = { deployment_id: { type: 'string' }, latitude: { type: 'number' }, longitude: { type: 'number' }, - date: { type: 'string' } + acquisition_date: { type: 'string' } } } } @@ -116,7 +116,7 @@ POST.apiDoc = { items: { title: 'manual telemetry records', type: 'object', - required: ['deployment_id', 'latitude', 'longitude', 'date'], + required: ['deployment_id', 'latitude', 'longitude', 'acquisition_date'], properties: { deployment_id: { type: 'string', @@ -128,7 +128,7 @@ POST.apiDoc = { longitude: { type: 'number' }, - date: { + acquisition_date: { type: 'string' } } @@ -204,7 +204,7 @@ PATCH.apiDoc = { longitude: { type: 'number' }, - date: { + acquisition_date: { type: 'string' } } diff --git a/api/src/paths/telemetry/vendor/deployments.ts b/api/src/paths/telemetry/vendor/deployments.ts new file mode 100644 index 0000000000..9f3e7eedb3 --- /dev/null +++ b/api/src/paths/telemetry/vendor/deployments.ts @@ -0,0 +1,106 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { authorizeRequestHandler } from '../../../request-handlers/security/authorization'; +import { BctwService, getBctwUser } from '../../../services/bctw-service'; +import { getLogger } from '../../../utils/logger'; + +const defaultLog = getLogger('paths/telemetry/manual'); + +const vendor_telemetry_responses = { + 200: { + description: 'Manual telemetry response object', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + type: 'object', + properties: { + telemetry_id: { type: 'string', format: 'uuid' }, + deployment_id: { type: 'string', format: 'uuid' }, + collar_transaction_id: { type: 'string', format: 'uuid' }, + critter_id: { type: 'string', format: 'uuid' }, + deviceid: { type: 'number' }, + latitude: { type: 'number', nullable: true }, + longitude: { type: 'number', nullable: true }, + elevation: { type: 'number', nullable: true }, + vendor: { type: 'string', nullable: true }, + acquisition_date: { type: 'string', nullable: true } + } + } + } + } + } + }, + 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' + } +}; + +export const POST: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + discriminator: 'SystemUser' + } + ] + }; + }), + getVendorTelemetryByDeploymentIds() +]; + +POST.apiDoc = { + description: 'Get a list of vendor retrieved telemetry by deployment ids', + tags: ['telemetry'], + security: [ + { + Bearer: [] + } + ], + responses: vendor_telemetry_responses, + requestBody: { + description: 'Request body', + required: true, + content: { + 'application/json': { + schema: { + title: 'Telemetry for Deployment ids', + type: 'array', + minItems: 1, + items: { + title: 'Vendor telemetry deployment ids', + type: 'string', + format: 'uuid' + } + } + } + } + } +}; + +export function getVendorTelemetryByDeploymentIds(): RequestHandler { + return async (req, res) => { + const user = getBctwUser(req); + const bctwService = new BctwService(user); + try { + const result = await bctwService.getVendorTelemetryByDeploymentIds(req.body); + return res.status(200).json(result); + } catch (error) { + defaultLog.error({ label: 'getManualTelemetryByDeploymentIds', message: 'error', error }); + throw error; + } + }; +} diff --git a/api/src/services/bctw-service.test.ts b/api/src/services/bctw-service.test.ts index 2d830b96df..12d2a46b99 100755 --- a/api/src/services/bctw-service.test.ts +++ b/api/src/services/bctw-service.test.ts @@ -23,7 +23,8 @@ import { IDeploymentUpdate, MANUAL_TELEMETRY, UPDATE_DEPLOYMENT_ENDPOINT, - UPSERT_DEVICE_ENDPOINT + UPSERT_DEVICE_ENDPOINT, + VENDOR_TELEMETRY } from './bctw-service'; import { KeycloakService } from './keycloak-service'; @@ -368,5 +369,29 @@ describe('BctwService', () => { expect(ret).to.be.true; }); }); + + describe('getManualTelemetryByDeploymentIds', () => { + it('should sent a post request', async () => { + const mockAxios = sinon.stub(bctwService.axiosInstance, 'post').resolves({ data: true }); + + const payload: any = { key: 'value' }; + const ret = await bctwService.getManualTelemetryByDeploymentIds(payload); + + expect(mockAxios).to.have.been.calledOnceWith(`${MANUAL_TELEMETRY}/deployments`, payload); + expect(ret).to.be.true; + }); + }); + + describe('getVendorTelemetryByDeploymentIds', () => { + it('should sent a post request', async () => { + const mockAxios = sinon.stub(bctwService.axiosInstance, 'post').resolves({ data: true }); + + const payload: any = { key: 'value' }; + const ret = await bctwService.getVendorTelemetryByDeploymentIds(payload); + + expect(mockAxios).to.have.been.calledOnceWith(`${VENDOR_TELEMETRY}/deployments`, payload); + expect(ret).to.be.true; + }); + }); }); }); diff --git a/api/src/services/bctw-service.ts b/api/src/services/bctw-service.ts index b00dfd1009..de4da3249d 100644 --- a/api/src/services/bctw-service.ts +++ b/api/src/services/bctw-service.ts @@ -131,6 +131,7 @@ export const GET_KEYX_STATUS_ENDPOINT = '/get-collars-keyx'; export const GET_TELEMETRY_POINTS_ENDPOINT = '/get-critters'; export const GET_TELEMETRY_TRACKS_ENDPOINT = '/get-critter-tracks'; export const MANUAL_TELEMETRY = '/manual-telemetry'; +export const VENDOR_TELEMETRY = '/vendor-telemetry'; export const DELETE_MANUAL_TELEMETRY = '/manual-telemetry/delete'; export const getBctwUser = (req: Request): IBctwUser => ({ @@ -450,6 +451,30 @@ export class BctwService { return this._makeGetRequest(MANUAL_TELEMETRY); } + /** + * retrieves manual telemetry from list of deployment ids + * + * @async + * @param {string[]} deployment_ids - bctw deployment_id + * @returns {*} IManualTelemetry[] + */ + async getManualTelemetryByDeploymentIds(deployment_ids: string[]): Promise { + const res = await this.axiosInstance.post(`${MANUAL_TELEMETRY}/deployments`, deployment_ids); + return res.data; + } + + /** + * retrieves manual telemetry from list of deployment ids + * + * @async + * @param {string[]} deployment_ids - bctw deployment_id + * @returns {*} IManualTelemetry[] + */ + async getVendorTelemetryByDeploymentIds(deployment_ids: string[]): Promise { + const res = await this.axiosInstance.post(`${VENDOR_TELEMETRY}/deployments`, deployment_ids); + return res.data; + } + /** * Delete manual telemetry records by telemetry_manual_id * Note: This is a post request that accepts an array of ids From 25efa0e1354e05cda485c3efc2cc93621ca11ea1 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 30 Nov 2023 11:46:13 -0700 Subject: [PATCH 46/88] updating telemetry api --- app/src/contexts/telemetryDataContext.tsx | 16 ++++----- .../surveys/telemetry/ManualTelemetryList.tsx | 2 -- app/src/hooks/useTelemetryApi.ts | 34 ++++++++++++++----- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/app/src/contexts/telemetryDataContext.tsx b/app/src/contexts/telemetryDataContext.tsx index ad542fa6bc..92f9017613 100644 --- a/app/src/contexts/telemetryDataContext.tsx +++ b/app/src/contexts/telemetryDataContext.tsx @@ -1,25 +1,25 @@ import useDataLoader, { DataLoader } from 'hooks/useDataLoader'; -import { IManualTelemetry, useTelemetryApi } from 'hooks/useTelemetryApi'; +import { IManualTelemetry, IVendorTelemetry, useTelemetryApi } from 'hooks/useTelemetryApi'; import { createContext, PropsWithChildren } from 'react'; export type ITelemetryDataContext = { - telemetryDataLoader: DataLoader<[], IManualTelemetry[], unknown>; - vendorTelemetryDataLoader: DataLoader<[], IManualTelemetry[], unknown>; + telemetryDataLoader: DataLoader<[ids: string[]], IManualTelemetry[], unknown>; + vendorTelemetryDataLoader: DataLoader<[ids: string[]], IVendorTelemetry[], unknown>; }; export const TelemetryDataContext = createContext({ - telemetryDataLoader: {} as DataLoader, - vendorTelemetryDataLoader: {} as DataLoader + telemetryDataLoader: {} as DataLoader<[ids: string[]], IManualTelemetry[], unknown>, + vendorTelemetryDataLoader: {} as DataLoader<[ids: string[]], IVendorTelemetry[], unknown> }); export const TelemetryDataContextProvider = (props: PropsWithChildren>) => { // const { projectId, surveyId } = useContext(SurveyContext); const telemetryApi = useTelemetryApi(); - const telemetryDataLoader = useDataLoader(() => telemetryApi.getManualTelemetry()); - const vendorTelemetryDataLoader = useDataLoader(() => telemetryApi.getVendorTelemetry()); + const telemetryDataLoader = useDataLoader(telemetryApi.getManualTelemetry); + const vendorTelemetryDataLoader = useDataLoader(telemetryApi.getVendorTelemetry); - telemetryDataLoader.load(); + // telemetryDataLoader.load(); const telemetryDataContext: ITelemetryDataContext = { telemetryDataLoader, diff --git a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx index e3d24acf56..2a4442f8b6 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx @@ -108,8 +108,6 @@ const ManualTelemetryList = () => { return data; }, [surveyContext.critterDataLoader.data, surveyContext.deploymentDataLoader.data]); - console.log(critterDeployments); - const handleMenuOpen = async (event: React.MouseEvent, device_id: number) => { setAnchorEl(event.currentTarget); setDeviceId(device_id); diff --git a/app/src/hooks/useTelemetryApi.ts b/app/src/hooks/useTelemetryApi.ts index ba5e500d41..1aa92a4512 100644 --- a/app/src/hooks/useTelemetryApi.ts +++ b/app/src/hooks/useTelemetryApi.ts @@ -18,39 +18,55 @@ export interface ICreateManualTelemetry { deployment_id: string; latitude: number; longitude: number; - date: string; + acquisition_date: string; } export interface IManualTelemetry extends ICreateManualTelemetry { telemetry_manual_id: string; } +export interface IVendorTelemetry extends ICreateManualTelemetry { + telemetry_id: string; +} + export const useTelemetryApi = () => { const config = useContext(ConfigContext); const apiAxios = useAxios(config?.API_HOST); const devices = useDeviceApi(apiAxios); - const getVendorTelemetry = async (): Promise => { - // const { data } = await apiAxios.get('/api/telemetry'); - return []; + const getVendorTelemetry = async (ids: string[]): Promise => { + const { data } = await apiAxios.post('/api/telemetry/vendor/deployments', ids); + return data; }; - const getManualTelemetry = async (): Promise => { - const { data } = await apiAxios.get('/api/telemetry'); + const getManualTelemetry = async (ids: string[]): Promise => { + const { data } = await apiAxios.post('/api/telemetry/manual/deployments', ids); return data; }; const createManualTelemetry = async (postData: ICreateManualTelemetry[]): Promise => { - const { data } = await apiAxios.post('/api/telemetry', postData); + const { data } = await apiAxios.post('/api/telemetry/manual', postData); return data; }; const updateManualTelemetry = async (updateData: IManualTelemetry[]) => { - const { data } = await apiAxios.patch('/api/telemetry', updateData); + const { data } = await apiAxios.patch('/api/telemetry/manual', updateData); return data; }; - return { devices, getManualTelemetry, createManualTelemetry, updateManualTelemetry, getVendorTelemetry }; + const deleteManualTelemetry = async (ids: string[]) => { + const { data } = await apiAxios.post('/api/telemetry/manual/delete', ids); + return data; + }; + + return { + devices, + getManualTelemetry, + createManualTelemetry, + updateManualTelemetry, + getVendorTelemetry, + deleteManualTelemetry + }; }; type TelemetryApiReturnType = ReturnType; From 57e5982abd717334204c5909cd69eec70e6c433c Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 30 Nov 2023 13:48:28 -0700 Subject: [PATCH 47/88] added main buttons and dialog to telemetry table --- app/src/contexts/telemetryTableContext.tsx | 90 ++++++-- .../telemetry/ManualTelemetryComponent.tsx | 199 ++++++++++++++++-- .../surveys/telemetry/ManualTelemetryPage.tsx | 11 +- app/src/hooks/useTelemetryApi.ts | 58 ++++- 4 files changed, 305 insertions(+), 53 deletions(-) diff --git a/app/src/contexts/telemetryTableContext.tsx b/app/src/contexts/telemetryTableContext.tsx index fb7a40724e..005d4e8b6c 100644 --- a/app/src/contexts/telemetryTableContext.tsx +++ b/app/src/contexts/telemetryTableContext.tsx @@ -1,17 +1,16 @@ +import Typography from '@mui/material/Typography'; import { GridRowId, GridRowSelectionModel, GridValidRowModel, useGridApiRef } from '@mui/x-data-grid'; import { GridApiCommunity } from '@mui/x-data-grid/internals'; +import { TelemetryTableI18N } from 'constants/i18n'; import { DialogContext } from 'contexts/dialogContext'; -import { createContext, PropsWithChildren, useContext, useState } from 'react'; +import { APIError } from 'hooks/api/useAxios'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import moment from 'moment'; +import { createContext, PropsWithChildren, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { v4 as uuidv4 } from 'uuid'; import { RowValidationError, TableValidationModel } from '../components/data-grid/DataGridValidationAlert'; import { SurveyContext } from './surveyContext'; import { TelemetryDataContext } from './telemetryDataContext'; -import Typography from '@mui/material/Typography'; -import moment from 'moment'; -import { useCallback, useEffect, useMemo } from 'react'; -import { useBiohubApi } from 'hooks/useBioHubApi'; -import { TelemetryTableI18N } from 'constants/i18n'; -import { APIError } from 'hooks/api/useAxios'; -import { v4 as uuidv4 } from 'uuid'; export interface IManualTelemetryRecord { alias: string; @@ -132,7 +131,7 @@ export const TelemetryTableContext = createContext({ export const TelemetryTableContextProvider = (props: PropsWithChildren>) => { const _muiDataGridApiRef = useGridApiRef(); - + const biohubApi = useBiohubApi(); const surveyContext = useContext(SurveyContext); @@ -175,7 +174,6 @@ export const TelemetryTableContextProvider = (props: PropsWithChildren = new Set(requiredColumns.filter((column) => !row[column])); + const missingColumns: Set = new Set( + requiredColumns.filter((column) => !row[column]) + ); Array.from(missingColumns).forEach((field: keyof IManualTelemetryTableRow) => { const columnName = tableColumns.find((column) => column.field === field)?.headerName ?? field; @@ -242,7 +242,6 @@ export const TelemetryTableContextProvider = (props: PropsWithChildren IManualTelemetryTableRow[] = useCallback(() => { @@ -446,8 +445,8 @@ export const TelemetryTableContextProvider = (props: PropsWithChildren { - return telemetryDataContext.telemetryDataLoader.refresh(); - }, [ telemetryDataContext.telemetryDataLoader ]); + return telemetryDataContext.telemetryDataLoader.refresh([]); + }, [telemetryDataContext.telemetryDataLoader]); // True if the data grid contains at least 1 unsaved record const hasUnsavedChanges = modifiedRowIds.length > 0 || addedRowIds.length > 0; @@ -458,14 +457,13 @@ export const TelemetryTableContextProvider = (props: PropsWithChildren { try { - /** TODO await biohubApi.observation.insertUpdateObservationRecords( projectId, surveyId, rowsToSave as IObservationTableRow[] ); - */ + */ setModifiedRowIds([]); setAddedRowIds([]); @@ -529,14 +527,13 @@ export const TelemetryTableContextProvider = (props: PropsWithChildren - {props.children} - + {props.children} ); }; + +/* + + const api = useTelemetryApi(); + const api2 = useBiohubApi(); + useEffect(() => { + const fetchData = async () => { + console.log('__________________________'); + const critters = await api2.survey.getSurveyCritters(1, 1); + const deployments = await api2.survey.getDeploymentsInSurvey(1, 1); + + const telemetry = await api.getManualTelemetry(['9f0e9eb5-bd6b-417e-a81d-58ac53198e79']); + const vendor = await api.getVendorTelemetry(['194bbe78-1997-4867-ab0e-c2f1b42dddd0']); + const data: (IManualTelemetry | IVendorTelemetry)[] = [...telemetry, ...vendor]; + const critterDeployment: ICritterDeployment[] = []; + deployments.forEach((deployment) => { + const critter = critters.find((critter) => critter.critter_id === deployment.critter_id); + if (critter) { + critterDeployment.push({ critter, deployment }); + } + }); + + const finalData: IManualTelemetryTableRow[] = []; + data.forEach((item) => { + const found = critterDeployment.find((cd) => cd.deployment.deployment_id === item.deployment_id); + if (found) { + let id = ''; + if ('telemetry_manual_id' in item) { + id = item.telemetry_manual_id; + } + if ('telemetry_id' in item) { + id = item.telemetry_id; + } + finalData.push({ + id, + alias: String(found.critter.animal_id), + device_id: found.deployment.device_id, + latitude: item.latitude, + longitude: item.longitude, + date: moment(item.acquisition_date).format('YYYY-MM-DD'), + time: moment(item.acquisition_date).format('HH:mm:ss') + }); + } + }); + console.log('_______'); + console.log(finalData); + }; + fetchData(); + }, []); + +*/ diff --git a/app/src/features/surveys/telemetry/ManualTelemetryComponent.tsx b/app/src/features/surveys/telemetry/ManualTelemetryComponent.tsx index a15d194081..f710b8d7cd 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryComponent.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryComponent.tsx @@ -1,39 +1,200 @@ +import { mdiDotsVertical, mdiImport, mdiPlus, mdiTrashCanOutline } from '@mdi/js'; +import Icon from '@mdi/react'; +import { LoadingButton } from '@mui/lab'; +import { ListItemIcon } from '@mui/material'; import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Collapse from '@mui/material/Collapse'; import { grey } from '@mui/material/colors'; +import IconButton from '@mui/material/IconButton'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; import Paper from '@mui/material/Paper'; import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import DataGridValidationAlert from 'components/data-grid/DataGridValidationAlert'; +import FileUploadDialog from 'components/dialog/FileUploadDialog'; +import YesNoDialog from 'components/dialog/YesNoDialog'; +import { UploadFileStatus } from 'components/file-upload/FileUploadItem'; +import { TelemetryTableI18N } from 'constants/i18n'; +import { DialogContext, ISnackbarProps } from 'contexts/dialogContext'; +import { TelemetryTableContext } from 'contexts/telemetryTableContext'; import { useTelemetryApi } from 'hooks/useTelemetryApi'; -import { useEffect } from 'react'; +import { useContext, useState } from 'react'; +import { pluralize as p } from 'utils/Utils'; import ManualTelemetryTable from './ManualTelemetryTable'; const ManualTelemetryComponent = () => { - const api = useTelemetryApi(); - useEffect(() => { - const fetchData = async () => { - const temp = await api.getManualTelemetry(); - console.log(temp); - }; - fetchData(); - }, []); + const [showImportDialog, setShowImportDialog] = useState(false); + const [processingRecords, setProcessingRecords] = useState(false); + const [showConfirmRemoveAllDialog, setShowConfirmRemoveAllDialog] = useState(false); + const [anchorEl, setAnchorEl] = useState(null); + const dialogContext = useContext(DialogContext); + const telemetryTableContext = useContext(TelemetryTableContext); + const telemetryApi = useTelemetryApi(); + const { hasUnsavedChanges, validationModel, _muiDataGridApiRef } = telemetryTableContext; + + const showSnackBar = (textDialogProps?: Partial) => { + dialogContext.setSnackbar({ ...textDialogProps, open: true }); + }; + + const handleCloseMenu = () => { + setAnchorEl(null); + }; + + const handleFileImport = async (file: File) => { + telemetryApi.uploadCsvForImport(file).then((response) => { + setShowImportDialog(false); + setProcessingRecords(true); + telemetryApi + .processTelemetryCsvSubmission(response.submissionId) + .then(() => { + showSnackBar({ + snackbarMessage: ( + + Telemetry imported successfully. + + ) + }); + telemetryTableContext.refreshRecords().then(() => { + setProcessingRecords(false); + }); + }) + .catch(() => { + setProcessingRecords(false); + }); + }); + }; + + const numSelectedRows = telemetryTableContext.rowSelectionModel.length; return ( <> + setShowImportDialog(false)} + onUpload={handleFileImport} + FileUploadProps={{ + dropZoneProps: { maxNumFiles: 1, acceptedFileExtensions: '.csv' }, + status: UploadFileStatus.STAGED + }} + /> + { + setShowConfirmRemoveAllDialog(false); + telemetryTableContext.revertRecords(); + }} + onClose={() => setShowConfirmRemoveAllDialog(false)} + onNo={() => setShowConfirmRemoveAllDialog(false)} + /> - - + + + + Observations ‌ + + ({0}) + + + + + + + + + telemetryTableContext.saveRecords()} + disabled={telemetryTableContext.isSaving}> + Save + + + + + + ) => { + setAnchorEl(event.currentTarget); + }} + size="small" + disabled={numSelectedRows === 0} + aria-label="observation options"> + + + + { + telemetryTableContext.deleteSelectedRecords(); + handleCloseMenu(); + }} + disabled={telemetryTableContext.isSaving}> + + + + Delete {p(numSelectedRows, 'Observation')} + + + + + + + { - - - + + + + + diff --git a/app/src/hooks/useTelemetryApi.ts b/app/src/hooks/useTelemetryApi.ts index 1aa92a4512..d38050cb5e 100644 --- a/app/src/hooks/useTelemetryApi.ts +++ b/app/src/hooks/useTelemetryApi.ts @@ -1,3 +1,4 @@ +import { CancelTokenSource } from 'axios'; import { ConfigContext } from 'contexts/configContext'; import { useContext } from 'react'; import useAxios from './api/useAxios'; @@ -31,31 +32,70 @@ export interface IVendorTelemetry extends ICreateManualTelemetry { export const useTelemetryApi = () => { const config = useContext(ConfigContext); - const apiAxios = useAxios(config?.API_HOST); - const devices = useDeviceApi(apiAxios); + const axios = useAxios(config?.API_HOST); + const devices = useDeviceApi(axios); const getVendorTelemetry = async (ids: string[]): Promise => { - const { data } = await apiAxios.post('/api/telemetry/vendor/deployments', ids); + const { data } = await axios.post('/api/telemetry/vendor/deployments', ids); return data; }; const getManualTelemetry = async (ids: string[]): Promise => { - const { data } = await apiAxios.post('/api/telemetry/manual/deployments', ids); + const { data } = await axios.post('/api/telemetry/manual/deployments', ids); return data; }; const createManualTelemetry = async (postData: ICreateManualTelemetry[]): Promise => { - const { data } = await apiAxios.post('/api/telemetry/manual', postData); + const { data } = await axios.post('/api/telemetry/manual', postData); return data; }; const updateManualTelemetry = async (updateData: IManualTelemetry[]) => { - const { data } = await apiAxios.patch('/api/telemetry/manual', updateData); + const { data } = await axios.patch('/api/telemetry/manual', updateData); return data; }; const deleteManualTelemetry = async (ids: string[]) => { - const { data } = await apiAxios.post('/api/telemetry/manual/delete', ids); + const { data } = await axios.post('/api/telemetry/manual/delete', ids); + return data; + }; + + /** + * Uploads a telemetry CSV for import. + * + * @param {File} file + * @param {CancelTokenSource} [cancelTokenSource] + * @param {(progressEvent: ProgressEvent) => void} [onProgress] + * @return {*} {Promise<{ submissionId: number }>} + */ + const uploadCsvForImport = async ( + file: File, + cancelTokenSource?: CancelTokenSource, + onProgress?: (progressEvent: ProgressEvent) => void + ): Promise<{ submissionId: number }> => { + const formData = new FormData(); + + formData.append('media', file); + + const { data } = await axios.post<{ submissionId: number }>(`/api/telemetry/manual/upload`, formData, { + cancelToken: cancelTokenSource?.token, + onUploadProgress: onProgress + }); + + return data; + }; + + /** + * Begins processing an uploaded telemetry CSV for import + * + * @param {number} submissionId + * @return {*} + */ + const processTelemetryCsvSubmission = async (submissionId: number) => { + const { data } = await axios.post(`/api/telemetry/manual/process`, { + observation_submission_id: submissionId + }); + return data; }; @@ -65,7 +105,9 @@ export const useTelemetryApi = () => { createManualTelemetry, updateManualTelemetry, getVendorTelemetry, - deleteManualTelemetry + deleteManualTelemetry, + uploadCsvForImport, + processTelemetryCsvSubmission }; }; From ab29a85bf1820d52cb8c7965408ce569aab083b5 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 30 Nov 2023 15:00:05 -0700 Subject: [PATCH 48/88] added telemetry table columns --- .../telemetry/ManualTelemetryComponent.tsx | 2 +- .../telemetry/ManualTelemetryTable.tsx | 318 +++++++++++++----- 2 files changed, 242 insertions(+), 78 deletions(-) diff --git a/app/src/features/surveys/telemetry/ManualTelemetryComponent.tsx b/app/src/features/surveys/telemetry/ManualTelemetryComponent.tsx index f710b8d7cd..380d007d62 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryComponent.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryComponent.tsx @@ -204,7 +204,7 @@ const ManualTelemetryComponent = () => { background: grey[100] }}> - + diff --git a/app/src/features/surveys/telemetry/ManualTelemetryTable.tsx b/app/src/features/surveys/telemetry/ManualTelemetryTable.tsx index 339658d7c5..ebd2244d7f 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryTable.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryTable.tsx @@ -1,10 +1,34 @@ +import { mdiTrashCanOutline } from '@mdi/js'; +import Icon from '@mdi/react'; import { cyan, grey } from '@mui/material/colors'; -import { DataGrid, GridColDef, useGridApiRef } from '@mui/x-data-grid'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; +import { DataGrid, GridCellParams, GridColDef } from '@mui/x-data-grid'; +import TextFieldDataGrid from 'components/data-grid/TextFieldDataGrid'; +import TimePickerDataGrid from 'components/data-grid/TimePickerDataGrid'; import { GridTableRowSkeleton } from 'components/loading/SkeletonLoaders'; -import { IManualTelemetryTableRow } from 'contexts/telemetryTableContext'; -import { v4 as uuidv4 } from 'uuid'; +import { DATE_FORMAT } from 'constants/dateTimeFormats'; +import { IManualTelemetryTableRow, TelemetryTableContext } from 'contexts/telemetryTableContext'; +import moment from 'moment'; +import { useCallback, useContext } from 'react'; +import { getFormattedDate } from 'utils/Utils'; +interface IManualTelemetryTableProps { + isLoading: boolean; +} +const ManualTelemetryTable = (props: IManualTelemetryTableProps) => { + const telemetryTableContext = useContext(TelemetryTableContext); + const { _muiDataGridApiRef } = telemetryTableContext; + const hasError = useCallback( + (params: GridCellParams): boolean => { + return Boolean( + telemetryTableContext.validationModel[params.row.id]?.some((error: any) => { + return error.field === params.field; + }) + ); + }, + [telemetryTableContext.validationModel] + ); -const ManualTelemetryTable = () => { const tableColumns: GridColDef[] = [ { field: 'alias', @@ -17,13 +41,32 @@ const ManualTelemetryTable = () => { headerAlign: 'left', align: 'left', valueSetter: (params) => { - return { ...params.row }; - }, - renderCell: (params) => { - return <>; + return { ...params.row, alias: String(params.value) }; }, + renderCell: (params) => ( + + {params.value} + + ), renderEditCell: (params) => { - return <>; + const error: boolean = hasError(params); + + return ( + { + _muiDataGridApiRef?.current.setEditCellValue({ + id: params.id, + field: params.field, + value: event.target.value + }); + }, + error + }} + /> + ); } }, { @@ -37,13 +80,37 @@ const ManualTelemetryTable = () => { headerAlign: 'left', align: 'left', valueSetter: (params) => { - return { ...params.row }; - }, - renderCell: (params) => { - return <>; + return { ...params.row, device_id: Number(params.value) }; }, + renderCell: (params) => ( + + {params.value} + + ), renderEditCell: (params) => { - return <>; + const error: boolean = hasError(params); + + return ( + { + if (!/^\d{0,7}$/.test(event.target.value)) { + // If the value is not a number, return + return; + } + + _muiDataGridApiRef?.current.setEditCellValue({ + id: params.id, + field: params.field, + value: event.target.value + }); + }, + error + }} + /> + ); } }, { @@ -56,13 +123,44 @@ const ManualTelemetryTable = () => { headerAlign: 'left', align: 'left', valueSetter: (params) => { - return { ...params.row }; - }, - renderCell: (params) => { - return <>; + if (/^-?\d{1,3}(?:\.\d{0,12})?$/.test(params.value)) { + // If the value is a legal latitude value + // Valid entries: `-1`, `-1.1`, `-123.456789` `1`, `1.1, `123.456789` + return { ...params.row, latitude: Number(params.value) }; + } + + const value = parseFloat(params.value); + return { ...params.row, latitude: value }; }, + renderCell: (params) => ( + + {params.value} + + ), renderEditCell: (params) => { - return <>; + const error: boolean = hasError(params); + + return ( + { + if (!/^-?\d{0,3}(?:\.\d{0,12})?$/.test(event.target.value)) { + // If the value is not a subset of a legal latitude value, prevent the value from being applied + return; + } + + _muiDataGridApiRef?.current.setEditCellValue({ + id: params.id, + field: params.field, + value: event.target.value + }); + }, + error + }} + /> + ); } }, { @@ -75,13 +173,44 @@ const ManualTelemetryTable = () => { headerAlign: 'left', align: 'left', valueSetter: (params) => { - return { ...params.row }; - }, - renderCell: (params) => { - return <>; + if (/^-?\d{1,3}(?:\.\d{0,12})?$/.test(params.value)) { + // If the value is a legal longitude value + // Valid entries: `-1`, `-1.1`, `-123.456789` `1`, `1.1, `123.456789` + return { ...params.row, longitude: Number(params.value) }; + } + + const value = parseFloat(params.value); + return { ...params.row, longitude: value }; }, + renderCell: (params) => ( + + {params.value} + + ), renderEditCell: (params) => { - return <>; + const error: boolean = hasError(params); + + return ( + { + if (!/^-?\d{0,3}(?:\.\d{0,12})?$/.test(event.target.value)) { + // If the value is not a subset of a legal longitude value, prevent the value from being applied + return; + } + + _muiDataGridApiRef?.current.setEditCellValue({ + id: params.id, + field: params.field, + value: event.target.value + }); + }, + error + }} + /> + ); } }, { @@ -94,14 +223,35 @@ const ManualTelemetryTable = () => { disableColumnMenu: true, headerAlign: 'left', align: 'left', - valueSetter: (params) => { - return { ...params.row }; - }, - renderCell: (params) => { - return <>; - }, + valueGetter: (params) => (params.row.date ? moment(params.row.date).toDate() : null), + renderCell: (params) => ( + + {getFormattedDate(DATE_FORMAT.ShortDateFormatMonthFirst, params.value)} + + ), renderEditCell: (params) => { - return <>; + const error = hasError(params); + + return ( + { + const value = moment(event.target.value).toDate(); + _muiDataGridApiRef?.current.setEditCellValue({ + id: params.id, + field: params.field, + value + }); + }, + + error + }} + /> + ); } }, { @@ -115,14 +265,65 @@ const ManualTelemetryTable = () => { headerAlign: 'left', align: 'left', valueSetter: (params) => { - return { ...params.row }; + return { ...params.row, time: params.value }; + }, + valueParser: (value) => { + if (!value) { + return null; + } + + if (moment.isMoment(value)) { + return value.format('HH:mm:ss'); + } + + return moment(value, 'HH:mm:ss').format('HH:mm:ss'); }, renderCell: (params) => { - return <>; + if (!params.value) { + return null; + } + + return ( + + {params.value} + + ); }, renderEditCell: (params) => { - return <>; + const error = hasError(params); + + return ( + + ); } + }, + { + field: 'actions', + headerName: '', + type: 'actions', + width: 70, + disableColumnMenu: true, + resizable: false, + headerClassName: 'pinnedColumn', + cellClassName: 'pinnedColumn', + getActions: (params) => [ + telemetryTableContext.deleteRecords([params.row])} + disabled={telemetryTableContext.isSaving} + key={`actions[${params.id}].handleDeleteRow`}> + + + ] } ]; @@ -130,58 +331,21 @@ const ManualTelemetryTable = () => { {}} + rows={telemetryTableContext.rows} + onRowEditStart={(params) => telemetryTableContext.onRowEditStart(params.id)} onRowEditStop={(_params, event) => { event.defaultMuiPrevented = true; }} localeText={{ noRowsLabel: 'No Records' }} - onRowSelectionModelChange={() => {}} - rowSelectionModel={undefined} + onRowSelectionModelChange={telemetryTableContext.onRowSelectionModelChange} + rowSelectionModel={telemetryTableContext.rowSelectionModel} getRowHeight={() => 'auto'} slots={{ loadingOverlay: GridTableRowSkeleton From 8f696f07a42e7371d8ce29d3b61d2a919bd4661d Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 30 Nov 2023 15:03:06 -0700 Subject: [PATCH 49/88] fixed value setter --- .../observations/observations-table/ObservationsTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/features/surveys/observations/observations-table/ObservationsTable.tsx b/app/src/features/surveys/observations/observations-table/ObservationsTable.tsx index db037db43e..43472e4fe6 100644 --- a/app/src/features/surveys/observations/observations-table/ObservationsTable.tsx +++ b/app/src/features/surveys/observations/observations-table/ObservationsTable.tsx @@ -459,7 +459,7 @@ const ObservationsTable = (props: ISpeciesObservationTableProps) => { } const value = parseFloat(params.value); - return { ...params.row, longitude: isNaN(value) ? null : value }; + return { ...params.row, latitude: isNaN(value) ? null : value }; }, renderCell: (params) => ( From 0759eafc6f73bd5d105ccc6c810e5336b909288a Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 1 Dec 2023 09:46:38 -0700 Subject: [PATCH 50/88] adding drop down --- app/src/contexts/telemetryTableContext.tsx | 21 +++---- .../telemetry/ManualTelemetryTable.tsx | 63 ++++++++++++++++++- 2 files changed, 68 insertions(+), 16 deletions(-) diff --git a/app/src/contexts/telemetryTableContext.tsx b/app/src/contexts/telemetryTableContext.tsx index 005d4e8b6c..6ede172e15 100644 --- a/app/src/contexts/telemetryTableContext.tsx +++ b/app/src/contexts/telemetryTableContext.tsx @@ -4,16 +4,16 @@ import { GridApiCommunity } from '@mui/x-data-grid/internals'; import { TelemetryTableI18N } from 'constants/i18n'; import { DialogContext } from 'contexts/dialogContext'; import { APIError } from 'hooks/api/useAxios'; -import { useBiohubApi } from 'hooks/useBioHubApi'; +import { useTelemetryApi } from 'hooks/useTelemetryApi'; import moment from 'moment'; import { createContext, PropsWithChildren, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { RowValidationError, TableValidationModel } from '../components/data-grid/DataGridValidationAlert'; -import { SurveyContext } from './surveyContext'; import { TelemetryDataContext } from './telemetryDataContext'; export interface IManualTelemetryRecord { alias: string; + deployment_id: string; device_id: number; latitude: number; longitude: number; @@ -96,7 +96,7 @@ export type ITelemetryTableContext = { /** * The state of the validation model */ - validationModel: any; + validationModel: TelemetryTableValidationModel; /** * Reflects the total count of telemetry records for the survey */ @@ -132,8 +132,7 @@ export const TelemetryTableContext = createContext({ export const TelemetryTableContextProvider = (props: PropsWithChildren>) => { const _muiDataGridApiRef = useGridApiRef(); - const biohubApi = useBiohubApi(); - const surveyContext = useContext(SurveyContext); + const telemetryApi = useTelemetryApi(); const telemetryDataContext = useContext(TelemetryDataContext); const dialogContext = useContext(DialogContext); @@ -242,15 +241,9 @@ export const TelemetryTableContextProvider = (props: PropsWithChildren { const telemetryTableContext = useContext(TelemetryTableContext); + const surveyContext = useContext(SurveyContext); + + useEffect(() => { + surveyContext.deploymentDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); + surveyContext.critterDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); + }, []); + + const critterDeployments: 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 }); + } + }); + console.log(data); + return data; + }, [surveyContext.critterDataLoader.data, surveyContext.deploymentDataLoader.data]); const { _muiDataGridApiRef } = telemetryTableContext; const hasError = useCallback( (params: GridCellParams): boolean => { return Boolean( - telemetryTableContext.validationModel[params.row.id]?.some((error: any) => { + telemetryTableContext.validationModel[params.row.id]?.some((error) => { return error.field === params.field; }) ); @@ -30,6 +54,41 @@ const ManualTelemetryTable = (props: IManualTelemetryTableProps) => { ); const tableColumns: GridColDef[] = [ + { + field: 'deployment_id', + headerName: 'Deployment', + editable: true, + flex: 1, + minWidth: 250, + type: 'string', + disableColumnMenu: true, + headerAlign: 'left', + align: 'left', + renderCell: (params) => { + return ( + + dataGridProps={params} + options={critterDeployments.map((item) => ({ + label: `${item.critter.animal_id}: ${item.deployment.device_id}`, + value: item.deployment.deployment_id + }))} + error={hasError(params)} + /> + ); + }, + renderEditCell: (params) => { + return ( + + dataGridProps={params} + options={critterDeployments.map((item) => ({ + label: `${item.critter.animal_id}: ${item.deployment.device_id}`, + value: item.deployment.deployment_id + }))} + error={hasError(params)} + /> + ); + } + }, { field: 'alias', headerName: 'Alias', From bfb4f2400739a678a7fad35b198955e6cd90808c Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 1 Dec 2023 11:44:39 -0700 Subject: [PATCH 51/88] save is wired up --- app/src/contexts/telemetryTableContext.tsx | 33 +++---- .../telemetry/ManualTelemetryTable.tsx | 89 +------------------ 2 files changed, 20 insertions(+), 102 deletions(-) diff --git a/app/src/contexts/telemetryTableContext.tsx b/app/src/contexts/telemetryTableContext.tsx index 6ede172e15..c00c4b431e 100644 --- a/app/src/contexts/telemetryTableContext.tsx +++ b/app/src/contexts/telemetryTableContext.tsx @@ -4,7 +4,7 @@ import { GridApiCommunity } from '@mui/x-data-grid/internals'; import { TelemetryTableI18N } from 'constants/i18n'; import { DialogContext } from 'contexts/dialogContext'; import { APIError } from 'hooks/api/useAxios'; -import { useTelemetryApi } from 'hooks/useTelemetryApi'; +import { ICreateManualTelemetry, useTelemetryApi } from 'hooks/useTelemetryApi'; import moment from 'moment'; import { createContext, PropsWithChildren, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; @@ -12,9 +12,7 @@ import { RowValidationError, TableValidationModel } from '../components/data-gri import { TelemetryDataContext } from './telemetryDataContext'; export interface IManualTelemetryRecord { - alias: string; deployment_id: string; - device_id: number; latitude: number; longitude: number; date: string; @@ -182,12 +180,11 @@ export const TelemetryTableContextProvider = (props: PropsWithChildren { @@ -361,8 +358,7 @@ export const TelemetryTableContextProvider = (props: PropsWithChildren { try { - /** TODO - await biohubApi.observation.insertUpdateObservationRecords( - projectId, - surveyId, - rowsToSave as IObservationTableRow[] - ); - */ - + const createData: ICreateManualTelemetry[] = (rowsToSave as IManualTelemetryTableRow[]).map((item) => { + return { + deployment_id: String(item.deployment_id), + latitude: Number(item.latitude), + longitude: Number(item.longitude), + acquisition_date: moment(moment(item.date).format('YYYY-MM-DD') + ' ' + item.time).format( + 'YYYY-MM-DD HH:mm:ss' + ) + }; + }); + console.log(createData); + const response = await telemetryApi.createManualTelemetry(createData); + console.log(response); setModifiedRowIds([]); setAddedRowIds([]); diff --git a/app/src/features/surveys/telemetry/ManualTelemetryTable.tsx b/app/src/features/surveys/telemetry/ManualTelemetryTable.tsx index bb6c4f70ad..c248d04569 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryTable.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryTable.tsx @@ -4,6 +4,7 @@ import { cyan, grey } from '@mui/material/colors'; import IconButton from '@mui/material/IconButton'; import Typography from '@mui/material/Typography'; import { DataGrid, GridCellParams, GridColDef } from '@mui/x-data-grid'; +import AutocompleteDataGridEditCell from 'components/data-grid/autocomplete/AutocompleteDataGridEditCell'; import AutocompleteDataGridViewCell from 'components/data-grid/autocomplete/AutocompleteDataGridViewCell'; import TextFieldDataGrid from 'components/data-grid/TextFieldDataGrid'; import TimePickerDataGrid from 'components/data-grid/TimePickerDataGrid'; @@ -38,7 +39,6 @@ const ManualTelemetryTable = (props: IManualTelemetryTableProps) => { data.push({ critter, deployment }); } }); - console.log(data); return data; }, [surveyContext.critterDataLoader.data, surveyContext.deploymentDataLoader.data]); const { _muiDataGridApiRef } = telemetryTableContext; @@ -60,10 +60,10 @@ const ManualTelemetryTable = (props: IManualTelemetryTableProps) => { editable: true, flex: 1, minWidth: 250, - type: 'string', disableColumnMenu: true, headerAlign: 'left', align: 'left', + type: 'string', renderCell: (params) => { return ( @@ -78,7 +78,7 @@ const ManualTelemetryTable = (props: IManualTelemetryTableProps) => { }, renderEditCell: (params) => { return ( - + dataGridProps={params} options={critterDeployments.map((item) => ({ label: `${item.critter.animal_id}: ${item.deployment.device_id}`, @@ -89,89 +89,6 @@ const ManualTelemetryTable = (props: IManualTelemetryTableProps) => { ); } }, - { - field: 'alias', - headerName: 'Alias', - editable: true, - flex: 1, - minWidth: 250, - type: 'string', - disableColumnMenu: true, - headerAlign: 'left', - align: 'left', - valueSetter: (params) => { - return { ...params.row, alias: String(params.value) }; - }, - renderCell: (params) => ( - - {params.value} - - ), - renderEditCell: (params) => { - const error: boolean = hasError(params); - - return ( - { - _muiDataGridApiRef?.current.setEditCellValue({ - id: params.id, - field: params.field, - value: event.target.value - }); - }, - error - }} - /> - ); - } - }, - { - field: 'device_id', - headerName: 'Device ID', - editable: true, - flex: 1, - minWidth: 250, - type: 'number', - disableColumnMenu: true, - headerAlign: 'left', - align: 'left', - valueSetter: (params) => { - return { ...params.row, device_id: Number(params.value) }; - }, - renderCell: (params) => ( - - {params.value} - - ), - renderEditCell: (params) => { - const error: boolean = hasError(params); - - return ( - { - if (!/^\d{0,7}$/.test(event.target.value)) { - // If the value is not a number, return - return; - } - - _muiDataGridApiRef?.current.setEditCellValue({ - id: params.id, - field: params.field, - value: event.target.value - }); - }, - error - }} - /> - ); - } - }, { field: 'latitude', headerName: 'Latitude', From b07f1a2d43b3da5da5368081e59e11ca373e83af Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 1 Dec 2023 14:24:31 -0700 Subject: [PATCH 52/88] creation and loading wired up --- app/src/contexts/telemetryTableContext.tsx | 121 ++++++++---------- .../surveys/telemetry/ManualTelemetryPage.tsx | 4 +- 2 files changed, 53 insertions(+), 72 deletions(-) diff --git a/app/src/contexts/telemetryTableContext.tsx b/app/src/contexts/telemetryTableContext.tsx index c00c4b431e..30e3889d96 100644 --- a/app/src/contexts/telemetryTableContext.tsx +++ b/app/src/contexts/telemetryTableContext.tsx @@ -6,7 +6,7 @@ import { DialogContext } from 'contexts/dialogContext'; import { APIError } from 'hooks/api/useAxios'; import { ICreateManualTelemetry, useTelemetryApi } from 'hooks/useTelemetryApi'; import moment from 'moment'; -import { createContext, PropsWithChildren, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { RowValidationError, TableValidationModel } from '../components/data-grid/DataGridValidationAlert'; import { TelemetryDataContext } from './telemetryDataContext'; @@ -127,7 +127,14 @@ export const TelemetryTableContext = createContext({ setRecordCount: () => undefined }); -export const TelemetryTableContextProvider = (props: PropsWithChildren>) => { +interface ITelemetryTableContextProviderProps { + deployment_ids: string[]; + children?: React.ReactNode; +} + +export const TelemetryTableContextProvider: React.FC = (props) => { + const { children, deployment_ids } = props; + const _muiDataGridApiRef = useGridApiRef(); const telemetryApi = useTelemetryApi(); @@ -434,8 +441,9 @@ export const TelemetryTableContextProvider = (props: PropsWithChildren { - return telemetryDataContext.telemetryDataLoader.refresh([]); - }, [telemetryDataContext.telemetryDataLoader]); + telemetryDataContext.telemetryDataLoader.refresh(deployment_ids); + telemetryDataContext.vendorTelemetryDataLoader.refresh(deployment_ids); + }, [telemetryDataContext.telemetryDataLoader, telemetryDataContext.vendorTelemetryDataLoader]); // True if the data grid contains at least 1 unsaved record const hasUnsavedChanges = modifiedRowIds.length > 0 || addedRowIds.length > 0; @@ -456,9 +464,8 @@ export const TelemetryTableContextProvider = (props: PropsWithChildren { + refreshRecords(); + }, [deployment_ids]); + /** * Runs when telemetry context data has changed. This does not occur when records are * deleted; Only on initial page load, and whenever records are saved. */ useEffect(() => { - if (!telemetryDataContext.telemetryDataLoader.hasLoaded) { + if ( + !telemetryDataContext.telemetryDataLoader.hasLoaded || + !telemetryDataContext.vendorTelemetryDataLoader.hasLoaded + ) { // Existing telemetry records have not yet loaded return; } - if (!telemetryDataContext.telemetryDataLoader.data) { + if (!telemetryDataContext.telemetryDataLoader.data || !telemetryDataContext.vendorTelemetryDataLoader.data) { // Existing telemetry data doesn't exist return; } // Collect rows from the telemetry data loader + const telemetry = telemetryDataContext.telemetryDataLoader.data; + const vendorTelemetry = telemetryDataContext.vendorTelemetryDataLoader.data; + const totalTelemetry = [...telemetry, ...vendorTelemetry]; + + const rows: IManualTelemetryTableRow[] = totalTelemetry.map((item) => { + let id = ''; + if ('telemetry_manual_id' in item) { + id = item.telemetry_manual_id; + } - /** TODO - const rows: IManualTelemetryTableRow[] = telemetryDataContext.telemetryDataLoader.data.surveyObservations.map( - (row: IObservationRecord) => ({ ...row, id: String(row.survey_observation_id) }) - ); - */ + if ('telemetry_id' in item) { + id = item.telemetry_id; + } - const rows: IManualTelemetryTableRow[] = []; // TODO placeholder; see above + return { + id, + deployment_id: item.deployment_id, + latitude: item.latitude, + longitude: item.longitude, + date: moment(item.acquisition_date).format('YYYY-MM-DD'), + time: moment(item.acquisition_date).format('HH:mm:ss') + }; + }); // Set initial rows for the table context setRows(rows); // Set initial record count // setRecordCount(observationsContext.observationsDataLoader.data.supplementaryObservationData.observationCount); // TODO - }, [telemetryDataContext.telemetryDataLoader.data, telemetryDataContext.telemetryDataLoader.hasLoaded]); + }, [ + telemetryDataContext.telemetryDataLoader.data, + telemetryDataContext.telemetryDataLoader.hasLoaded, + telemetryDataContext.vendorTelemetryDataLoader.data, + telemetryDataContext.vendorTelemetryDataLoader.hasLoaded + ]); /** * Runs when row records are being saved and transitioned from Edit mode to View mode. @@ -612,58 +646,5 @@ export const TelemetryTableContextProvider = (props: PropsWithChildren{props.children} - ); + return {children}; }; - -/* - - const api = useTelemetryApi(); - const api2 = useBiohubApi(); - useEffect(() => { - const fetchData = async () => { - console.log('__________________________'); - const critters = await api2.survey.getSurveyCritters(1, 1); - const deployments = await api2.survey.getDeploymentsInSurvey(1, 1); - - const telemetry = await api.getManualTelemetry(['9f0e9eb5-bd6b-417e-a81d-58ac53198e79']); - const vendor = await api.getVendorTelemetry(['194bbe78-1997-4867-ab0e-c2f1b42dddd0']); - const data: (IManualTelemetry | IVendorTelemetry)[] = [...telemetry, ...vendor]; - const critterDeployment: ICritterDeployment[] = []; - deployments.forEach((deployment) => { - const critter = critters.find((critter) => critter.critter_id === deployment.critter_id); - if (critter) { - critterDeployment.push({ critter, deployment }); - } - }); - - const finalData: IManualTelemetryTableRow[] = []; - data.forEach((item) => { - const found = critterDeployment.find((cd) => cd.deployment.deployment_id === item.deployment_id); - if (found) { - let id = ''; - if ('telemetry_manual_id' in item) { - id = item.telemetry_manual_id; - } - if ('telemetry_id' in item) { - id = item.telemetry_id; - } - finalData.push({ - id, - alias: String(found.critter.animal_id), - device_id: found.deployment.device_id, - latitude: item.latitude, - longitude: item.longitude, - date: moment(item.acquisition_date).format('YYYY-MM-DD'), - time: moment(item.acquisition_date).format('HH:mm:ss') - }); - } - }); - console.log('_______'); - console.log(finalData); - }; - fetchData(); - }, []); - -*/ diff --git a/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx b/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx index e950daa13c..de05e194cf 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx @@ -12,7 +12,7 @@ import ManualTelemetryList from './ManualTelemetryList'; const ManualTelemetryPage = () => { const surveyContext = useContext(SurveyContext); - + const deploymentIds = surveyContext.deploymentDataLoader.data?.map((item) => item.deployment_id); if (!surveyContext.surveyDataLoader.data) { return ; } @@ -42,7 +42,7 @@ const ManualTelemetryPage = () => { - + From 3c9027935174f1fec691de70b6f984bddf573c34 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 1 Dec 2023 16:08:41 -0700 Subject: [PATCH 53/88] added update flow to table buttons --- app/src/contexts/telemetryTableContext.tsx | 49 +++++++++++++++++----- app/src/hooks/useTelemetryApi.ts | 8 +++- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/app/src/contexts/telemetryTableContext.tsx b/app/src/contexts/telemetryTableContext.tsx index 30e3889d96..8a7eaf6ad8 100644 --- a/app/src/contexts/telemetryTableContext.tsx +++ b/app/src/contexts/telemetryTableContext.tsx @@ -4,7 +4,7 @@ import { GridApiCommunity } from '@mui/x-data-grid/internals'; import { TelemetryTableI18N } from 'constants/i18n'; import { DialogContext } from 'contexts/dialogContext'; import { APIError } from 'hooks/api/useAxios'; -import { ICreateManualTelemetry, useTelemetryApi } from 'hooks/useTelemetryApi'; +import { ICreateManualTelemetry, IUpdateManualTelemetry, useTelemetryApi } from 'hooks/useTelemetryApi'; import moment from 'moment'; import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; @@ -454,17 +454,44 @@ export const TelemetryTableContextProvider: React.FC { try { - const createData: ICreateManualTelemetry[] = (rowsToSave as IManualTelemetryTableRow[]).map((item) => { - return { - deployment_id: String(item.deployment_id), - latitude: Number(item.latitude), - longitude: Number(item.longitude), - acquisition_date: moment(moment(item.date).format('YYYY-MM-DD') + ' ' + item.time).format( - 'YYYY-MM-DD HH:mm:ss' - ) - }; + const createData: ICreateManualTelemetry[] = []; + const updateData: IUpdateManualTelemetry[] = []; + + // loop through records and decide based on initial data loaded if a record should be created or updated + (rowsToSave as IManualTelemetryTableRow[]).forEach((item) => { + const found = telemetryDataContext.telemetryDataLoader.data?.filter( + (search) => search.telemetry_manual_id === item.id + ); + if (found) { + // existing ID found, update record + updateData.push({ + telemetry_manual_id: String(item.id), + latitude: Number(item.latitude), + longitude: Number(item.longitude), + acquisition_date: moment(moment(item.date).format('YYYY-MM-DD') + ' ' + item.time).format( + 'YYYY-MM-DD HH:mm:ss' + ) + }); + } else { + // nothing found, create a new record + createData.push({ + deployment_id: String(item.deployment_id), + latitude: Number(item.latitude), + longitude: Number(item.longitude), + acquisition_date: moment(moment(item.date).format('YYYY-MM-DD') + ' ' + item.time).format( + 'YYYY-MM-DD HH:mm:ss' + ) + }); + } }); - await telemetryApi.createManualTelemetry(createData); + + if (createData.length) { + await telemetryApi.createManualTelemetry(createData); + } + + if (updateData.length) { + await telemetryApi.updateManualTelemetry(updateData); + } setModifiedRowIds([]); setAddedRowIds([]); diff --git a/app/src/hooks/useTelemetryApi.ts b/app/src/hooks/useTelemetryApi.ts index d38050cb5e..c4b48e9740 100644 --- a/app/src/hooks/useTelemetryApi.ts +++ b/app/src/hooks/useTelemetryApi.ts @@ -15,6 +15,12 @@ export interface ICritterDeploymentResponse { taxon: string; } +export interface IUpdateManualTelemetry { + telemetry_manual_id: string; + latitude: number; + longitude: number; + acquisition_date: string; +} export interface ICreateManualTelemetry { deployment_id: string; latitude: number; @@ -50,7 +56,7 @@ export const useTelemetryApi = () => { return data; }; - const updateManualTelemetry = async (updateData: IManualTelemetry[]) => { + const updateManualTelemetry = async (updateData: IUpdateManualTelemetry[]) => { const { data } = await axios.patch('/api/telemetry/manual', updateData); return data; }; From 777719364a4ba9eaeabd9749fdbf238a17821763 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 1 Dec 2023 16:32:04 -0700 Subject: [PATCH 54/88] added some stuff --- app/src/features/surveys/telemetry/ManualTelemetryPage.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx b/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx index de05e194cf..4419a67bfc 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx @@ -5,14 +5,17 @@ import Paper from '@mui/material/Paper'; import { SurveyContext } from 'contexts/surveyContext'; import { TelemetryDataContextProvider } from 'contexts/telemetryDataContext'; import { TelemetryTableContextProvider } from 'contexts/telemetryTableContext'; -import { useContext } from 'react'; +import { useContext, useMemo } from 'react'; import SurveyObservationHeader from '../observations/SurveyObservationHeader'; import ManualTelemetryComponent from './ManualTelemetryComponent'; import ManualTelemetryList from './ManualTelemetryList'; const ManualTelemetryPage = () => { const surveyContext = useContext(SurveyContext); - const deploymentIds = surveyContext.deploymentDataLoader.data?.map((item) => item.deployment_id); + const deploymentIds = useMemo(() => { + return surveyContext.deploymentDataLoader.data?.map((item) => item.deployment_id); + }, [surveyContext.deploymentDataLoader.data]); + if (!surveyContext.surveyDataLoader.data) { return ; } From d3f6e960f53db2610e04c26c247e97e91aa45b05 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Mon, 4 Dec 2023 12:29:48 -0800 Subject: [PATCH 55/88] fixed spelling --- .../observations-table/ObservationComponent.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/features/surveys/observations/observations-table/ObservationComponent.tsx b/app/src/features/surveys/observations/observations-table/ObservationComponent.tsx index 28abb7c73b..6d4e9c6790 100644 --- a/app/src/features/surveys/observations/observations-table/ObservationComponent.tsx +++ b/app/src/features/surveys/observations/observations-table/ObservationComponent.tsx @@ -26,7 +26,7 @@ import { useContext, useState } from 'react'; import { pluralize as p } from 'utils/Utils'; const ObservationComponent = () => { - const [showImportDiaolog, setShowImportDiaolog] = useState(false); + const [showImportDialog, setShowImportDialog] = useState(false); const [processingRecords, setProcessingRecords] = useState(false); const [menuAnchorEl, setMenuAnchorEl] = useState(null); const [showConfirmRemoveAllDialog, setShowConfirmRemoveAllDialog] = useState(false); @@ -47,7 +47,7 @@ const ObservationComponent = () => { const handleImportObservations = async (file: File) => { return biohubApi.observation.uploadCsvForImport(projectId, surveyId, file).then((response) => { - setShowImportDiaolog(false); + setShowImportDialog(false); setProcessingRecords(true); biohubApi.observation .processCsvSubmission(projectId, surveyId, response.submissionId) @@ -75,9 +75,9 @@ const ObservationComponent = () => { return ( <> setShowImportDiaolog(false)} + onClose={() => setShowImportDialog(false)} onUpload={handleImportObservations} FileUploadProps={{ dropZoneProps: { maxNumFiles: 1, acceptedFileExtensions: '.csv' }, @@ -126,7 +126,7 @@ const ObservationComponent = () => { variant="contained" color="primary" startIcon={} - onClick={() => setShowImportDiaolog(true)}> + onClick={() => setShowImportDialog(true)}> Import + + - - - + - Deployments ‌ - - ({critterDeployments?.length ?? 0}) + + Deployments ‌ + + ({critterDeployments?.length ?? 0}) + - - - - - {/* Display list of skeleton components while waiting for a response */} - - - {critterDeployments?.map((item) => ( - { - handleMenuOpen(event, id); - }} - /> - ))} + + + + {/* Display list of skeleton components while waiting for a response */} + + + {critterDeployments?.map((item) => ( + { + handleMenuOpen(event, id); + }} + /> + ))} + - - - )} + + ); + }} ); From 70b5d09a0bcaa3de32025ba87c372faac3518193 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 5 Dec 2023 11:15:57 -0800 Subject: [PATCH 70/88] updated api to use new endpoint --- app/src/contexts/telemetryDataContext.tsx | 14 +++----- app/src/contexts/telemetryTableContext.tsx | 33 +++++-------------- .../surveys/telemetry/ManualTelemetryList.tsx | 1 - app/src/hooks/useTelemetryApi.ts | 17 ++++++++++ 4 files changed, 30 insertions(+), 35 deletions(-) diff --git a/app/src/contexts/telemetryDataContext.tsx b/app/src/contexts/telemetryDataContext.tsx index 92f9017613..57d758662f 100644 --- a/app/src/contexts/telemetryDataContext.tsx +++ b/app/src/contexts/telemetryDataContext.tsx @@ -1,29 +1,25 @@ import useDataLoader, { DataLoader } from 'hooks/useDataLoader'; -import { IManualTelemetry, IVendorTelemetry, useTelemetryApi } from 'hooks/useTelemetryApi'; +import { ITelemetry, useTelemetryApi } from 'hooks/useTelemetryApi'; import { createContext, PropsWithChildren } from 'react'; export type ITelemetryDataContext = { - telemetryDataLoader: DataLoader<[ids: string[]], IManualTelemetry[], unknown>; - vendorTelemetryDataLoader: DataLoader<[ids: string[]], IVendorTelemetry[], unknown>; + telemetryDataLoader: DataLoader<[ids: string[]], ITelemetry[], unknown>; }; export const TelemetryDataContext = createContext({ - telemetryDataLoader: {} as DataLoader<[ids: string[]], IManualTelemetry[], unknown>, - vendorTelemetryDataLoader: {} as DataLoader<[ids: string[]], IVendorTelemetry[], unknown> + telemetryDataLoader: {} as DataLoader<[ids: string[]], ITelemetry[], unknown> }); export const TelemetryDataContextProvider = (props: PropsWithChildren>) => { // const { projectId, surveyId } = useContext(SurveyContext); const telemetryApi = useTelemetryApi(); - const telemetryDataLoader = useDataLoader(telemetryApi.getManualTelemetry); - const vendorTelemetryDataLoader = useDataLoader(telemetryApi.getVendorTelemetry); + const telemetryDataLoader = useDataLoader(telemetryApi.getAllTelemetry); // telemetryDataLoader.load(); const telemetryDataContext: ITelemetryDataContext = { - telemetryDataLoader, - vendorTelemetryDataLoader + telemetryDataLoader }; return {props.children}; diff --git a/app/src/contexts/telemetryTableContext.tsx b/app/src/contexts/telemetryTableContext.tsx index 7913fa816c..7449f8e1c7 100644 --- a/app/src/contexts/telemetryTableContext.tsx +++ b/app/src/contexts/telemetryTableContext.tsx @@ -17,6 +17,7 @@ export interface IManualTelemetryRecord { longitude: number; date: string; time: string; + telemetry_type: string; } export interface IManualTelemetryTableRow extends Partial { @@ -444,11 +445,7 @@ export const TelemetryTableContextProvider: React.FC 0 || addedRowIds.length > 0; @@ -542,7 +539,6 @@ export const TelemetryTableContextProvider: React.FC { - if ( - telemetryDataContext.telemetryDataLoader.isLoading || - telemetryDataContext.vendorTelemetryDataLoader.isLoading - ) { + if (telemetryDataContext.telemetryDataLoader.isLoading) { // Existing telemetry records have not yet loaded return; } // Collect rows from the telemetry data loader - const telemetry = telemetryDataContext.telemetryDataLoader.data || []; - const vendorTelemetry = telemetryDataContext.vendorTelemetryDataLoader.data || []; - const totalTelemetry = [...telemetry, ...vendorTelemetry]; + const totalTelemetry = telemetryDataContext.telemetryDataLoader.data || []; const rows: IManualTelemetryTableRow[] = totalTelemetry.map((item) => { - let id = ''; - if ('telemetry_manual_id' in item) { - id = item.telemetry_manual_id; - } - - if ('telemetry_id' in item) { - id = item.telemetry_id; - } - return { - id, + id: item.id, deployment_id: item.deployment_id, latitude: item.latitude, longitude: item.longitude, date: moment(item.acquisition_date).format('YYYY-MM-DD'), - time: moment(item.acquisition_date).format('HH:mm:ss') + time: moment(item.acquisition_date).format('HH:mm:ss'), + telemetry_type: item.telemetry_type }; }); @@ -589,7 +572,7 @@ export const TelemetryTableContextProvider: React.FC { setCritterId(''); }}> {(formikProps) => { - console.log(formikProps.values); return ( <> diff --git a/app/src/hooks/useTelemetryApi.ts b/app/src/hooks/useTelemetryApi.ts index 174c064d55..e0467a3b9c 100644 --- a/app/src/hooks/useTelemetryApi.ts +++ b/app/src/hooks/useTelemetryApi.ts @@ -36,11 +36,27 @@ export interface IVendorTelemetry extends ICreateManualTelemetry { telemetry_id: string; } +export interface ITelemetry { + id: string; + deployment_id: string; + telemetry_manual_id: string; + telemetry_id: number | null; + latitude: number; + longitude: number; + acquisition_date: string; + telemetry_type: string; +} + export const useTelemetryApi = () => { const config = useContext(ConfigContext); const axios = useAxios(config?.API_HOST); const devices = useDeviceApi(axios); + const getAllTelemetry = async (ids: string[]): Promise => { + const { data } = await axios.post('/api/telemetry/deployments', ids); + return data; + }; + const getVendorTelemetry = async (ids: string[]): Promise => { const { data } = await axios.post('/api/telemetry/vendor/deployments', ids); return data; @@ -115,6 +131,7 @@ export const useTelemetryApi = () => { return { devices, + getAllTelemetry, getManualTelemetry, createManualTelemetry, updateManualTelemetry, From 89d52b2d077eef21688429a97a457bd57820cc5f Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 5 Dec 2023 11:43:41 -0800 Subject: [PATCH 71/88] resolving code smells --- .../components/loading/SkeletonLoaders.tsx | 1 + app/src/contexts/telemetryDataContext.tsx | 14 +-- app/src/contexts/telemetryTableContext.tsx | 90 +++++++++---------- .../sampling-sites/SamplingSiteList.tsx | 4 +- 4 files changed, 55 insertions(+), 54 deletions(-) diff --git a/app/src/components/loading/SkeletonLoaders.tsx b/app/src/components/loading/SkeletonLoaders.tsx index 31a5999364..a07a982104 100644 --- a/app/src/components/loading/SkeletonLoaders.tsx +++ b/app/src/components/loading/SkeletonLoaders.tsx @@ -56,6 +56,7 @@ const GridTableRowSkeleton = (props: IMultipleSkeletonProps) => { .fill(null) .map(() => ( ; @@ -11,16 +11,16 @@ export const TelemetryDataContext = createContext({ }); export const TelemetryDataContextProvider = (props: PropsWithChildren>) => { - // const { projectId, surveyId } = useContext(SurveyContext); const telemetryApi = useTelemetryApi(); const telemetryDataLoader = useDataLoader(telemetryApi.getAllTelemetry); - // telemetryDataLoader.load(); - - const telemetryDataContext: ITelemetryDataContext = { - telemetryDataLoader - }; + const telemetryDataContext: ITelemetryDataContext = useMemo( + () => ({ + telemetryDataLoader + }), + [] + ); return {props.children}; }; diff --git a/app/src/contexts/telemetryTableContext.tsx b/app/src/contexts/telemetryTableContext.tsx index 7449f8e1c7..f32973f039 100644 --- a/app/src/contexts/telemetryTableContext.tsx +++ b/app/src/contexts/telemetryTableContext.tsx @@ -152,9 +152,9 @@ export const TelemetryTableContextProvider: React.FC([]); // True if the rows are in the process of transitioning from edit to view mode - const [_isStoppingEdit, _setIsStoppingEdit] = useState(false); + const [isStoppingEdit, setIsStoppingEdit] = useState(false); // True if the records are in the process of being saved to the server - const [_isSaving, _setIsSaving] = useState(false); + const [isCurrentltSaving, setIsCurrentlySaving] = useState(false); // Stores the current count of telemetry records for this survey const [recordCount, setRecordCount] = useState(0); // Stores the current validation state of the table @@ -247,8 +247,6 @@ export const TelemetryTableContextProvider: React.FC IManualTelemetryTableRow[] = useCallback(() => { @@ -337,7 +335,9 @@ export const TelemetryTableContextProvider: React.FC _commitDeleteRecords(telemetryRecords), + onYes: () => { + _commitDeleteRecords(telemetryRecords); + }, onClose: () => dialogContext.setYesNoDialog({ open: false }), onNo: () => dialogContext.setYesNoDialog({ open: false }) }); @@ -386,7 +386,7 @@ export const TelemetryTableContextProvider: React.FC { - if (_isStoppingEdit) { + if (isStoppingEdit) { // Stop edit mode already in progress return; } @@ -398,7 +398,7 @@ export const TelemetryTableContextProvider: React.FC { - // TODO: this will need to trim out any vendor specific data before hand - // TODO: so the array coming back from the api endpoint will then have to have a flag just saying if it's vendor or not - const found = telemetryDataContext.telemetryDataLoader.data?.find( - (search) => search.telemetry_manual_id === item.id - ); - if (found) { - // existing ID found, update record - updateData.push({ - telemetry_manual_id: String(item.id), - latitude: Number(item.latitude), - longitude: Number(item.longitude), - acquisition_date: moment(moment(item.date).format('YYYY-MM-DD') + ' ' + item.time).format( - 'YYYY-MM-DD HH:mm:ss' - ) - }); - } else { - // nothing found, create a new record - createData.push({ - deployment_id: String(item.deployment_id), - latitude: Number(item.latitude), - longitude: Number(item.longitude), - acquisition_date: moment(moment(item.date).format('YYYY-MM-DD') + ' ' + item.time).format( - 'YYYY-MM-DD HH:mm:ss' - ) - }); + if (item.telemetry_type === 'MANUAL') { + const found = telemetryDataContext.telemetryDataLoader.data?.find( + (search) => search.telemetry_manual_id === item.id + ); + if (found) { + // existing ID found, update record + updateData.push({ + telemetry_manual_id: String(item.id), + latitude: Number(item.latitude), + longitude: Number(item.longitude), + acquisition_date: moment(moment(item.date).format('YYYY-MM-DD') + ' ' + item.time).format( + 'YYYY-MM-DD HH:mm:ss' + ) + }); + } else { + // nothing found, create a new record + createData.push({ + deployment_id: String(item.deployment_id), + latitude: Number(item.latitude), + longitude: Number(item.longitude), + acquisition_date: moment(moment(item.date).format('YYYY-MM-DD') + ' ' + item.time).format( + 'YYYY-MM-DD HH:mm:ss' + ) + }); + } } }); @@ -521,10 +521,10 @@ export const TelemetryTableContextProvider: React.FC { @@ -532,8 +532,8 @@ export const TelemetryTableContextProvider: React.FC { - return _isSaving || _isStoppingEdit; - }, [_isSaving, _isStoppingEdit]); + return isCurrentltSaving || isStoppingEdit; + }, [isCurrentltSaving, isStoppingEdit]); useEffect(() => { // Begin fetching telemetry once we have deployments ids @@ -553,7 +553,7 @@ export const TelemetryTableContextProvider: React.FC { return { @@ -583,7 +583,7 @@ export const TelemetryTableContextProvider: React.FC value); _saveRecords(rowValues); - }, [_muiDataGridApiRef, _saveRecords, _isSaving, _isStoppingEdit, modifiedRowIds]); + }, [_muiDataGridApiRef, _saveRecords, isCurrentltSaving, isStoppingEdit, modifiedRowIds]); const telemetryTableContext: ITelemetryTableContext = useMemo( () => ({ diff --git a/app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx b/app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx index 23ea285ba3..cd16c9d55e 100644 --- a/app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx +++ b/app/src/features/surveys/observations/sampling-sites/SamplingSiteList.tsx @@ -327,9 +327,9 @@ const SamplingSiteList = () => { - {`${samplePeriod.start_date} ${samplePeriod.start_time || ''} - ${ + {`${samplePeriod.start_date} ${samplePeriod.start_time ?? ''} - ${ samplePeriod.end_date - } ${samplePeriod.end_time || ''}`} + } ${samplePeriod.end_time ?? ''}`} From 022b5b944298387a0302f082e4d854621f6c2e45 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 5 Dec 2023 11:48:34 -0800 Subject: [PATCH 72/88] more code smell clean up --- app/src/contexts/telemetryTableContext.tsx | 10 +++---- .../surveys/telemetry/ManualTelemetryCard.tsx | 1 - .../surveys/telemetry/ManualTelemetryList.tsx | 26 ++++++++----------- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/app/src/contexts/telemetryTableContext.tsx b/app/src/contexts/telemetryTableContext.tsx index f32973f039..debed59f37 100644 --- a/app/src/contexts/telemetryTableContext.tsx +++ b/app/src/contexts/telemetryTableContext.tsx @@ -154,7 +154,7 @@ export const TelemetryTableContextProvider: React.FC(0); // Stores the current validation state of the table @@ -532,8 +532,8 @@ export const TelemetryTableContextProvider: React.FC { - return isCurrentltSaving || isStoppingEdit; - }, [isCurrentltSaving, isStoppingEdit]); + return isCurrentlySaving || isStoppingEdit; + }, [isCurrentlySaving, isStoppingEdit]); useEffect(() => { // Begin fetching telemetry once we have deployments ids @@ -593,7 +593,7 @@ export const TelemetryTableContextProvider: React.FC value); _saveRecords(rowValues); - }, [_muiDataGridApiRef, _saveRecords, isCurrentltSaving, isStoppingEdit, modifiedRowIds]); + }, [_muiDataGridApiRef, _saveRecords, isCurrentlySaving, isStoppingEdit, modifiedRowIds]); const telemetryTableContext: ITelemetryTableContext = useMemo( () => ({ diff --git a/app/src/features/surveys/telemetry/ManualTelemetryCard.tsx b/app/src/features/surveys/telemetry/ManualTelemetryCard.tsx index 317726e1b6..63c9cecd7a 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryCard.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryCard.tsx @@ -9,7 +9,6 @@ import moment from 'moment'; export interface ManualTelemetryCardProps { device_id: number; name: string; // should be animal alias - taxon: string; start_date: string; end_date?: string; diff --git a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx index 4f57f3ac6e..fee0f802a1 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryList.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryList.tsx @@ -247,11 +247,9 @@ const ManualTelemetryList = () => { // success snack bar dialogContext.setSnackbar({ snackbarMessage: ( - <> - - Deployment Added - - + + Deployment Added + ), open: true }); @@ -278,11 +276,9 @@ const ManualTelemetryList = () => { await updateDevice(data); dialogContext.setSnackbar({ snackbarMessage: ( - <> - - Deployment Updated - - + + Deployment Updated + ), open: true }); @@ -304,14 +300,14 @@ const ManualTelemetryList = () => { }; const updateDeployments = async (data: AnimalDeployment) => { - for (const deployment of data.deployments || []) { + for (const deployment of data.deployments ?? []) { const existingDeployment = deployments?.find((item) => item.deployment_id === deployment.deployment_id); if ( !datesSameNullable(deployment?.attachment_start, existingDeployment?.attachment_start) || !datesSameNullable(deployment?.attachment_end, existingDeployment?.attachment_end) ) { try { - biohubApi.survey.updateDeployment( + await biohubApi.survey.updateDeployment( surveyContext.projectId, surveyContext.surveyId, data.survey_critter_id, @@ -426,7 +422,7 @@ const ManualTelemetryList = () => { }}> {critters?.map((item) => { return ( - + { }}> {critterDeployments?.map((item) => ( { From 2034536a8808271316462642bd2413be47db4326 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 5 Dec 2023 11:51:15 -0800 Subject: [PATCH 73/88] more smells --- app/src/contexts/telemetryTableContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/contexts/telemetryTableContext.tsx b/app/src/contexts/telemetryTableContext.tsx index debed59f37..0e7cd460d5 100644 --- a/app/src/contexts/telemetryTableContext.tsx +++ b/app/src/contexts/telemetryTableContext.tsx @@ -164,7 +164,7 @@ export const TelemetryTableContextProvider: React.FC { - const rowValues = Array.from(_muiDataGridApiRef.current.getRowModels?.()?.values()) as IManualTelemetryTableRow[]; + const rowValues = Array.from(_muiDataGridApiRef.current.getRowModels?.()?.values()); return rowValues.map((row) => { const editRow = _muiDataGridApiRef.current.state.editRows[row.id]; From 5119713859ef4aef3feebd770a6ab1c1a6f1f078 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 5 Dec 2023 11:57:29 -0800 Subject: [PATCH 74/88] ran make fix --- api/src/utils/xlsx-utils/worksheet-utils.ts | 2 +- app/src/constants/i18n.ts | 8 +++++--- app/src/contexts/telemetryTableContext.tsx | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/api/src/utils/xlsx-utils/worksheet-utils.ts b/api/src/utils/xlsx-utils/worksheet-utils.ts index 8b12326dad..96c6d2b41e 100644 --- a/api/src/utils/xlsx-utils/worksheet-utils.ts +++ b/api/src/utils/xlsx-utils/worksheet-utils.ts @@ -280,7 +280,7 @@ export interface IXLSXCSVValidator { export function validateCsvFile( xlsxWorksheets: xlsx.WorkSheet, columnValidator: IXLSXCSVValidator, - sheet: string = 'Sheet1' + sheet = 'Sheet1' ): boolean { // Validate the worksheet headers if (!validateWorksheetHeaders(xlsxWorksheets[sheet], columnValidator.columnNames)) { diff --git a/app/src/constants/i18n.ts b/app/src/constants/i18n.ts index 6930716a56..71f7a59127 100644 --- a/app/src/constants/i18n.ts +++ b/app/src/constants/i18n.ts @@ -1,4 +1,4 @@ -import { pluralize as p } from "utils/Utils"; +import { pluralize as p } from 'utils/Utils'; export const CreateProjectI18N = { cancelTitle: 'Discard changes and exit?', @@ -410,7 +410,8 @@ export const ObservationsTableI18N = { 'An error has occurred while attempting to delete observation records for this survey. Please try again. If the error persists, please contact your system administrator.', saveRecordsSuccessSnackbarMessage: 'Observations updated successfully.', deleteSingleRecordSuccessSnackbarMessage: 'Deleted observation record successfully.', - deleteMultipleRecordSuccessSnackbarMessage: (count: number) => `Deleted ${count} ${p(count, 'observation record')} successfully.` + deleteMultipleRecordSuccessSnackbarMessage: (count: number) => + `Deleted ${count} ${p(count, 'observation record')} successfully.` }; export const TelemetryTableI18N = { @@ -430,5 +431,6 @@ export const TelemetryTableI18N = { 'An error has occurred while attempting to delete telemetry records for this survey. Please try again. If the error persists, please contact your system administrator.', saveRecordsSuccessSnackbarMessage: 'Telemetry updated successfully.', deleteSingleRecordSuccessSnackbarMessage: 'Deleted telemetry record successfully.', - deleteMultipleRecordSuccessSnackbarMessage: (count: number) => `Deleted ${count} ${p(count, 'telemetry record')} successfully.` + deleteMultipleRecordSuccessSnackbarMessage: (count: number) => + `Deleted ${count} ${p(count, 'telemetry record')} successfully.` }; diff --git a/app/src/contexts/telemetryTableContext.tsx b/app/src/contexts/telemetryTableContext.tsx index 0e7cd460d5..debed59f37 100644 --- a/app/src/contexts/telemetryTableContext.tsx +++ b/app/src/contexts/telemetryTableContext.tsx @@ -164,7 +164,7 @@ export const TelemetryTableContextProvider: React.FC { - const rowValues = Array.from(_muiDataGridApiRef.current.getRowModels?.()?.values()); + const rowValues = Array.from(_muiDataGridApiRef.current.getRowModels?.()?.values()) as IManualTelemetryTableRow[]; return rowValues.map((row) => { const editRow = _muiDataGridApiRef.current.state.editRows[row.id]; From d04e1fc33d4046d21d352bbaac6eb0fdc32d47b9 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 5 Dec 2023 12:15:42 -0800 Subject: [PATCH 75/88] fixed bug caused by code smell fix --- app/src/contexts/telemetryDataContext.tsx | 11 ++++------- .../surveys/telemetry/ManualTelemetryPage.tsx | 2 +- app/src/hooks/useTelemetryApi.ts | 1 + 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/src/contexts/telemetryDataContext.tsx b/app/src/contexts/telemetryDataContext.tsx index ebb12a4748..7986a33f3c 100644 --- a/app/src/contexts/telemetryDataContext.tsx +++ b/app/src/contexts/telemetryDataContext.tsx @@ -1,6 +1,6 @@ import useDataLoader, { DataLoader } from 'hooks/useDataLoader'; import { ITelemetry, useTelemetryApi } from 'hooks/useTelemetryApi'; -import { createContext, PropsWithChildren, useMemo } from 'react'; +import { createContext, PropsWithChildren } from 'react'; export type ITelemetryDataContext = { telemetryDataLoader: DataLoader<[ids: string[]], ITelemetry[], unknown>; @@ -15,12 +15,9 @@ export const TelemetryDataContextProvider = (props: PropsWithChildren ({ - telemetryDataLoader - }), - [] - ); + const telemetryDataContext: ITelemetryDataContext = { + telemetryDataLoader + }; return {props.children}; }; diff --git a/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx b/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx index 1182ec554a..c366f79f1b 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryPage.tsx @@ -45,7 +45,7 @@ const ManualTelemetryPage = () => { - + diff --git a/app/src/hooks/useTelemetryApi.ts b/app/src/hooks/useTelemetryApi.ts index e0467a3b9c..316ccb4c9b 100644 --- a/app/src/hooks/useTelemetryApi.ts +++ b/app/src/hooks/useTelemetryApi.ts @@ -54,6 +54,7 @@ export const useTelemetryApi = () => { const getAllTelemetry = async (ids: string[]): Promise => { const { data } = await axios.post('/api/telemetry/deployments', ids); + console.log('API ENDPOINT FINISHED', data.length); return data; }; From 88845c967e72498adb3807ceb9cd40cc7c423890 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 5 Dec 2023 14:04:20 -0800 Subject: [PATCH 76/88] updated telemetry count --- app/src/contexts/telemetryTableContext.tsx | 7 ++++++- .../surveys/telemetry/ManualTelemetryComponent.tsx | 2 +- app/src/hooks/useTelemetryApi.ts | 1 - 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/contexts/telemetryTableContext.tsx b/app/src/contexts/telemetryTableContext.tsx index debed59f37..469cb6ce00 100644 --- a/app/src/contexts/telemetryTableContext.tsx +++ b/app/src/contexts/telemetryTableContext.tsx @@ -230,6 +230,10 @@ export const TelemetryTableContextProvider: React.FC 0 ? validation : null; }; + useEffect(() => { + setRecordCount(rows.length); + }, [rows]); + const _commitDeleteRecords = useCallback( async (telemetryRecords: IManualTelemetryTableRow[]): Promise => { if (!telemetryRecords.length) { @@ -370,7 +374,8 @@ export const TelemetryTableContextProvider: React.FC { }}> Telemetry ‌ - ({0}) + ({telemetryTableContext.recordCount}) diff --git a/app/src/hooks/useTelemetryApi.ts b/app/src/hooks/useTelemetryApi.ts index 316ccb4c9b..e0467a3b9c 100644 --- a/app/src/hooks/useTelemetryApi.ts +++ b/app/src/hooks/useTelemetryApi.ts @@ -54,7 +54,6 @@ export const useTelemetryApi = () => { const getAllTelemetry = async (ids: string[]): Promise => { const { data } = await axios.post('/api/telemetry/deployments', ids); - console.log('API ENDPOINT FINISHED', data.length); return data; }; From a53796b5cb4cfc1575d790cb458ac90ee0546074 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 5 Dec 2023 14:12:49 -0800 Subject: [PATCH 77/88] ignore-skip From 399efb751f8c5d565df61f77372046f529048c80 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 5 Dec 2023 14:16:18 -0800 Subject: [PATCH 78/88] hiding import button until it is working --- .../telemetry/ManualTelemetryComponent.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/src/features/surveys/telemetry/ManualTelemetryComponent.tsx b/app/src/features/surveys/telemetry/ManualTelemetryComponent.tsx index 895c4316f6..32ab1c08fe 100644 --- a/app/src/features/surveys/telemetry/ManualTelemetryComponent.tsx +++ b/app/src/features/surveys/telemetry/ManualTelemetryComponent.tsx @@ -121,13 +121,15 @@ const ManualTelemetryComponent = () => { - + {false && ( + + )}