From 606a5359e0e95a205c1c3183bf2d1300d3ffb8dd Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Wed, 11 Dec 2024 13:19:58 -0800 Subject: [PATCH 01/13] Minor UI cleanup/tweaks. Resolves some warnings about incorrect html. Fix two incorrect useEffects for the device/deployment edit forms. --- .../telemetry/manual/delete.test.ts | 2 +- .../telemetry/manual/index.test.ts | 2 +- api/src/paths/telemetry/vendor/deployments.ts | 2 +- .../fields/AnimalAutocompleteField.tsx | 7 +++-- .../fields/DeviceAutocompleteField.tsx | 9 +++--- .../telemetry/list/SurveyDeploymentList.tsx | 9 +----- .../deployments/form/DeploymentForm.tsx | 2 +- .../table/DeploymentsContainer.tsx | 2 +- .../deployments/table/DeploymentsTable.tsx | 30 +++++++++++-------- .../manage/devices/table/DevicesTable.tsx | 4 +-- 10 files changed, 34 insertions(+), 35 deletions(-) diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/telemetry/manual/delete.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/telemetry/manual/delete.test.ts index c94f13e282..e32243af91 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/telemetry/manual/delete.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/telemetry/manual/delete.test.ts @@ -5,7 +5,7 @@ import { TelemetryVendorService } from '../../../../../../../../services/telemet import { getMockDBConnection, getRequestHandlerMocks } from '../../../../../../../../__mocks__/db'; import { bulkDeleteManualTelemetry } from './delete'; -describe('telemetry/manual/delete', () => { +describe('delete', () => { afterEach(() => { sinon.restore(); }); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/telemetry/manual/index.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/telemetry/manual/index.test.ts index ca6642b3e1..a0c278c97e 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/telemetry/manual/index.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/telemetry/manual/index.test.ts @@ -5,7 +5,7 @@ import * as db from '../../../../../../../../database/db'; import { TelemetryVendorService } from '../../../../../../../../services/telemetry-services/telemetry-vendor-service'; import { getMockDBConnection, getRequestHandlerMocks } from '../../../../../../../../__mocks__/db'; -describe('telemetry/manual/index', () => { +describe('index', () => { afterEach(() => { sinon.restore(); }); diff --git a/api/src/paths/telemetry/vendor/deployments.ts b/api/src/paths/telemetry/vendor/deployments.ts index de4f3e3e90..e2b2997ce7 100644 --- a/api/src/paths/telemetry/vendor/deployments.ts +++ b/api/src/paths/telemetry/vendor/deployments.ts @@ -5,7 +5,7 @@ import { getBctwUser } from '../../../services/bctw-service/bctw-service'; import { BctwTelemetryService } from '../../../services/bctw-service/bctw-telemetry-service'; import { getLogger } from '../../../utils/logger'; -const defaultLog = getLogger('paths/telemetry/manual'); +const defaultLog = getLogger('paths/telemetry/vendor/deployments'); const vendor_telemetry_responses = { 200: { diff --git a/app/src/components/fields/AnimalAutocompleteField.tsx b/app/src/components/fields/AnimalAutocompleteField.tsx index 154ec4b343..1ffbcb207a 100644 --- a/app/src/components/fields/AnimalAutocompleteField.tsx +++ b/app/src/components/fields/AnimalAutocompleteField.tsx @@ -86,7 +86,7 @@ export interface IAnimalAutocompleteFieldProps { export const AnimalAutocompleteField = (props: IAnimalAutocompleteFieldProps) => { const { formikFieldName, label, onSelect, defaultAnimal, required, disabled, clearOnSelect, placeholder } = props; - const { touched, errors, setFieldValue, values } = useFormikContext>(); + const { touched, errors, setFieldValue } = useFormikContext>(); const surveyContext = useSurveyContext(); @@ -94,12 +94,13 @@ export const AnimalAutocompleteField = (props: IAnima const [inputValue, setInputValue] = useState(defaultAnimal?.animal_id ?? ''); useEffect(() => { - if (!defaultAnimal || get(values, formikFieldName)) { + if (!defaultAnimal) { return; } + // Set the input value to the default animal's animal_id setInputValue(String(defaultAnimal.animal_id)); - }, [defaultAnimal, formikFieldName, values]); + }, [defaultAnimal]); // Survey animals to choose from const options = surveyContext.critterDataLoader.data; diff --git a/app/src/components/fields/DeviceAutocompleteField.tsx b/app/src/components/fields/DeviceAutocompleteField.tsx index fc10712b2d..1a6b5d9dfc 100644 --- a/app/src/components/fields/DeviceAutocompleteField.tsx +++ b/app/src/components/fields/DeviceAutocompleteField.tsx @@ -91,7 +91,7 @@ export const DeviceAutocompleteField = (props: IDevic const { formikFieldName, label, options, onSelect, defaultDevice, required, disabled, clearOnSelect, placeholder } = props; - const { touched, errors, setFieldValue, values } = useFormikContext>(); + const { touched, errors, setFieldValue } = useFormikContext>(); const codesContext = useCodesContext(); @@ -103,12 +103,13 @@ export const DeviceAutocompleteField = (props: IDevic const [inputValue, setInputValue] = useState(String(defaultDevice?.device_id ?? '')); useEffect(() => { - if (!defaultDevice || get(values, formikFieldName)) { + if (!defaultDevice) { return; } - setInputValue(String(defaultDevice.device_id)); - }, [defaultDevice, formikFieldName, values]); + // Set the input value to the default device's serial + setInputValue(String(defaultDevice.serial)); + }, [defaultDevice]); return ( { }}> setDeploymentAnchorEl(null)}> @@ -392,12 +391,6 @@ export const SurveyDeploymentList = (props: ISurveyDeploymentListProps) => { sx={{ background: grey[100] }}> - {deployments.map((deployment) => { const animal = surveyContext.critterDataLoader.data?.find( (animal) => animal.critterbase_critter_id === deployment.critterbase_critter_id diff --git a/app/src/features/surveys/telemetry/manage/deployments/form/DeploymentForm.tsx b/app/src/features/surveys/telemetry/manage/deployments/form/DeploymentForm.tsx index c80e5874cd..e18643f9aa 100644 --- a/app/src/features/surveys/telemetry/manage/deployments/form/DeploymentForm.tsx +++ b/app/src/features/surveys/telemetry/manage/deployments/form/DeploymentForm.tsx @@ -105,7 +105,7 @@ export const DeploymentForm = (props: IDeploymentFormProps) => { summary={ <> Enter information about the device and animal. - + You must  { isLoading={deploymentsDataLoader.isLoading} isLoadingFallback={} isLoadingFallbackDelay={100}> - + } diff --git a/app/src/features/surveys/telemetry/manage/deployments/table/DeploymentsTable.tsx b/app/src/features/surveys/telemetry/manage/deployments/table/DeploymentsTable.tsx index efc6308a68..f24872e401 100644 --- a/app/src/features/surveys/telemetry/manage/deployments/table/DeploymentsTable.tsx +++ b/app/src/features/surveys/telemetry/manage/deployments/table/DeploymentsTable.tsx @@ -159,8 +159,8 @@ export const DeploymentsTable = (props: IDeploymentsTableProps) => { field: 'deployment2_id', headerName: 'Deployment ID', description: 'The unique key for the deployment', - width: 100, - minWidth: 100, + width: 85, + minWidth: 85, renderHeader: (params) => ( @@ -198,7 +198,7 @@ export const DeploymentsTable = (props: IDeploymentsTableProps) => { return ( {serial} - + {vendor} @@ -213,7 +213,7 @@ export const DeploymentsTable = (props: IDeploymentsTableProps) => { renderCell: (params) => ( {params.row.frequency}  - + {codesContext.codesDataLoader.data?.frequency_units.find( (frequencyUnit) => frequencyUnit.id === params.row.frequency_unit_id )?.name ?? null} @@ -241,15 +241,19 @@ export const DeploymentsTable = (props: IDeploymentsTableProps) => { headerName: 'End', description: 'The end date of the deployment', flex: 1, - renderCell: (params) => ( - <> - {params.row.attachment_end_time - ? dayjs(`${params.row.attachment_end_date} ${params.row.attachment_end_time}`).format( - DATE_FORMAT.MediumDateTimeFormat - ) - : dayjs(params.row.attachment_end_date).format(DATE_FORMAT.MediumDateFormat)} - - ) + renderCell: (params) => { + if (!params.row.attachment_end_date) { + return null; + } + + if (params.row.attachment_end_time) { + return dayjs(`${params.row.attachment_end_date} ${params.row.attachment_end_time}`).format( + DATE_FORMAT.MediumDateTimeFormat + ); + } + + return dayjs(params.row.attachment_end_date).format(DATE_FORMAT.MediumDateFormat); + } }, { field: 'status', diff --git a/app/src/features/surveys/telemetry/manage/devices/table/DevicesTable.tsx b/app/src/features/surveys/telemetry/manage/devices/table/DevicesTable.tsx index b6fdbde1e2..89b3833758 100644 --- a/app/src/features/surveys/telemetry/manage/devices/table/DevicesTable.tsx +++ b/app/src/features/surveys/telemetry/manage/devices/table/DevicesTable.tsx @@ -137,8 +137,8 @@ export const DevicesTable = (props: IDevicesTableProps) => { field: 'device_id', headerName: 'Device ID', description: 'The unique key for the device', - width: 100, - minWidth: 100, + width: 85, + minWidth: 85, renderHeader: (params) => ( From 39765abf6b67086f937073eb29ce58bd8bf6d9b1 Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Wed, 11 Dec 2024 16:44:43 -0800 Subject: [PATCH 02/13] Update delete survey procedure --- .../src/procedures/delete_survey_procedure.ts | 104 +++++++++++++++++- 1 file changed, 102 insertions(+), 2 deletions(-) diff --git a/database/src/procedures/delete_survey_procedure.ts b/database/src/procedures/delete_survey_procedure.ts index 460257526a..53add146c9 100644 --- a/database/src/procedures/delete_survey_procedure.ts +++ b/database/src/procedures/delete_survey_procedure.ts @@ -22,6 +22,8 @@ export async function seed(knex: Knex): Promise { BEGIN + -------- delete basic survey data -------- + WITH occurrence_submissions AS ( @@ -152,17 +154,81 @@ export async function seed(knex: Knex): Promise { DELETE FROM survey_location WHERE survey_id = p_survey_id; + DELETE FROM survey_intended_outcome + WHERE survey_id = p_survey_id; + + -------- delete device, deployment, credential, telemetry data -------- + + DELETE FROM telemetry_manual + WHERE deployment2_id IN (SELECT deployment2_id FROM deployment2 WHERE survey_id = p_survey_id); + + DELETE FROM survey_telemetry_vendor_credential + WHERE survey_telemetry_credential_attachment_id IN (SELECT survey_telemetry_credential_attachment_id from survey_telemetry_credential_attachment WHERE survey_id = p_survey_id); + + DELETE FROM survey_telemetry_credential_attachment + WHERE survey_id = p_survey_id; + DELETE FROM deployment WHERE critter_id IN (SELECT critter_id FROM critter WHERE survey_id = p_survey_id); - DELETE FROM critter + DELETE FROM deployment2 WHERE survey_id = p_survey_id; - DELETE FROM survey_intended_outcome + DELETE FROM device + WHERE survey_id = p_survey_id; + + -------- delete animal data -------- + + DELETE FROM subcount_critter + WHERE critter_id IN (SELECT critter_id FROM critter WHERE survey_id = p_survey_id); + + DELETE FROM critter_mortality_attachment + WHERE critter_id IN (SELECT critter_id FROM critter WHERE survey_id = p_survey_id); + + DELETE FROM critter_capture_attachment + WHERE critter_id IN (SELECT critter_id FROM critter WHERE survey_id = p_survey_id); + + DELETE FROM critter WHERE survey_id = p_survey_id; -------- delete observation data -------- + DELETE FROM observation_subcount_qualitative_environment + WHERE observation_subcount_id IN ( + SELECT observation_subcount_id FROM observation_subcount + WHERE survey_observation_id IN ( + SELECT survey_observation_id FROM survey_observation + WHERE survey_id = p_survey_id + ) + ); + + DELETE FROM observation_subcount_quantitative_environment + WHERE observation_subcount_id IN ( + SELECT observation_subcount_id FROM observation_subcount + WHERE survey_observation_id IN ( + SELECT survey_observation_id FROM survey_observation + WHERE survey_id = p_survey_id + ) + ); + + DELETE FROM observation_subcount_qualitative_measurement + WHERE observation_subcount_id IN ( + SELECT observation_subcount_id FROM observation_subcount + WHERE survey_observation_id IN ( + SELECT survey_observation_id FROM survey_observation + WHERE survey_id = p_survey_id + ) + ); + + DELETE FROM observation_subcount_quantitative_measurement + WHERE observation_subcount_id IN ( + SELECT observation_subcount_id FROM observation_subcount + WHERE survey_observation_id IN ( + SELECT survey_observation_id FROM survey_observation + WHERE survey_id = p_survey_id + ) + ); + DELETE FROM observation_subcount WHERE survey_observation_id IN ( SELECT survey_observation_id FROM survey_observation @@ -195,6 +261,7 @@ export async function seed(knex: Knex): Promise { WHERE survey_id = p_survey_id; -------- delete sampling data -------- + DELETE FROM survey_sample_period WHERE survey_sample_method_id IN ( SELECT survey_sample_method_id @@ -220,8 +287,41 @@ export async function seed(knex: Knex): Promise { DELETE FROM survey_sample_site WHERE survey_id = p_survey_id; + -------- delete technique data -------- + + DELETE FROM method_technique_attractant + WHERE method_technique_id IN ( + SELECT method_technique_id + FROM method_technique + WHERE survey_id = p_survey_id + ); + + DELETE FROM method_technique_attribute_qualitative + WHERE method_technique_id IN ( + SELECT method_technique_id + FROM method_technique + WHERE survey_id = p_survey_id + ); + + DELETE FROM method_technique_attribute_quantitative + WHERE method_technique_id IN ( + SELECT method_technique_id + FROM method_technique + WHERE survey_id = p_survey_id + ); + + DELETE FROM method_technique_vantage_mode + WHERE method_technique_id IN ( + SELECT method_technique_id + FROM method_technique + WHERE survey_id = p_survey_id + ); + + DELETE FROM method_technique + WHERE survey_id = p_survey_id; -------- delete the survey -------- + DELETE FROM survey WHERE survey_id = p_survey_id; From a4cdc8833a7cd9da261522ba61f00e6e728593ff Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Thu, 12 Dec 2024 10:26:39 -0800 Subject: [PATCH 03/13] Delete old bctw-related api code --- api/src/paths/telemetry/code.test.ts | 57 ------- api/src/paths/telemetry/code.ts | 88 ----------- api/src/paths/telemetry/deployments.test.ts | 71 --------- api/src/paths/telemetry/deployments.ts | 90 ------------ .../telemetry/vendor/deployments.test.ts | 75 ---------- api/src/paths/telemetry/vendor/deployments.ts | 109 -------------- .../bctw-service/bctw-deployment-service.ts | 129 ---------------- .../bctw-service/bctw-keyx-service.test.ts | 83 ----------- .../bctw-service/bctw-keyx-service.ts | 102 ------------- api/src/services/bctw-service/bctw-service.ts | 124 ---------------- .../bctw-service/bctw-telemetry-service.ts | 139 ------------------ 11 files changed, 1067 deletions(-) delete mode 100644 api/src/paths/telemetry/code.test.ts delete mode 100644 api/src/paths/telemetry/code.ts delete mode 100644 api/src/paths/telemetry/deployments.test.ts delete mode 100644 api/src/paths/telemetry/deployments.ts delete mode 100644 api/src/paths/telemetry/vendor/deployments.test.ts delete mode 100644 api/src/paths/telemetry/vendor/deployments.ts delete mode 100644 api/src/services/bctw-service/bctw-deployment-service.ts delete mode 100644 api/src/services/bctw-service/bctw-keyx-service.test.ts delete mode 100644 api/src/services/bctw-service/bctw-keyx-service.ts delete mode 100644 api/src/services/bctw-service/bctw-service.ts delete mode 100644 api/src/services/bctw-service/bctw-telemetry-service.ts diff --git a/api/src/paths/telemetry/code.test.ts b/api/src/paths/telemetry/code.test.ts deleted file mode 100644 index e510c3b65f..0000000000 --- a/api/src/paths/telemetry/code.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { expect } from 'chai'; -import sinon from 'sinon'; -import { SystemUser } from '../../repositories/user-repository'; -import { BctwService } from '../../services/bctw-service/bctw-service'; -import { getRequestHandlerMocks } from '../../__mocks__/db'; -import { getCodeValues } from './code'; - -describe('getCodeValues', () => { - afterEach(() => { - sinon.restore(); - }); - - it('returns a list of Bctw code objects', async () => { - const mockCodeValues = [ - { - code_header_title: 'title', - code_header_name: 'name', - id: 123, - code: 'code', - description: 'description', - long_description: 'long_description' - } - ]; - const mockGetCode = sinon.stub(BctwService.prototype, 'getCode').resolves(mockCodeValues); - - const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - - mockReq.system_user = { user_identifier: 'user', user_guid: 'guid' } as SystemUser; - - const requestHandler = getCodeValues(); - - await requestHandler(mockReq, mockRes, mockNext); - - expect(mockRes.jsonValue).to.eql(mockCodeValues); - expect(mockRes.statusValue).to.equal(200); - expect(mockGetCode).to.have.been.calledOnce; - }); - - it('catches and re-throws errors', async () => { - const mockError = new Error('mock error'); - const mockGetCode = sinon.stub(BctwService.prototype, 'getCode').rejects(mockError); - - const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - - mockReq.system_user = { user_identifier: 'user', user_guid: 'guid' } as SystemUser; - - const requestHandler = getCodeValues(); - - try { - await requestHandler(mockReq, mockRes, mockNext); - expect.fail(); - } catch (actualError) { - expect(actualError).to.equal(mockError); - expect(mockGetCode).to.have.been.calledOnce; - } - }); -}); diff --git a/api/src/paths/telemetry/code.ts b/api/src/paths/telemetry/code.ts deleted file mode 100644 index a2537809a7..0000000000 --- a/api/src/paths/telemetry/code.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { RequestHandler } from 'express'; -import { Operation } from 'express-openapi'; -import { authorizeRequestHandler } from '../../request-handlers/security/authorization'; -import { BctwService, getBctwUser } from '../../services/bctw-service/bctw-service'; -import { getLogger } from '../../utils/logger'; - -const defaultLog = getLogger('paths/telemetry/code'); - -export const GET: Operation = [ - authorizeRequestHandler(() => { - return { - and: [ - { - discriminator: 'SystemUser' - } - ] - }; - }), - getCodeValues() -]; - -GET.apiDoc = { - description: 'Get a list of "code" values from the exterior telemetry system.', - tags: ['telemetry'], - security: [ - { - Bearer: [] - } - ], - responses: { - 200: { - description: 'Generic telemetry code response.', - content: { - 'application/json': { - schema: { - type: 'array', - items: { - type: 'object', - additionalProperties: false, - properties: { - id: { type: 'number' }, - code: { type: 'string' }, - code_header_title: { type: 'string' }, - code_header_name: { type: 'string' }, - description: { type: 'string' }, - long_description: { type: 'string' } - } - } - } - } - } - }, - 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 function getCodeValues(): RequestHandler { - return async (req, res) => { - const user = getBctwUser(req); - - const bctwService = new BctwService(user); - - const codeHeader = String(req.query.codeHeader); - - try { - const result = await bctwService.getCode(codeHeader); - - return res.status(200).json(result); - } catch (error) { - defaultLog.error({ label: 'getCodeValues', message: 'error', error }); - throw error; - } - }; -} diff --git a/api/src/paths/telemetry/deployments.test.ts b/api/src/paths/telemetry/deployments.test.ts deleted file mode 100644 index 471f6a2f23..0000000000 --- a/api/src/paths/telemetry/deployments.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { expect } from 'chai'; -import sinon from 'sinon'; -import { SystemUser } from '../../repositories/user-repository'; -import { BctwTelemetryService, IAllTelemetry } from '../../services/bctw-service/bctw-telemetry-service'; -import { getRequestHandlerMocks } from '../../__mocks__/db'; -import { getAllTelemetryByDeploymentIds } from './deployments'; - -const mockTelemetry: IAllTelemetry[] = [ - { - id: '123-123-123', - telemetry_id: null, - telemetry_manual_id: '123-123-123', - deployment_id: '345-345-345', - latitude: 49.123, - longitude: -126.123, - acquisition_date: '2021-01-01', - telemetry_type: 'manual' - }, - { - id: '567-567-567', - telemetry_id: '567-567-567', - telemetry_manual_id: null, - deployment_id: '345-345-345', - latitude: 49.123, - longitude: -126.123, - acquisition_date: '2021-01-01', - telemetry_type: 'vendor' - } -]; - -describe('getAllTelemetryByDeploymentIds', () => { - afterEach(() => { - sinon.restore(); - }); - it('should retrieve both manual and vendor telemetry', async () => { - const mockGetTelemetry = sinon - .stub(BctwTelemetryService.prototype, 'getAllTelemetryByDeploymentIds') - .resolves(mockTelemetry); - - const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - - mockReq.system_user = { user_identifier: 'user', user_guid: 'guid' } as SystemUser; - - const requestHandler = getAllTelemetryByDeploymentIds(); - - await requestHandler(mockReq, mockRes, mockNext); - - expect(mockRes.jsonValue).to.eql(mockTelemetry); - expect(mockRes.statusValue).to.equal(200); - expect(mockGetTelemetry).to.have.been.calledOnce; - }); - it('should catch error', async () => { - const mockError = new Error('test error'); - const mockGetTelemetry = sinon - .stub(BctwTelemetryService.prototype, 'getAllTelemetryByDeploymentIds') - .rejects(mockError); - - const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - - mockReq.system_user = { user_identifier: 'user', user_guid: 'guid' } as SystemUser; - - const requestHandler = getAllTelemetryByDeploymentIds(); - - try { - await requestHandler(mockReq, mockRes, mockNext); - } catch (err) { - expect(err).to.equal(mockError); - expect(mockGetTelemetry).to.have.been.calledOnce; - } - }); -}); diff --git a/api/src/paths/telemetry/deployments.ts b/api/src/paths/telemetry/deployments.ts deleted file mode 100644 index 035276ad86..0000000000 --- a/api/src/paths/telemetry/deployments.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { RequestHandler } from 'express'; -import { Operation } from 'express-openapi'; -import { AllTelemetrySchema } from '../../openapi/schemas/telemetry'; -import { authorizeRequestHandler } from '../../request-handlers/security/authorization'; -import { getBctwUser } from '../../services/bctw-service/bctw-service'; -import { BctwTelemetryService } from '../../services/bctw-service/bctw-telemetry-service'; -import { getLogger } from '../../utils/logger'; - -const defaultLog = getLogger('paths/telemetry/deployments'); - -export const GET: Operation = [ - authorizeRequestHandler(() => { - return { - and: [ - { - discriminator: 'SystemUser' - } - ] - }; - }), - getAllTelemetryByDeploymentIds() -]; - -GET.apiDoc = { - description: 'Get manual and vendor telemetry for a set of deployment Ids', - tags: ['telemetry'], - security: [ - { - Bearer: [] - } - ], - parameters: [ - { - in: 'query', - name: 'bctwDeploymentIds', - schema: { - type: 'array', - items: { type: 'string', format: 'uuid', minimum: 1 } - }, - required: true - } - ], - responses: { - 200: { - description: 'Manual and Vendor telemetry response object', - content: { - 'application/json': { - schema: { - type: 'array', - items: AllTelemetrySchema - } - } - } - }, - 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 function getAllTelemetryByDeploymentIds(): RequestHandler { - return async (req, res) => { - const user = getBctwUser(req); - - const bctwTelemetryService = new BctwTelemetryService(user); - - try { - const bctwDeploymentIds = req.query.bctwDeploymentIds as string[]; - - const result = await bctwTelemetryService.getAllTelemetryByDeploymentIds(bctwDeploymentIds); - - return res.status(200).json(result); - } catch (error) { - defaultLog.error({ label: 'getAllTelemetryByDeploymentIds', message: 'error', error }); - throw error; - } - }; -} diff --git a/api/src/paths/telemetry/vendor/deployments.test.ts b/api/src/paths/telemetry/vendor/deployments.test.ts deleted file mode 100644 index c1a0d360cd..0000000000 --- a/api/src/paths/telemetry/vendor/deployments.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { expect } from 'chai'; -import sinon from 'sinon'; -import { SystemUser } from '../../../repositories/user-repository'; -import { BctwTelemetryService, IVendorTelemetry } from '../../../services/bctw-service/bctw-telemetry-service'; -import { getRequestHandlerMocks } from '../../../__mocks__/db'; -import { getVendorTelemetryByDeploymentIds } from './deployments'; - -const mockTelemetry: IVendorTelemetry[] = [ - { - telemetry_id: '123-123-123', - deployment_id: '345-345-345', - latitude: 49.123, - longitude: -126.123, - acquisition_date: '2021-01-01', - collar_transaction_id: '45-45-45', - critter_id: '78-78-78', - deviceid: 123456, - elevation: 200, - vendor: 'vendor1' - }, - { - telemetry_id: '456-456-456', - deployment_id: '789-789-789', - latitude: 49.123, - longitude: -126.123, - acquisition_date: '2021-01-01', - collar_transaction_id: '54-54-54', - critter_id: '87-87-87', - deviceid: 654321, - elevation: 10, - vendor: 'vendor2' - } -]; - -describe('getVendorTelemetryByDeploymentIds', () => { - afterEach(() => { - sinon.restore(); - }); - it('should retrieve all vendor telemetry by deployment ids', async () => { - const mockGetTelemetry = sinon - .stub(BctwTelemetryService.prototype, 'getVendorTelemetryByDeploymentIds') - .resolves(mockTelemetry); - - const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - - mockReq.system_user = { user_identifier: 'user', user_guid: 'guid' } as SystemUser; - - const requestHandler = getVendorTelemetryByDeploymentIds(); - - await requestHandler(mockReq, mockRes, mockNext); - - expect(mockRes.jsonValue).to.eql(mockTelemetry); - expect(mockRes.statusValue).to.equal(200); - expect(mockGetTelemetry).to.have.been.calledOnce; - }); - it('should catch error', async () => { - const mockError = new Error('test error'); - const mockGetTelemetry = sinon - .stub(BctwTelemetryService.prototype, 'getVendorTelemetryByDeploymentIds') - .rejects(mockError); - - const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - - mockReq.system_user = { user_identifier: 'user', user_guid: 'guid' } as SystemUser; - - const requestHandler = getVendorTelemetryByDeploymentIds(); - - try { - await requestHandler(mockReq, mockRes, mockNext); - } catch (err) { - expect(err).to.equal(mockError); - expect(mockGetTelemetry).to.have.been.calledOnce; - } - }); -}); diff --git a/api/src/paths/telemetry/vendor/deployments.ts b/api/src/paths/telemetry/vendor/deployments.ts deleted file mode 100644 index e2b2997ce7..0000000000 --- a/api/src/paths/telemetry/vendor/deployments.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { RequestHandler } from 'express'; -import { Operation } from 'express-openapi'; -import { authorizeRequestHandler } from '../../../request-handlers/security/authorization'; -import { getBctwUser } from '../../../services/bctw-service/bctw-service'; -import { BctwTelemetryService } from '../../../services/bctw-service/bctw-telemetry-service'; -import { getLogger } from '../../../utils/logger'; - -const defaultLog = getLogger('paths/telemetry/vendor/deployments'); - -const vendor_telemetry_responses = { - 200: { - description: 'Manual telemetry response object', - content: { - 'application/json': { - schema: { - type: 'array', - items: { - type: 'object', - additionalProperties: false, - 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 BctwTelemetryService(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/bctw-deployment-service.ts b/api/src/services/bctw-service/bctw-deployment-service.ts deleted file mode 100644 index 1d4a9da582..0000000000 --- a/api/src/services/bctw-service/bctw-deployment-service.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { z } from 'zod'; -import { BctwService } from './bctw-service'; - -export const BctwDeploymentRecordWithDeviceMeta = z.object({ - assignment_id: z.string().uuid(), - collar_id: z.string().uuid(), - critter_id: z.string().uuid(), - created_at: z.string(), - created_by_user_id: z.string().nullable(), - updated_at: z.string().nullable(), - updated_by_user_id: z.string().nullable(), - valid_from: z.string(), - valid_to: z.string().nullable(), - attachment_start: z.string(), - attachment_end: z.string().nullable(), - deployment_id: z.string(), - device_id: z.number().nullable(), - device_make: z.number().nullable(), - device_model: z.string().nullable(), - frequency: z.number().nullable(), - frequency_unit: z.number().nullable() -}); -export type BctwDeploymentRecordWithDeviceMeta = z.infer; - -export const BctwDeploymentRecord = z.object({ - assignment_id: z.string(), - collar_id: z.string(), - critter_id: z.string(), - created_at: z.string(), - created_by_user_id: z.string(), - updated_at: z.string().nullable(), - updated_by_user_id: z.string().nullable(), - valid_from: z.string(), - valid_to: z.string().nullable(), - attachment_start: z.string(), - attachment_end: z.string().nullable(), - deployment_id: z.string(), - device_id: z.number() -}); -export type BctwDeploymentRecord = z.infer; - -export const BctwDeploymentUpdate = z.object({ - deployment_id: z.string(), - attachment_start: z.string(), - attachment_end: z.string().nullable() -}); -export type BctwDeploymentUpdate = z.infer; - -export const BctwDeployDevice = z.object({ - deployment_id: z.string().uuid(), - device_id: z.number(), - frequency: z.number().optional(), - frequency_unit: z.string().optional(), - device_make: z.string().optional(), - device_model: z.string().optional(), - attachment_start: z.string(), - attachment_end: z.string().nullable(), - critter_id: z.string() -}); -export type BctwDeployDevice = z.infer; - -export class BctwDeploymentService extends BctwService { - /** - * Create a new deployment for a telemetry device on a critter. - * - * @param {BctwDeployDevice} device - * @return {*} {Promise} - * @memberof BctwDeploymentService - */ - async createDeployment(device: BctwDeployDevice): Promise { - const { data } = await this.axiosInstance.post('/deploy-device', device); - - return data; - } - - /** - * Get deployment records for a list of deployment IDs. - * - * @param {string[]} deploymentIds - * @return {*} {Promise} - * @memberof BctwDeploymentService - */ - async getDeploymentsByIds(deploymentIds: string[]): Promise { - const { data } = await this.axiosInstance.post('/get-deployments', deploymentIds); - - return data; - } - - /** - * Get all existing deployments for a list of critter IDs. - * - * @param {string[]} critter_ids - * @return {*} {Promise} - * @memberof BctwDeploymentService - */ - async getDeploymentsByCritterId(critter_ids: string[]): Promise { - const { data } = await this.axiosInstance.get('/get-deployments-by-critter-id', { - params: { critter_ids: critter_ids } - }); - - return data; - } - - /** - * Update the start and end dates of an existing deployment. - * - * @param {BctwDeploymentUpdate} deployment - * @return {*} {Promise} - * @memberof BctwDeploymentService - */ - async updateDeployment(deployment: BctwDeploymentUpdate): Promise[]> { - const { data } = await this.axiosInstance.patch('/update-deployment', deployment); - - return data; - } - - /** - * Soft deletes the deployment in BCTW. - * - * @param {string} deployment_id uuid - * @returns {*} {Promise} - * @memberof BctwDeploymentService - */ - async deleteDeployment(deployment_id: string): Promise { - const { data } = await this.axiosInstance.delete(`/delete-deployment/${deployment_id}`); - - return data; - } -} diff --git a/api/src/services/bctw-service/bctw-keyx-service.test.ts b/api/src/services/bctw-service/bctw-keyx-service.test.ts deleted file mode 100644 index 52ad7a64e1..0000000000 --- a/api/src/services/bctw-service/bctw-keyx-service.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import chai, { expect } from 'chai'; -import FormData from 'form-data'; -import { describe } from 'mocha'; -import sinon from 'sinon'; -import sinonChai from 'sinon-chai'; -import { BctwKeyxService } from '../bctw-service/bctw-keyx-service'; - -chai.use(sinonChai); - -describe('BctwKeyxService', () => { - afterEach(() => { - sinon.restore(); - }); - - describe('uploadKeyX', () => { - it('should send a post request', async () => { - const mockUser = { keycloak_guid: 'abc123', username: 'testuser' }; - - const bctwKeyxService = new BctwKeyxService(mockUser); - - const mockAxios = sinon - .stub(bctwKeyxService.axiosInstance, 'post') - .resolves({ data: { results: [], errors: [] } }); - - const mockMulterFile = { buffer: 'buffer', originalname: 'originalname.keyx' } as unknown as Express.Multer.File; - - sinon.stub(FormData.prototype, 'append'); - - const mockGetFormDataHeaders = sinon - .stub(FormData.prototype, 'getHeaders') - .resolves({ 'content-type': 'multipart/form-data' }); - - const result = await bctwKeyxService.uploadKeyX(mockMulterFile); - - expect(mockGetFormDataHeaders).to.have.been.calledOnce; - expect(result).to.eql({ totalKeyxFiles: 0, newRecords: 0, existingRecords: 0 }); - expect(mockAxios).to.have.been.calledOnce; - }); - - it('should throw an error if the file is not a valid keyx file', async () => { - const mockUser = { keycloak_guid: 'abc123', username: 'testuser' }; - - const bctwKeyxService = new BctwKeyxService(mockUser); - - sinon.stub(bctwKeyxService.axiosInstance, 'post').rejects(); - - const mockMulterFile = { - buffer: 'buffer', - originalname: 'originalname.notValid' // invalid file extension - } as unknown as Express.Multer.File; - - sinon.stub(FormData.prototype, 'append'); - - sinon.stub(FormData.prototype, 'getHeaders').resolves({ 'content-type': 'multipart/form-data' }); - - await bctwKeyxService - .uploadKeyX(mockMulterFile) - .catch((e) => - expect(e.message).to.equal('File is neither a .keyx file, nor an archive containing only .keyx files') - ); - }); - - it('should throw an error if the response body has errors', async () => { - const mockUser = { keycloak_guid: 'abc123', username: 'testuser' }; - - const bctwKeyxService = new BctwKeyxService(mockUser); - - sinon - .stub(bctwKeyxService.axiosInstance, 'post') - .resolves({ data: { results: [], errors: [{ error: 'error' }] } }); - - const mockMulterFile = { buffer: 'buffer', originalname: 'originalname.keyx' } as unknown as Express.Multer.File; - - sinon.stub(FormData.prototype, 'append'); - - sinon.stub(FormData.prototype, 'getHeaders').resolves({ 'content-type': 'multipart/form-data' }); - - await bctwKeyxService - .uploadKeyX(mockMulterFile) - .catch((e) => expect(e.message).to.equal('API request failed with errors')); - }); - }); -}); diff --git a/api/src/services/bctw-service/bctw-keyx-service.ts b/api/src/services/bctw-service/bctw-keyx-service.ts deleted file mode 100644 index 5ced5e4c17..0000000000 --- a/api/src/services/bctw-service/bctw-keyx-service.ts +++ /dev/null @@ -1,102 +0,0 @@ -import FormData from 'form-data'; -import { z } from 'zod'; -import { ApiError, ApiErrorType } from '../../errors/api-error'; -import { checkFileForKeyx } from '../../utils/media/media-utils'; -import { BctwService } from './bctw-service'; - -export const BctwUploadKeyxResponse = z.object({ - errors: z.array( - z.object({ - row: z.string(), - error: z.string(), - rownum: z.number() - }) - ), - results: z.array( - z.object({ - idcollar: z.number(), - comtype: z.string(), - idcom: z.string(), - collarkey: z.string(), - collartype: z.number(), - dtlast_fetch: z.string().nullable() - }) - ) -}); -export type BctwUploadKeyxResponse = z.infer; - -export const BctwKeyXDetails = z.object({ - device_id: z.number(), - keyx: z - .object({ - idcom: z.string(), - comtype: z.string(), - idcollar: z.number(), - collarkey: z.string(), - collartype: z.number() - }) - .nullable() -}); -export type BctwKeyXDetails = z.infer; - -export class BctwKeyxService extends BctwService { - /** - * Upload a single or multiple zipped keyX files to the BCTW API. - * - * @param {Express.Multer.File} keyX - * @return {*} {Promise} - * @memberof BctwKeyxService - */ - async uploadKeyX(keyX: Express.Multer.File) { - const isValidKeyX = checkFileForKeyx(keyX); - - if (isValidKeyX.error) { - throw new ApiError(ApiErrorType.GENERAL, isValidKeyX.error); - } - - const formData = new FormData(); - - formData.append('xml', keyX.buffer, keyX.originalname); - - const config = { - headers: { - ...formData.getHeaders() - } - }; - - const response = await this.axiosInstance.post('/import-xml', formData, config); - - const data: BctwUploadKeyxResponse = response.data; - - if (data.errors.length) { - const actualErrors: string[] = []; - - for (const error of data.errors) { - // Ignore errors that indicate that a keyX already exists - if (!error.error.endsWith('already exists')) { - actualErrors.push(error.error); - } - } - - if (actualErrors.length) { - throw new ApiError(ApiErrorType.UNKNOWN, 'API request failed with errors', actualErrors); - } - } - - return { - totalKeyxFiles: data.results.length + data.errors.length, - newRecords: data.results.length, - existingRecords: data.errors.length - }; - } - - async getKeyXDetails(deviceIds: number[]): Promise { - const { data } = await this.axiosInstance.get('/get-collars-keyx', { - params: { - device_ids: deviceIds.map((id) => String(id)) - } - }); - - return data; - } -} diff --git a/api/src/services/bctw-service/bctw-service.ts b/api/src/services/bctw-service/bctw-service.ts deleted file mode 100644 index eb28484b91..0000000000 --- a/api/src/services/bctw-service/bctw-service.ts +++ /dev/null @@ -1,124 +0,0 @@ -import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios'; -import { Request } from 'express'; -import { z } from 'zod'; -import { ApiError, ApiErrorType } from '../../errors/api-error'; -import { HTTP500 } from '../../errors/http-error'; -import { ICodeResponse } from '../../models/bctw'; -import { KeycloakService } from '../keycloak-service'; - -export const BctwUser = z.object({ - keycloak_guid: z.string(), - username: z.string() -}); -export type BctwUser = z.infer; - -export const getBctwUser = (req: Request): BctwUser => ({ - keycloak_guid: req.system_user?.user_guid ?? '', - username: req.system_user?.user_identifier ?? '' -}); - -export class BctwService { - user: BctwUser; - keycloak: KeycloakService; - axiosInstance: AxiosInstance; - - constructor(user: BctwUser) { - this.user = user; - this.keycloak = new KeycloakService(); - this.axiosInstance = axios.create({ - headers: { - user: this.getUserHeader() - }, - baseURL: process.env.BCTW_API_HOST || '', - timeout: 10000 - }); - - this.axiosInstance.interceptors.response.use( - (response: AxiosResponse) => { - return response; - }, - (error: AxiosError) => { - if ( - error?.code === 'ECONNREFUSED' || - error?.code === 'ECONNRESET' || - error?.code === 'ETIMEOUT' || - error?.code === 'ECONNABORTED' - ) { - return Promise.reject( - new HTTP500('Connection to the BCTW API server was refused. Please try again later.', [error?.message]) - ); - } - const data: any = error.response?.data; - const errMsg = data?.error ?? data?.errors ?? data ?? 'Unknown error'; - const issues = data?.issues ?? []; - - return Promise.reject( - new ApiError( - ApiErrorType.UNKNOWN, - `API request failed with status code ${error?.response?.status}: ${errMsg}`, - [].concat(errMsg).concat(issues) - ) - ); - } - ); - - // Async request interceptor - this.axiosInstance.interceptors.request.use( - async (config) => { - const token = await this.getToken(); - config.headers['Authorization'] = `Bearer ${token}`; - - return config; - }, - (error) => { - return Promise.reject(error); - } - ); - } - - /** - * Return user information as a JSON string. - * - * @return {*} {string} - * @memberof BctwService - */ - getUserHeader(): string { - return JSON.stringify(this.user); - } - - /** - * Retrieve an authentication token using Keycloak service. - * - * @return {*} {Promise} - * @memberof BctwService - */ - async getToken(): Promise { - const token = await this.keycloak.getKeycloakServiceToken(); - return token; - } - - /** - * Get the health of the platform. - * - * @return {*} {Promise} - * @memberof BctwService - */ - async getHealth(): Promise { - const { data } = await this.axiosInstance.get('/health'); - - return data; - } - - /** - * Get a list of all BCTW codes with a given header name. - * - * @param {string} codeHeaderName - * @return {*} {Promise} - * @memberof BctwService - */ - async getCode(codeHeaderName: string): Promise { - const { data } = await this.axiosInstance.get('/get-code', { params: { codeHeader: codeHeaderName } }); - - return data; - } -} diff --git a/api/src/services/bctw-service/bctw-telemetry-service.ts b/api/src/services/bctw-service/bctw-telemetry-service.ts deleted file mode 100644 index c8d1d2deba..0000000000 --- a/api/src/services/bctw-service/bctw-telemetry-service.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { z } from 'zod'; -import { BctwService } from './bctw-service'; - -export const IAllTelemetry = z - .object({ - id: z.string().uuid(), - deployment_id: z.string().uuid(), - latitude: z.number(), - longitude: z.number(), - acquisition_date: z.string(), - telemetry_type: z.string() - }) - .and( - // One of telemetry_id or telemetry_manual_id is expected to be non-null - z.union([ - z.object({ - telemetry_id: z.string().uuid(), - telemetry_manual_id: z.null() - }), - z.object({ - telemetry_id: z.null(), - telemetry_manual_id: z.string().uuid() - }) - ]) - ); -export type IAllTelemetry = z.infer; - -export const IVendorTelemetry = z.object({ - telemetry_id: z.string(), - deployment_id: z.string().uuid(), - collar_transaction_id: z.string().uuid(), - critter_id: z.string().uuid(), - deviceid: z.number(), - latitude: z.number(), - longitude: z.number(), - elevation: z.number(), - vendor: z.string(), - acquisition_date: z.string() -}); -export type IVendorTelemetry = z.infer; - -export const IManualTelemetry = z.object({ - telemetry_manual_id: z.string().uuid(), - deployment_id: z.string().uuid(), - latitude: z.number(), - longitude: z.number(), - acquisition_date: z.string() -}); -export type IManualTelemetry = z.infer; - -export interface ICreateManualTelemetry { - deployment_id: string; - latitude: number; - longitude: number; - acquisition_date: string; -} - -export class BctwTelemetryService extends BctwService { - /** - * Get all manual telemetry records - * This set of telemetry is mostly useful for testing purposes. - * - * @returns {*} IManualTelemetry[] - **/ - async getManualTelemetry(): Promise { - const res = await this.axiosInstance.get('/manual-telemetry'); - return res.data; - } - - /** - * retrieves manual telemetry from list of deployment ids - * - * @async - * @param {string[]} deployment_ids - bctw deployments - * @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 deployments - * @returns {*} IVendorTelemetry[] - */ - async getVendorTelemetryByDeploymentIds(deployment_ids: string[]): Promise { - const res = await this.axiosInstance.post('/vendor-telemetry/deployments', deployment_ids); - return res.data; - } - - /** - * retrieves manual and vendor telemetry from list of deployment ids - * - * @async - * @param {string[]} deploymentIds - bctw deployments - * @returns {*} IAllTelemetry[] - */ - async getAllTelemetryByDeploymentIds(deploymentIds: string[]): Promise { - const res = await this.axiosInstance.post('/all-telemetry/deployments', deploymentIds); - return res.data; - } - - /** - * Delete manual telemetry records by telemetry_manual_id - * Note: This is a post request that accepts an array of ids - * @param {string[]} telemetry_manual_ids - * - * @returns {*} IManualTelemetry[] - **/ - async deleteManualTelemetry(telemetry_manual_ids: string[]): Promise { - const res = await this.axiosInstance.post('/manual-telemetry/delete', telemetry_manual_ids); - return res.data; - } - - /** - * Bulk create manual telemetry records - * @param {ICreateManualTelemetry[]} payload - * - * @returns {*} IManualTelemetry[] - **/ - async createManualTelemetry(payload: ICreateManualTelemetry[]): Promise { - const res = await this.axiosInstance.post('/manual-telemetry', payload); - return res.data; - } - - /** - * Bulk update manual telemetry records - * @param {IManualTelemetry} payload - * - * @returns {*} IManualTelemetry[] - **/ - async updateManualTelemetry(payload: IManualTelemetry[]): Promise { - const res = await this.axiosInstance.patch('/manual-telemetry', payload); - return res.data; - } -} From ff76ba0eb296bc2acd336658cab54e1bf18157f1 Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Thu, 12 Dec 2024 10:27:27 -0800 Subject: [PATCH 04/13] Fix telemetry credential upload endpoint --- .../survey/{surveyId}/attachments/telemetry/index.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/telemetry/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/telemetry/index.ts index 4c93e7440b..b4e66ade26 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/telemetry/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/telemetry/index.ts @@ -1,14 +1,11 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { TELEMETRY_CREDENTIAL_ATTACHMENT_TYPE } from '../../../../../../../constants/attachments'; import { PROJECT_PERMISSION, SYSTEM_ROLE } from '../../../../../../../constants/roles'; import { getDBConnection } from '../../../../../../../database/db'; import { HTTP400 } from '../../../../../../../errors/http-error'; import { fileSchema } from '../../../../../../../openapi/schemas/file'; import { authorizeRequestHandler } from '../../../../../../../request-handlers/security/authorization'; import { AttachmentService } from '../../../../../../../services/attachment-service'; -import { BctwKeyxService } from '../../../../../../../services/bctw-service/bctw-keyx-service'; -import { getBctwUser } from '../../../../../../../services/bctw-service/bctw-service'; import { uploadFileToS3 } from '../../../../../../../utils/file-utils'; import { getLogger } from '../../../../../../../utils/logger'; import { isValidTelementryCredentialFile } from '../../../../../../../utils/media/media-utils'; @@ -157,12 +154,6 @@ export function postSurveyTelemetryCredentialAttachment(): RequestHandler { isTelemetryCredentialFile.type ); - // Upload telemetry credential file content to BCTW (for supported file types) - if (isTelemetryCredentialFile.type === TELEMETRY_CREDENTIAL_ATTACHMENT_TYPE.KEYX) { - const bctwKeyxService = new BctwKeyxService(getBctwUser(req)); - await bctwKeyxService.uploadKeyX(rawMediaFile); - } - // Upload telemetry credential file to SIMS S3 Storage const metadata = { filename: rawMediaFile.originalname, From d2ffebd25881ef67ae5db9ad96f47c5ec79b7199 Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Thu, 12 Dec 2024 10:27:38 -0800 Subject: [PATCH 05/13] Misc fix --- .../critters/{critterId}/deployments2/index.ts | 2 +- .../survey/{surveyId}/critters/{critterId}/index.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/critters/{critterId}/deployments2/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/critters/{critterId}/deployments2/index.ts index 3ae4b72625..43ecd041fa 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/critters/{critterId}/deployments2/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/critters/{critterId}/deployments2/index.ts @@ -7,7 +7,7 @@ import { TelemetryDeploymentService } from '../../../../../../../../services/tel import { getLogger } from '../../../../../../../../utils/logger'; import { numberOrNull } from '../../../../../../../../utils/string-utils'; -const defaultLog = getLogger('paths/project/{projectId}/survey/{surveyId}/critters/{critterId}/deployments'); +const defaultLog = getLogger('paths/project/{projectId}/survey/{surveyId}/critters/{critterId}/deployments2'); export const POST: Operation = [ authorizeRequestHandler((req) => { diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/critters/{critterId}/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/critters/{critterId}/index.ts index 05c7479330..21037369a6 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/critters/{critterId}/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/critters/{critterId}/index.ts @@ -5,9 +5,12 @@ import { getDBConnection } from '../../../../../../../database/db'; import { HTTP400, HTTPError, HTTPErrorType } from '../../../../../../../errors/http-error'; import { bulkUpdateResponse, critterBulkRequestObject } from '../../../../../../../openapi/schemas/critter'; import { authorizeRequestHandler } from '../../../../../../../request-handlers/security/authorization'; -import { getBctwUser } from '../../../../../../../services/bctw-service/bctw-service'; import { CritterAttachmentService } from '../../../../../../../services/critter-attachment-service'; -import { CritterbaseService, ICritterbaseUser } from '../../../../../../../services/critterbase-service'; +import { + CritterbaseService, + getCritterbaseUser, + ICritterbaseUser +} from '../../../../../../../services/critterbase-service'; import { SurveyCritterService } from '../../../../../../../services/survey-critter-service'; import { getLogger } from '../../../../../../../utils/logger'; @@ -95,7 +98,7 @@ export function updateSurveyCritter(): RequestHandler { const connection = getDBConnection(req.keycloak_token); try { await connection.open(); - const user = getBctwUser(req); + const user = getCritterbaseUser(req); if (!critterbaseCritterId) { throw new HTTPError(HTTPErrorType.BAD_REQUEST, 400, 'No external critter ID was found.'); From 793d598ee461c7d4d59e79162e49424a7873204f Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Thu, 12 Dec 2024 14:09:27 -0800 Subject: [PATCH 06/13] Fix Manual telemetry create, edit, delete. --- app/src/contexts/telemetryTableContext.tsx | 74 ++++++++++++++++--- .../telemetry/table/TelemetryTable.tsx | 14 ++-- app/src/hooks/api/useTelemetryApi.ts | 71 ++++++++++-------- .../interfaces/useTelemetryApi.interface.ts | 5 +- 4 files changed, 112 insertions(+), 52 deletions(-) diff --git a/app/src/contexts/telemetryTableContext.tsx b/app/src/contexts/telemetryTableContext.tsx index b8e4a54879..9124244d2b 100644 --- a/app/src/contexts/telemetryTableContext.tsx +++ b/app/src/contexts/telemetryTableContext.tsx @@ -2,23 +2,25 @@ import Typography from '@mui/material/Typography'; import { GridCellParams, GridColumnVisibilityModel, + GridPaginationModel, GridRowId, GridRowModes, GridRowModesModel, GridRowSelectionModel, + GridSortModel, GridValidRowModel, useGridApiRef } from '@mui/x-data-grid'; import { GridApiCommunity, GridStateColDef } from '@mui/x-data-grid/internals'; import { TelemetryTableI18N } from 'constants/i18n'; import { SIMS_TELEMETRY_HIDDEN_COLUMNS } from 'constants/session-storage'; -import { DialogContext } from 'contexts/dialogContext'; import { default as dayjs } from 'dayjs'; import { APIError } from 'hooks/api/useAxios'; import { useBiohubApi } from 'hooks/useBioHubApi'; +import { useDialogContext, useSurveyContext } from 'hooks/useContext'; import { usePersistentState } from 'hooks/usePersistentState'; import { IAllTelemetry } from 'interfaces/useTelemetryApi.interface'; -import { createContext, PropsWithChildren, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { createContext, PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { RowValidationError, TableValidationModel } from '../components/data-grid/DataGridValidationAlert'; @@ -68,9 +70,24 @@ export type IAllTelemetryTableContext = { * Reflects the total count of telemetry records for the survey */ recordCount: number; + /** + * The pagination model, which defines which telemetry records to fetch and load in the table. + */ + paginationModel: GridPaginationModel; + /** + * Sets the pagination model. + */ + setPaginationModel: (model: GridPaginationModel) => void; + /** + * The sort model, which defines how the telemetry records should be sorted. + */ + sortModel: GridSortModel; + /** + * Sets the sort model. + */ + setSortModel: (mode: GridSortModel) => void; /** * Columns hidden from table view - * */ hiddenColumns: string[]; /** @@ -159,7 +176,8 @@ export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextPr const biohubApi = useBiohubApi(); - const dialogContext = useContext(DialogContext); + const surveyContext = useSurveyContext(); + const dialogContext = useDialogContext(); // The data grid rows const [rows, setRows] = useState([]); @@ -191,6 +209,15 @@ export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextPr // Count of table records const recordCount = rows.length; + // Pagination model + const [paginationModel, setPaginationModel] = useState({ + page: 0, + pageSize: 25 + }); + + // Sort model + const [sortModel, setSortModel] = useState([{ field: 'date', sort: 'desc' }]); + // True if table has unsaved changes, deferring value to prevent ui issue with controls rendering const hasUnsavedChanges = _modifiedRowIds.current.length > 0 || _stagedRowIds.current.length > 0; @@ -421,7 +448,11 @@ export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextPr try { if (modifiedRowIdsToDelete.length) { - await biohubApi.telemetry.deleteManualTelemetry(modifiedRowIdsToDelete); + await biohubApi.telemetry.deleteManualTelemetry( + surveyContext.projectId, + surveyContext.surveyId, + modifiedRowIdsToDelete + ); } // Remove row IDs from validation model @@ -470,7 +501,7 @@ export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextPr }); } }, - [biohubApi, dialogContext] + [biohubApi.telemetry, dialogContext, surveyContext.projectId, surveyContext.surveyId] ); /** @@ -588,26 +619,29 @@ export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextPr try { // create a new records const createData = createRows.map((row) => ({ - deployment_id: String(row.deployment_id), + deployment2_id: row.deployment_id, latitude: Number(row.latitude), longitude: Number(row.longitude), - acquisition_date: dayjs(`${row.date}T${row.time}`).toISOString() + acquisition_date: dayjs(`${row.date}T${row.time}`).toISOString(), + transmission_date: null })); // update existing records const updateData = updateRows.map((row) => ({ telemetry_manual_id: String(row.id), + deployment2_id: row.deployment_id, latitude: Number(row.latitude), longitude: Number(row.longitude), - acquisition_date: dayjs(`${row.date}T${row.time}`).toISOString() + acquisition_date: dayjs(`${row.date}T${row.time}`).toISOString(), + transmission_date: null })); if (createData.length) { - await biohubApi.telemetry.createManualTelemetry(createData); + await biohubApi.telemetry.createManualTelemetry(surveyContext.projectId, surveyContext.surveyId, createData); } if (updateData.length) { - await biohubApi.telemetry.updateManualTelemetry(updateData); + await biohubApi.telemetry.updateManualTelemetry(surveyContext.projectId, surveyContext.surveyId, updateData); } revertRecords(); @@ -637,7 +671,15 @@ export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextPr _isSavingData.current = false; } }, - [dialogContext, _updateRowsMode, _isSavingData, revertRecords, refreshRecords, biohubApi] + [ + revertRecords, + dialogContext, + refreshRecords, + biohubApi.telemetry, + surveyContext.projectId, + surveyContext.surveyId, + _updateRowsMode + ] ); /** @@ -722,6 +764,10 @@ export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextPr isSaving: _isSavingData.current, validationModel, recordCount, + setPaginationModel, + paginationModel, + setSortModel, + sortModel, toggleColumnsVisibility, hiddenColumns, onRowEditStart @@ -743,6 +789,10 @@ export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextPr isLoading, validationModel, recordCount, + setPaginationModel, + paginationModel, + setSortModel, + sortModel, columnVisibilityModel, setColumnVisibilityModel, toggleColumnsVisibility, diff --git a/app/src/features/surveys/telemetry/table/TelemetryTable.tsx b/app/src/features/surveys/telemetry/table/TelemetryTable.tsx index 54f6cd8871..4f4338091e 100644 --- a/app/src/features/surveys/telemetry/table/TelemetryTable.tsx +++ b/app/src/features/surveys/telemetry/table/TelemetryTable.tsx @@ -19,7 +19,7 @@ import useDataLoader from 'hooks/useDataLoader'; import { IAnimalDeploymentWithCritter } from 'interfaces/useSurveyApi.interface'; import { useEffect, useMemo } from 'react'; -const MANUAL_TELEMETRY_TYPE = 'MANUAL'; +const MANUAL_TELEMETRY_TYPE = 'manual'; interface IManualTelemetryTableProps { isLoading: boolean; @@ -102,18 +102,18 @@ export const TelemetryTable = (props: IManualTelemetryTableProps) => { onRowEditStop={(_params, event) => { event.defaultMuiPrevented = true; }} + // Pagination + paginationMode="server" + rowCount={telemetryTableContext.recordCount} + pageSizeOptions={[25, 50, 100]} + paginationModel={telemetryTableContext.paginationModel} + onPaginationModelChange={telemetryTableContext.setPaginationModel} // Styling rowHeight={56} localeText={{ noRowsLabel: 'No Records' }} getRowHeight={() => 'auto'} - initialState={{ - pagination: { - paginationModel: { page: 0, pageSize: 25 } - } - }} - pageSizeOptions={[25, 50, 100]} slots={{ loadingOverlay: SkeletonTable }} diff --git a/app/src/hooks/api/useTelemetryApi.ts b/app/src/hooks/api/useTelemetryApi.ts index e813c8f7f7..7953a4e96b 100644 --- a/app/src/hooks/api/useTelemetryApi.ts +++ b/app/src/hooks/api/useTelemetryApi.ts @@ -5,7 +5,6 @@ import { IAllTelemetry, ICreateManualTelemetry, IFindTelemetryResponse, - IManualTelemetry, IUpdateManualTelemetry, TelemetryDeviceKeyFile, TelemetrySpatial @@ -59,21 +58,6 @@ const useTelemetryApi = (axios: AxiosInstance) => { return data; }; - /** - * Get list of manual and vendor telemetry by deployment ids - * - * @param {number[]} deploymentIds - * @return {*} {Promise} - */ - const getAllTelemetryByDeploymentIds = async (deploymentIds: number[]): Promise => { - const { data } = await axios.get('/api/telemetry/deployments', { - params: { - bctwDeploymentIds: deploymentIds - } - }); - return data; - }; - /** * Get all telemetry for a survey. * @@ -114,38 +98,60 @@ const useTelemetryApi = (axios: AxiosInstance) => { }; /** - * Bulk create Manual Telemetry + * Bulk create Manual Telemetry records. * + * @param {number} projectId + * @param {number} surveyIdF * @param {ICreateManualTelemetry[]} manualTelemetry Manual Telemetry create objects - * @return {*} {Promise} + * @return {*} {Promise} */ const createManualTelemetry = async ( + projectId: number, + surveyId: number, manualTelemetry: ICreateManualTelemetry[] - ): Promise => { - const { data } = await axios.post('/api/telemetry/manual', manualTelemetry); - return data; + ): Promise => { + await axios.post(`/api/project/${projectId}/survey/${surveyId}/deployments2/telemetry/manual`, { + telemetry: manualTelemetry + }); + + return; }; /** - * Bulk update Manual Telemetry + * Bulk update Manual Telemetry records. * + * @param {number} projectId + * @param {number} surveyId * @param {IUpdateManualTelemetry[]} manualTelemetry Manual Telemetry update objects - * @return {*} + * @return {*} {Promise} */ - const updateManualTelemetry = async (manualTelemetry: IUpdateManualTelemetry[]) => { - const { data } = await axios.patch('/api/telemetry/manual', manualTelemetry); - return data; + const updateManualTelemetry = async ( + projectId: number, + surveyId: number, + manualTelemetry: IUpdateManualTelemetry[] + ): Promise => { + await axios.put(`/api/project/${projectId}/survey/${surveyId}/deployments2/telemetry/manual`, { + telemetry: manualTelemetry + }); + + return; }; /** - * Delete manual telemetry records + * Bulk delete manual telemetry records. + * + * @param {number} projectId + * @param {number} surveyId * * @param {string[]} telemetryIds Manual Telemetry ids to delete - * @return {*} + * @return {*} {Promise} */ - const deleteManualTelemetry = async (telemetryIds: string[]) => { - const { data } = await axios.post('/api/telemetry/manual/delete', telemetryIds); - return data; + const deleteManualTelemetry = async (projectId: number, surveyId: number, telemetryIds: string[]): Promise => { + await axios.post(`/api/project/${projectId}/survey/${surveyId}/deployments2/telemetry/manual/delete`, { + telemetry_manual_ids: telemetryIds + }); + + return; }; /** @@ -183,6 +189,8 @@ const useTelemetryApi = (axios: AxiosInstance) => { /** * Begins processing an uploaded telemetry CSV for import * + * @TODO Update to use new API endpoints (bctw migration feature) + * * @param {number} submissionId * @return {*} */ @@ -245,7 +253,6 @@ const useTelemetryApi = (axios: AxiosInstance) => { return { findTelemetry, getTelemetryById, - getAllTelemetryByDeploymentIds, getTelemetryForSurvey, getTelemetrySpatialForSurvey, createManualTelemetry, diff --git a/app/src/interfaces/useTelemetryApi.interface.ts b/app/src/interfaces/useTelemetryApi.interface.ts index acecda0df9..70d24de299 100644 --- a/app/src/interfaces/useTelemetryApi.interface.ts +++ b/app/src/interfaces/useTelemetryApi.interface.ts @@ -30,15 +30,18 @@ export interface IFindTelemetryResponse { export interface IUpdateManualTelemetry { telemetry_manual_id: string; + deployment2_id: number; latitude: number; longitude: number; acquisition_date: string; + transmission_date: string | null; } export interface ICreateManualTelemetry { - deployment_id: string; + deployment2_id: number; latitude: number; longitude: number; acquisition_date: string; + transmission_date: string | null; } export interface IManualTelemetry extends ICreateManualTelemetry { From 068d15ac542ea681f789f399e77d80d7ea708e92 Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Fri, 13 Dec 2024 11:21:48 -0800 Subject: [PATCH 07/13] Update backend service/repo functions. Remove some duplicate code. --- .../survey/{surveyId}/deployments2/index.ts | 7 +- .../deployments2/{deploymentId}/index.ts | 5 + ...lemetry-deployment-repository.interface.ts | 1 + .../telemetry-deployment-repository.ts | 167 +++++++++++++----- .../telemetry-vendor-repository.ts | 14 +- api/src/repositories/telemetry-repository.ts | 156 ---------------- .../telemetry/import-telemetry-strategy.ts | 2 +- .../telemetry-deployment-service.ts | 35 ++-- .../telemetry-vendor-service.ts | 6 +- 9 files changed, 170 insertions(+), 223 deletions(-) delete mode 100644 api/src/repositories/telemetry-repository.ts diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/index.ts index 9613f7e396..6add7d7a74 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/index.ts @@ -200,6 +200,10 @@ GET.apiDoc = { nullable: true }, // device data + serial: { + type: 'string', + description: 'Serial number of the device.' + }, device_make_id: { type: 'integer', minimum: 1, @@ -269,8 +273,9 @@ export function getDeploymentsInSurvey(): RequestHandler { const telemetryDeploymentService = new TelemetryDeploymentService(connection); const [deployments, deploymentsCount] = await Promise.all([ - telemetryDeploymentService.getDeploymentsForSurveyId( + telemetryDeploymentService.getDeploymentsForSurvey( surveyId, + [], ensureCompletePaginationOptions(paginationOptions) ), telemetryDeploymentService.getDeploymentsCount(surveyId) diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/{deploymentId}/index.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/{deploymentId}/index.ts index cc73da2a45..e53593a43f 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/{deploymentId}/index.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/{deploymentId}/index.ts @@ -101,6 +101,7 @@ GET.apiDoc = { 'critterbase_end_capture_id', 'critterbase_end_mortality_id', // device data + 'serial', 'device_make_id', 'model', // critter data @@ -198,6 +199,10 @@ GET.apiDoc = { nullable: true }, // device data + serial: { + type: 'string', + description: 'Serial number of the device.' + }, device_make_id: { type: 'integer', minimum: 1, diff --git a/api/src/repositories/telemetry-repositories/telemetry-deployment-repository.interface.ts b/api/src/repositories/telemetry-repositories/telemetry-deployment-repository.interface.ts index 9ce16b78e1..86727d39d6 100644 --- a/api/src/repositories/telemetry-repositories/telemetry-deployment-repository.interface.ts +++ b/api/src/repositories/telemetry-repositories/telemetry-deployment-repository.interface.ts @@ -16,6 +16,7 @@ export type CreateDeployment = z.infer; export const ExtendedDeploymentRecord = DeploymentRecord.merge( DeviceRecord.pick({ + serial: true, device_make_id: true, model: true }).merge( diff --git a/api/src/repositories/telemetry-repositories/telemetry-deployment-repository.ts b/api/src/repositories/telemetry-repositories/telemetry-deployment-repository.ts index d250a43786..465f5fc254 100644 --- a/api/src/repositories/telemetry-repositories/telemetry-deployment-repository.ts +++ b/api/src/repositories/telemetry-repositories/telemetry-deployment-repository.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import { DeploymentRecord } from '../../database-models/deployment'; import { getKnex } from '../../database/db'; import { ApiExecuteSQLError } from '../../errors/api-error'; +import { IDeploymentAdvancedFilters } from '../../models/deployment-view'; import { ApiPaginationOptions } from '../../zod-schema/pagination'; import { BaseRepository } from '../base-repository'; import { @@ -68,71 +69,116 @@ export class TelemetryDeploymentRepository extends BaseRepository { } /** - * Get a deployment by its ID. Includes additional device and critter data. + * Retrieves the paginated list of deployments under a survey, based on the provided filter params. * - * @param {number} surveyId The survey ID - * @param {number[]} deploymentIds A list of deployment IDs + * @param {number} surveyId + * @param {number[]} [deploymentIds] + * @param {ApiPaginationOptions} [pagination] * @return {*} {Promise} * @memberof TelemetryDeploymentRepository */ - async getDeploymentsByIds(surveyId: number, deploymentIds: number[]): Promise { - const sqlStatement = SQL` - SELECT - -- deployment data - deployment2.deployment2_id, - deployment2.survey_id, - deployment2.critter_id, - deployment2.device_id, - deployment2.device_key, - deployment2.frequency, - deployment2.frequency_unit_id, - deployment2.attachment_start_date, - deployment2.attachment_start_time, - deployment2.attachment_start_timestamp, - deployment2.attachment_end_date, - deployment2.attachment_end_time, - deployment2.attachment_end_timestamp, - deployment2.critterbase_start_capture_id, - deployment2.critterbase_end_capture_id, - deployment2.critterbase_end_mortality_id, - -- device data - device.device_make_id, - device.model, - -- critter data - critter.critterbase_critter_id - FROM - deployment2 - INNER JOIN - device - ON deployment2.device_id = device.device_id - INNER JOIN - critter - ON deployment2.critter_id = critter.critter_id - WHERE - deployment2.deployment2_id = ANY (${deploymentIds}) - AND - deployment2.survey_id = ${surveyId}; - `; + async getDeploymentsForSurvey( + surveyId: number, + deploymentIds?: number[], + pagination?: ApiPaginationOptions + ): Promise { + const knex = getKnex(); - const response = await this.connection.sql(sqlStatement, ExtendedDeploymentRecord); + const queryBuilder = knex + .queryBuilder() + .select( + // deployment data + 'deployment2.deployment2_id', + 'deployment2.survey_id', + 'deployment2.critter_id', + 'deployment2.device_id', + 'deployment2.device_key', + 'deployment2.frequency', + 'deployment2.frequency_unit_id', + 'deployment2.attachment_start_date', + 'deployment2.attachment_start_time', + 'deployment2.attachment_start_timestamp', + 'deployment2.attachment_end_date', + 'deployment2.attachment_end_time', + 'deployment2.attachment_end_timestamp', + 'deployment2.critterbase_start_capture_id', + 'deployment2.critterbase_end_capture_id', + 'deployment2.critterbase_end_mortality_id', + // device data + 'device.serial', + 'device.device_make_id', + 'device.model', + // critter data + 'critter.critterbase_critter_id' + ) + .from('deployment2') + .innerJoin('survey', 'deployment2.survey_id', 'survey.survey_id') + .innerJoin('device', 'deployment2.device_id', 'device.device_id') + .innerJoin('critter', 'deployment2.critter_id', 'critter.critter_id') + .where('deployment2.survey_id', surveyId); + + if (deploymentIds?.length) { + // Filter results by deployment IDs + queryBuilder.whereIn('deployment2.deployment2_id', deploymentIds); + } + + if (pagination) { + queryBuilder.limit(pagination.limit).offset((pagination.page - 1) * pagination.limit); + + if (pagination.sort && pagination.order) { + queryBuilder.orderBy(pagination.sort, pagination.order); + } + } + + console.log(queryBuilder.toSQL().toNative().sql); + console.log(queryBuilder.toSQL().toNative().bindings); + + const response = await this.connection.knex(queryBuilder, ExtendedDeploymentRecord); return response.rows; } /** - * Get deployments for a survey ID. Includes additional device and critter data. + * Retrieves the paginated list of all deployments that are available to the user, based on their permissions and + * provided filter criteria. * - * @param {number} surveyId - * @param {ApiPaginationOptions} [pagination] + * @param {boolean} isUserAdmin Whether the user making the request is an admin + * @param {(number | null)} systemUserId The system user id of the user making the request + * @param {IDeploymentAdvancedFilters} filterFields The filter fields to apply + * @param {ApiPaginationOptions} [pagination] The pagination/sorting options to apply * @return {*} {Promise} * @memberof TelemetryDeploymentRepository */ - async getDeploymentsForSurveyId( - surveyId: number, + async findDeployments( + isUserAdmin: boolean, + systemUserId: number | null, + filterFields: IDeploymentAdvancedFilters, pagination?: ApiPaginationOptions ): Promise { const knex = getKnex(); + const getSurveyIdsQuery = knex.select(['survey_id']).from('survey'); + + // Ensure that users can only see observations that they are participating in, unless they are an administrator. + if (!isUserAdmin) { + getSurveyIdsQuery.whereIn('survey.project_id', (subqueryBuilder) => + subqueryBuilder + .select('project.project_id') + .from('project') + .leftJoin('project_participation', 'project_participation.project_id', 'project.project_id') + .where('project_participation.system_user_id', systemUserId) + ); + } + + if (filterFields.system_user_id) { + getSurveyIdsQuery.whereIn('p.project_id', (subQueryBuilder) => { + subQueryBuilder + .select('project_id') + .from('project_participation') + .where('system_user_id', filterFields.system_user_id); + }); + } + const queryBuilder = knex .queryBuilder() .select( @@ -154,15 +200,37 @@ export class TelemetryDeploymentRepository extends BaseRepository { 'deployment2.critterbase_end_capture_id', 'deployment2.critterbase_end_mortality_id', // device data + 'device.serial', 'device.device_make_id', 'device.model', // critter data 'critter.critterbase_critter_id' ) .from('deployment2') + .innerJoin('survey', 'deployment2.survey_id', 'survey.survey_id') .innerJoin('device', 'deployment2.device_id', 'device.device_id') .innerJoin('critter', 'deployment2.critter_id', 'critter.critter_id') - .where('deployment2.survey_id', surveyId); + .whereIn('deployment2.survey_id', getSurveyIdsQuery); + + if (filterFields.survey_ids?.length) { + // Filter results by survey IDs + queryBuilder.whereIn('survey.survey_id', filterFields.survey_ids); + } + + if (filterFields.deployment_ids?.length) { + // Filter results by deployment IDs + queryBuilder.whereIn('deployment2.deployment2_id', filterFields.deployment_ids); + } + + if (filterFields.system_user_id) { + // If a system user ID is provided, filter results by the projects/surveys that user has access to + queryBuilder.whereIn('survey.project_id', (subQueryBuilder) => { + subQueryBuilder + .select('project_id') + .from('project_participation') + .where('system_user_id', filterFields.system_user_id); + }); + } if (pagination) { queryBuilder.limit(pagination.limit).offset((pagination.page - 1) * pagination.limit); @@ -172,6 +240,9 @@ export class TelemetryDeploymentRepository extends BaseRepository { } } + console.log(queryBuilder.toSQL().toNative().sql); + console.log(queryBuilder.toSQL().toNative().bindings); + const response = await this.connection.knex(queryBuilder, ExtendedDeploymentRecord); return response.rows; diff --git a/api/src/repositories/telemetry-repositories/telemetry-vendor-repository.ts b/api/src/repositories/telemetry-repositories/telemetry-vendor-repository.ts index 0f50ce7a8b..56fa1145bf 100644 --- a/api/src/repositories/telemetry-repositories/telemetry-vendor-repository.ts +++ b/api/src/repositories/telemetry-repositories/telemetry-vendor-repository.ts @@ -691,7 +691,12 @@ export class TelemetryVendorRepository extends BaseRepository { queryBuilder.limit(options.pagination.limit).offset((options.pagination.page - 1) * options.pagination.limit); if (options.pagination.sort && options.pagination.order) { - queryBuilder.orderBy(options.pagination.sort, options.pagination.order); + if (options.pagination.sort === 'acquisition_time') { + // Allow sorting by acquisition_time, which is not a real database column + queryBuilder.orderByRaw(knex.raw(`acquisition_date::time ${options.pagination.order}`)); + } else { + queryBuilder.orderBy(options.pagination.sort, options.pagination.order); + } } } @@ -896,7 +901,12 @@ export class TelemetryVendorRepository extends BaseRepository { queryBuilder.limit(pagination.limit).offset((pagination.page - 1) * pagination.limit); if (pagination.sort && pagination.order) { - queryBuilder.orderBy(pagination.sort, pagination.order); + if (pagination.sort === 'acquisition_time') { + // Allow sorting by acquisition_time, which is not a real database column + queryBuilder.orderByRaw(knex.raw(`acquisition_date::time ${pagination.order}`)); + } else { + queryBuilder.orderBy(pagination.sort, pagination.order); + } } } diff --git a/api/src/repositories/telemetry-repository.ts b/api/src/repositories/telemetry-repository.ts deleted file mode 100644 index 4246e7ba9e..0000000000 --- a/api/src/repositories/telemetry-repository.ts +++ /dev/null @@ -1,156 +0,0 @@ -import SQL from 'sql-template-strings'; -import { z } from 'zod'; -import { getKnex } from '../database/db'; -import { ApiExecuteSQLError } from '../errors/api-error'; -import { getLogger } from '../utils/logger'; -import { BaseRepository } from './base-repository'; - -const defaultLog = getLogger('repositories/telemetry-repository'); - -export const Deployment = z.object({ - /** - * SIMS deployment primary ID - */ - deployment_id: z.number(), - /** - * SIMS critter primary ID - */ - critter_id: z.number(), - /** - * BCTW deployment primary ID - */ - bctw_deployment_id: z.string().uuid() -}); - -export type Deployment = z.infer; - -/** - * Interface reflecting survey telemetry retrieved from the database - */ -export const TelemetrySubmissionRecord = z.object({ - survey_telemetry_submission_id: z.number(), - survey_id: z.number(), - key: z.string(), - original_filename: z.string(), - create_date: z.string(), - create_user: z.number(), - update_date: z.string().nullable(), - update_user: z.number().nullable() -}); - -export type TelemetrySubmissionRecord = z.infer; - -export class TelemetryRepository extends BaseRepository { - async insertSurveyTelemetrySubmission( - submission_id: number, - key: string, - survey_id: number, - original_filename: string - ): Promise { - defaultLog.debug({ label: 'insertSurveyTelemetrySubmission' }); - const sqlStatement = SQL` - INSERT INTO - survey_telemetry_submission - (survey_telemetry_submission_id, key, survey_id, original_filename) - VALUES - (${submission_id}, ${key}, ${survey_id}, ${original_filename}) - RETURNING *;`; - - const response = await this.connection.sql(sqlStatement, TelemetrySubmissionRecord); - - return response.rows[0]; - } - - /** - * Retrieves the next submission ID from the survey_telemetry_submission_id_seq sequence - * - * @return {*} {Promise} - * @memberof TelemetryRepository - */ - async getNextSubmissionId(): Promise { - const sqlStatement = SQL` - SELECT nextval('biohub.survey_telemetry_submission_id_seq')::integer as survey_telemetry_submission; - `; - const response = await this.connection.sql<{ survey_telemetry_submission: number }>(sqlStatement); - return response.rows[0].survey_telemetry_submission; - } - - /** - * Retrieves the telemetry submission record by the given submission ID. - * - * @param {number} submissionId - * @return {*} {Promise} - * @memberof TelemetryRepository - */ - async getTelemetrySubmissionById(submissionId: number): Promise { - const queryBuilder = getKnex() - .queryBuilder() - .select('*') - .from('survey_telemetry_submission') - .where('survey_telemetry_submission_id', submissionId); - - const response = await this.connection.knex(queryBuilder, TelemetrySubmissionRecord); - - if (!response.rowCount) { - throw new ApiExecuteSQLError('Failed to get telemetry submission', [ - 'TelemetryRepository->getTelemetrySubmissionById', - 'rowCount was null or undefined, expected rowCount = 1' - ]); - } - - return response.rows[0]; - } - - /** - * Get deployments for the given critter ids. - * - * Note: SIMS does not store deployment information, beyond an ID. Deployment details must be fetched from the - * external BCTW API. - * - * @param {number[]} critterIds - * @return {*} {Promise} - * @memberof TelemetryRepository - */ - async getDeploymentsByCritterIds(critterIds: number[]): Promise { - const queryBuilder = getKnex() - .queryBuilder() - .select(['deployment_id', 'critter_id', 'bctw_deployment_id']) - .from('deployment') - .whereIn('critter_id', critterIds); - - const response = await this.connection.knex(queryBuilder, Deployment); - - return response.rows; - } - - /** - * Get deployments for the provided survey id. - * - * Note: SIMS does not store deployment information, beyond an ID. Deployment details must be fetched from the - * external BCTW API. - * - * @param {number} surveyId - * @return {*} {Promise} - * @memberof TelemetryRepository - */ - async getDeploymentsBySurveyId(surveyId: number): Promise { - const sqlStatement = SQL` - SELECT - deployment.deployment_id, - deployment.critter_id, - deployment.bctw_deployment_id - FROM - deployment - LEFT JOIN - critter - ON - critter.critter_id = deployment.critter_id - WHERE - critter.survey_id = ${surveyId}; - `; - - const response = await this.connection.sql(sqlStatement, Deployment); - - return response.rows; - } -} diff --git a/api/src/services/import-services/telemetry/import-telemetry-strategy.ts b/api/src/services/import-services/telemetry/import-telemetry-strategy.ts index 51eb107ba2..826e967cc8 100644 --- a/api/src/services/import-services/telemetry/import-telemetry-strategy.ts +++ b/api/src/services/import-services/telemetry/import-telemetry-strategy.ts @@ -61,7 +61,7 @@ export class ImportTelemetryStrategy extends DBService implements CSVImportStrat */ async validateRows(rows: Row[]) { const getColumnCell = generateColumnCellGetterFromColumnValidator(this.columnValidator); - const deployments = await this.telemetryVendorService.deploymentService.getDeploymentsForSurveyId(this.surveyId); + const deployments = await this.telemetryVendorService.deploymentService.getDeploymentsForSurvey(this.surveyId); const rowsToValidate: Partial[] = []; diff --git a/api/src/services/telemetry-services/telemetry-deployment-service.ts b/api/src/services/telemetry-services/telemetry-deployment-service.ts index f9b1816b17..f44562f384 100644 --- a/api/src/services/telemetry-services/telemetry-deployment-service.ts +++ b/api/src/services/telemetry-services/telemetry-deployment-service.ts @@ -1,6 +1,7 @@ import { DeploymentRecord } from '../../database-models/deployment'; import { IDBConnection } from '../../database/db'; import { ApiGeneralError } from '../../errors/api-error'; +import { IDeploymentAdvancedFilters } from '../../models/deployment-view'; import { TelemetryDeploymentRepository } from '../../repositories/telemetry-repositories/telemetry-deployment-repository'; import { CreateDeployment, @@ -46,7 +47,7 @@ export class TelemetryDeploymentService extends DBService { * @memberof TelemetryDeploymentService */ async getDeploymentById(surveyId: number, deploymentId: number): Promise { - const deployments = await this.telemetryDeploymentRepository.getDeploymentsByIds(surveyId, [deploymentId]); + const deployments = await this.telemetryDeploymentRepository.getDeploymentsForSurvey(surveyId, [deploymentId]); if (deployments.length !== 1) { throw new ApiGeneralError(`Failed to get deployment`, ['TelemetryDeploymentService->getDeploymentById']); @@ -56,30 +57,40 @@ export class TelemetryDeploymentService extends DBService { } /** - * Get deployments from a list of deployment IDs. + * Retrieves the paginated list of deployments under a survey, based on the provided filter params. * - * @param {number} surveyId The survey ID - * @param {number[]} deploymentIds A list of deployment IDs + * @param {number} surveyId + * @param {number[]} [deploymentIds] + * @param {ApiPaginationOptions} [pagination] * @return {*} {Promise} - * @memberof TelemetryDeploymentService + * @memberof TelemetryDeploymentRepository */ - async getDeploymentsByIds(surveyId: number, deploymentIds: number[]): Promise { - return this.telemetryDeploymentRepository.getDeploymentsByIds(surveyId, deploymentIds); + async getDeploymentsForSurvey( + surveyId: number, + deploymentIds?: number[], + pagination?: ApiPaginationOptions + ): Promise { + return this.telemetryDeploymentRepository.getDeploymentsForSurvey(surveyId, deploymentIds, pagination); } /** - * Get deployments for a Survey. + * Retrieves the paginated list of all deployments that are available to the user, based on their permissions and + * provided filter criteria. * - * @param {number} surveyId + * @param {boolean} isUserAdmin + * @param {(number | null)} systemUserId + * @param {IDeploymentAdvancedFilters} filterFields * @param {ApiPaginationOptions} [pagination] * @return {*} {Promise} * @memberof TelemetryDeploymentService */ - async getDeploymentsForSurveyId( - surveyId: number, + async findDeployments( + isUserAdmin: boolean, + systemUserId: number | null, + filterFields: IDeploymentAdvancedFilters, pagination?: ApiPaginationOptions ): Promise { - return this.telemetryDeploymentRepository.getDeploymentsForSurveyId(surveyId, pagination); + return this.telemetryDeploymentRepository.findDeployments(isUserAdmin, systemUserId, filterFields, pagination); } /** diff --git a/api/src/services/telemetry-services/telemetry-vendor-service.ts b/api/src/services/telemetry-services/telemetry-vendor-service.ts index 1e8de48f75..39644939f4 100644 --- a/api/src/services/telemetry-services/telemetry-vendor-service.ts +++ b/api/src/services/telemetry-services/telemetry-vendor-service.ts @@ -97,7 +97,7 @@ export class TelemetryVendorService extends DBService { * @returns {Promise<[Telemetry[], number]>} Tuple of telemetry data and total count */ async getTelemetryForSurvey(surveyId: number, options?: TelemetryOptions): Promise<[Telemetry[], number]> { - const deployments = await this.deploymentService.getDeploymentsForSurveyId(surveyId); + const deployments = await this.deploymentService.getDeploymentsForSurvey(surveyId); const deploymentIds = deployments.map((deployment) => deployment.deployment2_id); if (!options?.pagination) { @@ -119,7 +119,7 @@ export class TelemetryVendorService extends DBService { * @return {Promise<[TelemetrySpatial[], number]>} - A tuple containing the telemetry spatial data and the total count */ async getTelemetrySpatialForSurvey(surveyId: number): Promise<[TelemetrySpatial[], number]> { - const deployments = await this.deploymentService.getDeploymentsForSurveyId(surveyId); + const deployments = await this.deploymentService.getDeploymentsForSurvey(surveyId); const deploymentIds = deployments.map((deployment) => deployment.deployment2_id); const telemetry = await this.vendorRepository.getTelemetrySpatialByDeploymentIds(surveyId, deploymentIds); @@ -178,7 +178,7 @@ export class TelemetryVendorService extends DBService { */ async bulkCreateManualTelemetry(surveyId: number, telemetry: CreateManualTelemetry[]): Promise { const deploymentIds = [...new Set(telemetry.map((record) => record.deployment2_id))]; - const deployments = await this.deploymentService.getDeploymentsByIds(surveyId, deploymentIds); + const deployments = await this.deploymentService.getDeploymentsForSurvey(surveyId, deploymentIds); if (deployments.length !== deploymentIds.length) { throw new ApiGeneralError('Failed to create manual telemetry', [ From 19ddb45352a7db415d0b508daebf0d3222fcd3ac Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Fri, 13 Dec 2024 12:52:58 -0800 Subject: [PATCH 08/13] Add telemetry pagination/sorting. Add placeholder when critterbase record cant be found. --- api/src/models/deployment-view.ts | 25 +++++ app/src/contexts/observationsContext.tsx | 10 +- app/src/contexts/telemetryContext.tsx | 48 +++++++++ app/src/contexts/telemetryTableContext.tsx | 98 ++++++++++++++----- app/src/features/projects/ProjectsRouter.tsx | 5 +- app/src/features/surveys/SurveyRouter.tsx | 10 +- .../search/MeasurementsSearch.tsx | 15 +-- .../surveys/telemetry/TelemetryPage.tsx | 44 +-------- .../telemetry/list/SurveyDeploymentList.tsx | 62 +++++------- .../list/SurveyDeploymentListItem.tsx | 6 +- .../list/SurveyDeploymentListItemDetails.tsx | 13 ++- .../telemetry/table/TelemetryTable.tsx | 8 +- .../table/utils/GridColumnDefinitions.tsx | 8 +- .../survey-spatial/SurveySpatialContainer.tsx | 24 ++++- app/src/hooks/api/useTelemetryApi.ts | 7 +- .../hooks/api/useTelemetryDeploymentApi.ts | 7 +- app/src/hooks/useContext.tsx | 18 ++++ .../interfaces/useTelemetryApi.interface.ts | 6 ++ .../useTelemetryDeploymentApi.interface.ts | 9 ++ 19 files changed, 276 insertions(+), 147 deletions(-) create mode 100644 api/src/models/deployment-view.ts create mode 100644 app/src/contexts/telemetryContext.tsx diff --git a/api/src/models/deployment-view.ts b/api/src/models/deployment-view.ts new file mode 100644 index 0000000000..562a53cc1d --- /dev/null +++ b/api/src/models/deployment-view.ts @@ -0,0 +1,25 @@ +export interface IDeploymentAdvancedFilters { + /** + * Filter results by system user id. + * + * Note: This is not the id of the user making the request. + * + * @type {number} + * @memberof IAnimalAdvancedFilters + */ + system_user_id?: number; + /** + * Filter results by deployment ids. + * + * @type {number[]} + * @memberof IDeploymentAdvancedFilters + */ + deployment_ids?: number[]; + /** + * Filter results by survey ids. + * + * @type {number[]} + * @memberof IAnimalAdvancedFilters + */ + survey_ids?: number[]; +} diff --git a/app/src/contexts/observationsContext.tsx b/app/src/contexts/observationsContext.tsx index a5d92d4d86..2d469b74a2 100644 --- a/app/src/contexts/observationsContext.tsx +++ b/app/src/contexts/observationsContext.tsx @@ -1,7 +1,6 @@ import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader, { DataLoader } from 'hooks/useDataLoader'; import { IGetSurveyObservationsResponse } from 'interfaces/useObservationApi.interface'; -import { IPartialTaxonomy } from 'interfaces/useTaxonomyApi.interface'; import { createContext, PropsWithChildren, useContext } from 'react'; import { ApiPaginationRequestOptions } from 'types/misc'; import { SurveyContext } from './surveyContext'; @@ -21,10 +20,6 @@ export type IObservationsContext = { IGetSurveyObservationsResponse, unknown >; - /** - * Data Loader used for retrieving species observed in a survey - */ - observedSpeciesDataLoader: DataLoader<[], IPartialTaxonomy[], unknown>; }; export const ObservationsContext = createContext(undefined); @@ -38,11 +33,8 @@ export const ObservationsContextProvider = (props: PropsWithChildren biohubApi.observation.getObservedSpecies(projectId, surveyId)); - const observationsContext: IObservationsContext = { - observationsDataLoader, - observedSpeciesDataLoader + observationsDataLoader }; return {props.children}; diff --git a/app/src/contexts/telemetryContext.tsx b/app/src/contexts/telemetryContext.tsx new file mode 100644 index 0000000000..fa149ddb86 --- /dev/null +++ b/app/src/contexts/telemetryContext.tsx @@ -0,0 +1,48 @@ +import { SurveyContext } from 'contexts/surveyContext'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import useDataLoader, { DataLoader } from 'hooks/useDataLoader'; +import { GetSurveyTelemetryResponse } from 'interfaces/useTelemetryApi.interface'; +import { GetSurveyDeploymentsResponse } from 'interfaces/useTelemetryDeploymentApi.interface'; + +import { createContext, PropsWithChildren, useContext } from 'react'; +import { ApiPaginationRequestOptions } from 'types/misc'; + +/** + * Context object that stores information about survey telemetry + * + * @export + * @interface ITelemetryContext + */ +export type ITelemetryContext = { + /** + * Data Loader used for retrieving survey deployments records. + */ + deploymentDataLoader: DataLoader<[pagination?: ApiPaginationRequestOptions], GetSurveyDeploymentsResponse, unknown>; + /** + * Data Loader used for retrieving survey telemetry records. + */ + telemetryDataLoader: DataLoader<[pagination?: ApiPaginationRequestOptions], GetSurveyTelemetryResponse, unknown>; +}; + +export const TelemetryContext = createContext(undefined); + +export const TelemetryContextProvider = (props: PropsWithChildren>) => { + const { projectId, surveyId } = useContext(SurveyContext); + + const biohubApi = useBiohubApi(); + + const deploymentDataLoader = useDataLoader((pagination?: ApiPaginationRequestOptions) => + biohubApi.telemetryDeployment.getDeploymentsInSurvey(projectId, surveyId, pagination) + ); + + const telemetryDataLoader = useDataLoader((pagination?: ApiPaginationRequestOptions) => + biohubApi.telemetry.getTelemetryForSurvey(projectId, surveyId, pagination) + ); + + const telemetryContext: ITelemetryContext = { + deploymentDataLoader, + telemetryDataLoader + }; + + return {props.children}; +}; diff --git a/app/src/contexts/telemetryTableContext.tsx b/app/src/contexts/telemetryTableContext.tsx index 9124244d2b..646bc075ab 100644 --- a/app/src/contexts/telemetryTableContext.tsx +++ b/app/src/contexts/telemetryTableContext.tsx @@ -17,16 +17,19 @@ import { SIMS_TELEMETRY_HIDDEN_COLUMNS } from 'constants/session-storage'; import { default as dayjs } from 'dayjs'; import { APIError } from 'hooks/api/useAxios'; import { useBiohubApi } from 'hooks/useBioHubApi'; -import { useDialogContext, useSurveyContext } from 'hooks/useContext'; +import { useDialogContext, useSurveyContext, useTelemetryContext } from 'hooks/useContext'; import { usePersistentState } from 'hooks/usePersistentState'; -import { IAllTelemetry } from 'interfaces/useTelemetryApi.interface'; +import { GetSurveyTelemetryResponse } from 'interfaces/useTelemetryApi.interface'; import { createContext, PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { firstOrNull } from 'utils/Utils'; import { v4 as uuidv4 } from 'uuid'; import { RowValidationError, TableValidationModel } from '../components/data-grid/DataGridValidationAlert'; +export const MANUAL_TELEMETRY_TYPE = 'manual'; + export interface IManualTelemetryRecord { deployment_id: number; - device_id: string; + serial: string; latitude: number | null; longitude: number | null; date: string; @@ -117,7 +120,7 @@ export type IAllTelemetryTableContext = { /** * Refreshes the Telemetry Table with already existing records */ - refreshRecords: () => Promise; + refreshRecords: () => Promise; /** * The IDs of the selected telemetry table rows */ @@ -163,14 +166,10 @@ export type IAllTelemetryTableContext = { export const TelemetryTableContext = createContext(undefined); -type IAllTelemetryTableContextProviderProps = PropsWithChildren<{ - isLoading: boolean; - telemetryData: IAllTelemetry[]; - refreshRecords: () => Promise; -}>; +type IAllTelemetryTableContextProviderProps = PropsWithChildren; export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextProviderProps) => { - const { children, isLoading, telemetryData, refreshRecords } = props; + const { children } = props; const _muiDataGridApiRef = useGridApiRef(); @@ -179,6 +178,15 @@ export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextPr const surveyContext = useSurveyContext(); const dialogContext = useDialogContext(); + const { + telemetryDataLoader: { + data: telemetryData, + isLoading: isLoadingTelemetryData, + // hasLoaded: hasLoadedTelemetryData, + refresh: refreshTelemetryData + } + } = useTelemetryContext(); + // The data grid rows const [rows, setRows] = useState([]); @@ -207,7 +215,7 @@ export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextPr const _isSavingData = useRef(false); // Count of table records - const recordCount = rows.length; + const recordCount = telemetryData?.count ?? 0; // Pagination model const [paginationModel, setPaginationModel] = useState({ @@ -216,7 +224,7 @@ export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextPr }); // Sort model - const [sortModel, setSortModel] = useState([{ field: 'date', sort: 'desc' }]); + const [sortModel, setSortModel] = useState([{ field: 'acquisition_date', sort: 'desc' }]); // True if table has unsaved changes, deferring value to prevent ui issue with controls rendering const hasUnsavedChanges = _modifiedRowIds.current.length > 0 || _stagedRowIds.current.length > 0; @@ -575,7 +583,7 @@ export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextPr longitude: '' as unknown as number, date: '', time: '', - telemetry_type: 'MANUAL' + telemetry_type: MANUAL_TELEMETRY_TYPE }; // Append new record to initial rows @@ -655,7 +663,7 @@ export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextPr open: true }); - return refreshRecords(); + return refreshTelemetryData(); } catch (error) { _updateRowsMode(_modifiedRowIds.current, GridRowModes.Edit, true); const apiError = error as APIError; @@ -674,7 +682,7 @@ export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextPr [ revertRecords, dialogContext, - refreshRecords, + refreshTelemetryData, biohubApi.telemetry, surveyContext.projectId, surveyContext.surveyId, @@ -713,22 +721,60 @@ export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextPr await _saveRecords(newRows, updateRows); }, [_validateRows, _getEditedIds, _getEditedRows, _saveRecords]); + /** + * Refreshes the observations table with the latest records from the server. + * + * @return {*} + */ + const refreshTelemetryRecords = useCallback(async () => { + const sort = firstOrNull(sortModel); + + let sortField = sort?.field; + + // Convert frontend column names to the backend column names supported by the api + if (sortField === 'date') { + sortField = 'acquisition_date'; + } else if (sortField === 'time') { + sortField = 'acquisition_time'; + } else if (sortField === 'telemetry_type') { + sortField = 'vendor'; + } + + return refreshTelemetryData({ + limit: paginationModel.pageSize, + sort: sortField || undefined, + order: sort?.sort || undefined, + + // API pagination pages begin at 1, but MUI DataGrid pagination begins at 0. + page: paginationModel.page + 1 + }); + }, [paginationModel.page, paginationModel.pageSize, refreshTelemetryData, sortModel]); + + /** + * Fetch new rows based on sort/ pagination model changes + */ + useEffect(() => { + refreshTelemetryRecords(); + // Should not re-run this effect on `refreshObservationRecords` changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [paginationModel, sortModel]); + /** * Parse the telemetry data to the table format and set the rows. * */ useEffect(() => { - if (!telemetryData) { + if (!telemetryData?.telemetry) { // No telemetry data, clear the table setRows([]); return; } - const rows: IManualTelemetryTableRow[] = telemetryData.map((item) => { + const rows: IManualTelemetryTableRow[] = telemetryData.telemetry.map((item) => { return { id: item.telemetry_id, deployment_id: item.deployment_id, - device_id: item.serial, + serial: item.serial, latitude: item.latitude, longitude: item.longitude, date: dayjs(item.acquisition_date).format('YYYY-MM-DD'), @@ -752,7 +798,7 @@ export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextPr deleteRecords, deleteSelectedRecords, revertRecords, - refreshRecords, + refreshRecords: refreshTelemetryRecords, hasUnsavedChanges, rowSelectionModel, onRowSelectionModelChange: setRowSelectionModel, @@ -760,14 +806,14 @@ export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextPr onRowModesModelChange: setRowModesModel, columnVisibilityModel, onColumnVisibilityModelChange: setColumnVisibilityModel, - isLoading, + isLoading: isLoadingTelemetryData, isSaving: _isSavingData.current, validationModel, recordCount, - setPaginationModel, paginationModel, - setSortModel, + setPaginationModel, sortModel, + setSortModel, toggleColumnsVisibility, hiddenColumns, onRowEditStart @@ -782,17 +828,17 @@ export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextPr deleteRecords, deleteSelectedRecords, revertRecords, - refreshRecords, + refreshTelemetryRecords, hasUnsavedChanges, rowSelectionModel, rowModesModel, - isLoading, + isLoadingTelemetryData, validationModel, recordCount, - setPaginationModel, paginationModel, - setSortModel, + setPaginationModel, sortModel, + setSortModel, columnVisibilityModel, setColumnVisibilityModel, toggleColumnsVisibility, diff --git a/app/src/features/projects/ProjectsRouter.tsx b/app/src/features/projects/ProjectsRouter.tsx index 671a54c9e2..c12f681f05 100644 --- a/app/src/features/projects/ProjectsRouter.tsx +++ b/app/src/features/projects/ProjectsRouter.tsx @@ -1,7 +1,6 @@ import { ProjectRoleRouteGuard, SystemRoleRouteGuard } from 'components/security/RouteGuards'; import { PROJECT_PERMISSION, SYSTEM_ROLE } from 'constants/roles'; import { DialogContextProvider } from 'contexts/dialogContext'; -import { ObservationsContextProvider } from 'contexts/observationsContext'; import { ProjectAuthStateContextProvider } from 'contexts/projectAuthStateContext'; import { ProjectContextProvider } from 'contexts/projectContext'; import { SurveyContextProvider } from 'contexts/surveyContext'; @@ -93,9 +92,7 @@ const ProjectsRouter: React.FC = () => { ]} validSystemRoles={[SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.DATA_ADMINISTRATOR]}> - - - + diff --git a/app/src/features/surveys/SurveyRouter.tsx b/app/src/features/surveys/SurveyRouter.tsx index c08a416961..99cb7b9a94 100644 --- a/app/src/features/surveys/SurveyRouter.tsx +++ b/app/src/features/surveys/SurveyRouter.tsx @@ -2,6 +2,8 @@ import { ProjectRoleRouteGuard } from 'components/security/RouteGuards'; import { PROJECT_PERMISSION, SYSTEM_ROLE } from 'constants/roles'; import { AnimalPageContextProvider } from 'contexts/animalPageContext'; import { DialogContextProvider } from 'contexts/dialogContext'; +import { ObservationsContextProvider } from 'contexts/observationsContext'; +import { TelemetryContextProvider } from 'contexts/telemetryContext'; import { AnimalRouter } from 'features/surveys/animals/AnimalRouter'; import EditSurveyPage from 'features/surveys/edit/EditSurveyPage'; import { SurveyObservationPage } from 'features/surveys/observations/SurveyObservationPage'; @@ -68,7 +70,9 @@ const SurveyRouter: React.FC = () => { PROJECT_PERMISSION.OBSERVER ]} validSystemRoles={[SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.DATA_ADMINISTRATOR]}> - + + + @@ -84,7 +88,9 @@ const SurveyRouter: React.FC = () => { PROJECT_PERMISSION.OBSERVER ]} validSystemRoles={[SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.DATA_ADMINISTRATOR]}> - + + + diff --git a/app/src/features/surveys/observations/observations-table/configure-columns/components/measurements/search/MeasurementsSearch.tsx b/app/src/features/surveys/observations/observations-table/configure-columns/components/measurements/search/MeasurementsSearch.tsx index d8c2d8762d..295bb60e38 100644 --- a/app/src/features/surveys/observations/observations-table/configure-columns/components/measurements/search/MeasurementsSearch.tsx +++ b/app/src/features/surveys/observations/observations-table/configure-columns/components/measurements/search/MeasurementsSearch.tsx @@ -2,7 +2,7 @@ import green from '@mui/material/colors/green'; import ColouredRectangleChip from 'components/chips/ColouredRectangleChip'; import { MeasurementsSearchAutocomplete } from 'features/surveys/observations/observations-table/configure-columns/components/measurements/search/MeasurementsSearchAutocomplete'; import { useBiohubApi } from 'hooks/useBioHubApi'; -import { useObservationsContext, useSurveyContext } from 'hooks/useContext'; +import { useSurveyContext } from 'hooks/useContext'; import { useCritterbaseApi } from 'hooks/useCritterbaseApi'; import useDataLoader from 'hooks/useDataLoader'; import { CBMeasurementType } from 'interfaces/useCritterApi.interface'; @@ -42,9 +42,12 @@ export const MeasurementsSearch: React.FC = (props) => const critterbaseApi = useCritterbaseApi(); const surveyContext = useSurveyContext(); - const observationsContext = useObservationsContext(); const biohubApi = useBiohubApi(); + const observedSpeciesDataLoader = useDataLoader(() => + biohubApi.observation.getObservedSpecies(surveyContext.projectId, surveyContext.surveyId) + ); + const measurementsDataLoader = useDataLoader((searchTerm: string, tsns?: number[]) => critterbaseApi.xref.getMeasurementTypeDefinitionsBySearchTerm(searchTerm, tsns) ); @@ -52,14 +55,14 @@ export const MeasurementsSearch: React.FC = (props) => const hierarchyDataLoader = useDataLoader((tsns: number[]) => biohubApi.taxonomy.getTaxonHierarchyByTSNs(tsns)); useEffect(() => { - if (!observationsContext.observedSpeciesDataLoader.data) { - observationsContext.observedSpeciesDataLoader.load(); + if (!observedSpeciesDataLoader.data) { + observedSpeciesDataLoader.load(); } - }, [observationsContext.observedSpeciesDataLoader]); + }, [observedSpeciesDataLoader]); const focalOrObservedSpecies: number[] = [ ...(surveyContext.surveyDataLoader.data?.surveyData.species.focal_species.map((species) => species.tsn) ?? []), - ...(observationsContext.observedSpeciesDataLoader.data?.map((species) => species.tsn) ?? []) + ...(observedSpeciesDataLoader.data?.map((species) => species.tsn) ?? []) ]; useEffect(() => { diff --git a/app/src/features/surveys/telemetry/TelemetryPage.tsx b/app/src/features/surveys/telemetry/TelemetryPage.tsx index efcb789f48..b74122f87d 100644 --- a/app/src/features/surveys/telemetry/TelemetryPage.tsx +++ b/app/src/features/surveys/telemetry/TelemetryPage.tsx @@ -5,41 +5,12 @@ import { TelemetryTableContextProvider } from 'contexts/telemetryTableContext'; import { SurveyDeploymentList } from 'features/surveys/telemetry/list/SurveyDeploymentList'; import { TelemetryTableContainer } from 'features/surveys/telemetry/table/TelemetryTableContainer'; import { TelemetryHeader } from 'features/surveys/telemetry/TelemetryHeader'; -import { useBiohubApi } from 'hooks/useBioHubApi'; import { useProjectContext, useSurveyContext } from 'hooks/useContext'; -import useDataLoader from 'hooks/useDataLoader'; -import { useEffect } from 'react'; export const TelemetryPage = () => { - const biohubApi = useBiohubApi(); - const projectContext = useProjectContext(); const surveyContext = useSurveyContext(); - const deploymentDataLoader = useDataLoader((projectId: number, surveyId: number) => - biohubApi.telemetryDeployment.getDeploymentsInSurvey(projectId, surveyId) - ); - - const telemetryDataLoader = useDataLoader((projectId: number, surveyId: number) => - biohubApi.telemetry.getTelemetryForSurvey(projectId, surveyId) - ); - - /** - * Load the deployments and telemetry data when the page is initially loaded. - */ - useEffect(() => { - deploymentDataLoader.load(surveyContext.projectId, surveyContext.surveyId); - telemetryDataLoader.load(surveyContext.projectId, surveyContext.surveyId); - }, [deploymentDataLoader, telemetryDataLoader, surveyContext.projectId, surveyContext.surveyId]); - - /** - * Refresh the data for the telemetry page. - */ - const refreshData = async () => { - deploymentDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); - telemetryDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); - }; - if (!surveyContext.surveyDataLoader.data || !projectContext.projectDataLoader.data) { return ; } @@ -63,22 +34,11 @@ export const TelemetryPage = () => { {/* Telematry List */} - { - refreshData(); - }} - /> + {/* Telemetry Component */} - { - refreshData(); - }}> + diff --git a/app/src/features/surveys/telemetry/list/SurveyDeploymentList.tsx b/app/src/features/surveys/telemetry/list/SurveyDeploymentList.tsx index a88d1c8248..fb12ae3f97 100644 --- a/app/src/features/surveys/telemetry/list/SurveyDeploymentList.tsx +++ b/app/src/features/surveys/telemetry/list/SurveyDeploymentList.tsx @@ -19,49 +19,40 @@ import { LoadingGuard } from 'components/loading/LoadingGuard'; import { SkeletonList } from 'components/loading/SkeletonLoaders'; import { SurveyDeploymentListItem } from 'features/surveys/telemetry/list/SurveyDeploymentListItem'; import { useBiohubApi } from 'hooks/useBioHubApi'; -import { useCodesContext, useDialogContext, useSurveyContext } from 'hooks/useContext'; -import { TelemetryDeployment } from 'interfaces/useTelemetryDeploymentApi.interface'; -import { useState } from 'react'; +import { useCodesContext, useDialogContext, useSurveyContext, useTelemetryContext } from 'hooks/useContext'; +import { useEffect, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; -export interface ISurveyDeploymentListProps { - deployments: TelemetryDeployment[]; - /** - * Flag to indicate if the deployments are loading. - * - * @type {boolean} - * @memberof ISurveyDeploymentListProps - */ - isLoading: boolean; - /** - * Refresh the deployments. - * - * @memberof ISurveyDeploymentListProps - */ - refreshRecords: () => void; -} - /** * Renders a list of all deployments in the survey * * @returns {*} */ -export const SurveyDeploymentList = (props: ISurveyDeploymentListProps) => { - const { deployments, isLoading, refreshRecords } = props; - +export const SurveyDeploymentList = () => { const dialogContext = useDialogContext(); const codesContext = useCodesContext(); const surveyContext = useSurveyContext(); + const telemetryContext = useTelemetryContext(); const biohubApi = useBiohubApi(); + const deploymentDataLoader = telemetryContext.deploymentDataLoader; + const [bulkDeploymentAnchorEl, setBulkDeploymentAnchorEl] = useState(null); const [deploymentAnchorEl, setDeploymentAnchorEl] = useState(null); const [checkboxSelectedIds, setCheckboxSelectedIds] = useState([]); const [selectedDeploymentId, setSelectedDeploymentId] = useState(); - const deploymentCount = deployments?.length ?? 0; + const deployments = deploymentDataLoader.data?.deployments ?? []; + const deploymentsCount = deploymentDataLoader.data?.count ?? 0; + + /** + * Load the deployments and telemetry data when the page is initially loaded. + */ + useEffect(() => { + deploymentDataLoader.load(); + }, [deploymentDataLoader]); const handleBulkActionMenuClick = (event: React.MouseEvent) => { setBulkDeploymentAnchorEl(event.currentTarget); @@ -102,7 +93,7 @@ export const SurveyDeploymentList = (props: ISurveyDeploymentListProps) => { .then(() => { dialogContext.setYesNoDialog({ open: false }); setBulkDeploymentAnchorEl(null); - refreshRecords(); + deploymentDataLoader.refresh(); }) .catch((error: any) => { dialogContext.setYesNoDialog({ open: false }); @@ -132,7 +123,7 @@ export const SurveyDeploymentList = (props: ISurveyDeploymentListProps) => { .then(() => { dialogContext.setYesNoDialog({ open: false }); setDeploymentAnchorEl(null); - refreshRecords(); + deploymentDataLoader.refresh(); }) .catch((error: any) => { dialogContext.setYesNoDialog({ open: false }); @@ -296,7 +287,7 @@ export const SurveyDeploymentList = (props: ISurveyDeploymentListProps) => { Deployments ‌ - ({deploymentCount}) + ({deploymentsCount}) @@ -322,10 +313,13 @@ export const SurveyDeploymentList = (props: ISurveyDeploymentListProps) => { } isLoadingFallbackDelay={100} - hasNoData={!deploymentCount} + hasNoData={!deploymentsCount} hasNoDataFallback={ { sx={{ mr: 0.75 }} - checked={checkboxSelectedIds.length > 0 && checkboxSelectedIds.length === deploymentCount} + checked={checkboxSelectedIds.length > 0 && checkboxSelectedIds.length === deploymentsCount} indeterminate={ - checkboxSelectedIds.length >= 1 && checkboxSelectedIds.length < deploymentCount + checkboxSelectedIds.length >= 1 && checkboxSelectedIds.length < deploymentsCount } onClick={() => { - if (checkboxSelectedIds.length === deploymentCount) { + if (checkboxSelectedIds.length === deploymentsCount) { // Unselect all setCheckboxSelectedIds([]); return; @@ -396,10 +390,6 @@ export const SurveyDeploymentList = (props: ISurveyDeploymentListProps) => { (animal) => animal.critterbase_critter_id === deployment.critterbase_critter_id ); - if (!animal) { - return null; - } - // Replace the deployment frequency_unit IDs with their human readable codes const hydratedDeployment = { ...deployment, diff --git a/app/src/features/surveys/telemetry/list/SurveyDeploymentListItem.tsx b/app/src/features/surveys/telemetry/list/SurveyDeploymentListItem.tsx index e590bd11ca..3d6ba693d2 100644 --- a/app/src/features/surveys/telemetry/list/SurveyDeploymentListItem.tsx +++ b/app/src/features/surveys/telemetry/list/SurveyDeploymentListItem.tsx @@ -18,7 +18,7 @@ import { ICritterSimpleResponse } from 'interfaces/useCritterApi.interface'; import { TelemetryDeployment } from 'interfaces/useTelemetryDeploymentApi.interface'; export interface ISurveyDeploymentListItemProps { - animal: ICritterSimpleResponse; + animal?: ICritterSimpleResponse; deployment: Omit & { frequency_unit: string | null }; isChecked: boolean; handleDeploymentMenuClick: (event: React.MouseEvent, deploymentId: number) => void; @@ -101,14 +101,14 @@ export const SurveyDeploymentListItem = (props: ISurveyDeploymentListItemProps) overflow: 'hidden', textOverflow: 'ellipsis' }}> - {deployment.device_id} + {deployment.serial} {deployment.frequency} {deployment.frequency_unit} - {animal.animal_id} + {`${deployment.deployment2_id}: ${animal?.animal_id || 'Unknown'}`} diff --git a/app/src/features/surveys/telemetry/list/SurveyDeploymentListItemDetails.tsx b/app/src/features/surveys/telemetry/list/SurveyDeploymentListItemDetails.tsx index 0fd65bd05f..5c232a457b 100644 --- a/app/src/features/surveys/telemetry/list/SurveyDeploymentListItemDetails.tsx +++ b/app/src/features/surveys/telemetry/list/SurveyDeploymentListItemDetails.tsx @@ -49,10 +49,21 @@ export const SurveyDeploymentListItemDetails = (props: ISurveyDeploymentListItem const endDateFormatted = endDate ? dayjs(endDate).format(DATE_FORMAT.MediumDateFormat) : null; - if (!startCaptureDataLoader.data) { + if (startCaptureDataLoader.isLoading || !startCaptureDataLoader.isReady) { return ; } + if (!startCaptureDataLoader.data) { + // A Critterbase capture record could not be fetched, or does not exist (which should not happen) + return ( + + + {'Could not load animal capture data.'} + + + ); + } + const startDate = dayjs(startCaptureDataLoader.data.capture_date).format(DATE_FORMAT.MediumDateFormat); const startTime = startCaptureDataLoader.data.capture_time; diff --git a/app/src/features/surveys/telemetry/table/TelemetryTable.tsx b/app/src/features/surveys/telemetry/table/TelemetryTable.tsx index 4f4338091e..73a10d9a1d 100644 --- a/app/src/features/surveys/telemetry/table/TelemetryTable.tsx +++ b/app/src/features/surveys/telemetry/table/TelemetryTable.tsx @@ -7,7 +7,7 @@ import { GenericTimeColDef } from 'components/data-grid/GenericGridColumnDefinitions'; import { SkeletonTable } from 'components/loading/SkeletonLoaders'; -import { IManualTelemetryTableRow } from 'contexts/telemetryTableContext'; +import { IManualTelemetryTableRow, MANUAL_TELEMETRY_TYPE } from 'contexts/telemetryTableContext'; import { DeploymentColDef, DeviceColDef, @@ -19,8 +19,6 @@ import useDataLoader from 'hooks/useDataLoader'; import { IAnimalDeploymentWithCritter } from 'interfaces/useSurveyApi.interface'; import { useEffect, useMemo } from 'react'; -const MANUAL_TELEMETRY_TYPE = 'manual'; - interface IManualTelemetryTableProps { isLoading: boolean; } @@ -108,6 +106,10 @@ export const TelemetryTable = (props: IManualTelemetryTableProps) => { pageSizeOptions={[25, 50, 100]} paginationModel={telemetryTableContext.paginationModel} onPaginationModelChange={telemetryTableContext.setPaginationModel} + // Sorting + sortingMode="server" + sortModel={telemetryTableContext.sortModel} + onSortModelChange={telemetryTableContext.setSortModel} // Styling rowHeight={56} localeText={{ diff --git a/app/src/features/surveys/telemetry/table/utils/GridColumnDefinitions.tsx b/app/src/features/surveys/telemetry/table/utils/GridColumnDefinitions.tsx index 04d0c979b9..2005be72ad 100644 --- a/app/src/features/surveys/telemetry/table/utils/GridColumnDefinitions.tsx +++ b/app/src/features/surveys/telemetry/table/utils/GridColumnDefinitions.tsx @@ -42,7 +42,7 @@ export const DeploymentColDef = (props: { dataGridProps={params} options={props.critterDeployments.map((item) => { return { - label: `${item.critter.animal_id}: ${item.deployment.device_id}`, + label: `${item.deployment.deployment2_id}: ${item.critter.animal_id}`, value: item.deployment.deployment2_id }; })} @@ -57,7 +57,7 @@ export const DeploymentColDef = (props: { dataGridProps={params} options={props.critterDeployments.map((item) => ({ - label: `${item.critter.animal_id}: ${item.deployment.device_id}`, + label: `${item.deployment.deployment2_id}: ${item.critter.animal_id}`, value: item.deployment.deployment2_id }))} error={error} @@ -71,7 +71,7 @@ export const DeviceColDef = (props: { critterDeployments: IAnimalDeploymentWithCritter[]; }): GridColDef => { return { - field: 'device_id', + field: 'serial', headerName: 'Device', hideable: true, minWidth: 120, @@ -83,7 +83,7 @@ export const DeviceColDef = (props: { { props.critterDeployments.find( (deployment) => deployment.deployment.deployment2_id === params.row.deployment_id - )?.deployment.device_id + )?.deployment.serial } ) diff --git a/app/src/features/surveys/view/survey-spatial/SurveySpatialContainer.tsx b/app/src/features/surveys/view/survey-spatial/SurveySpatialContainer.tsx index e64444ed3c..15d10dc42b 100644 --- a/app/src/features/surveys/view/survey-spatial/SurveySpatialContainer.tsx +++ b/app/src/features/surveys/view/survey-spatial/SurveySpatialContainer.tsx @@ -6,9 +6,12 @@ import { SurveySpatialToolbar } from 'features/surveys/view/survey-spatial/components/SurveySpatialToolbar'; import { SurveySpatialTelemetry } from 'features/surveys/view/survey-spatial/components/telemetry/SurveySpatialTelemetry'; -import { useObservationsContext, useTaxonomyContext } from 'hooks/useContext'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import { useSurveyContext, useTaxonomyContext } from 'hooks/useContext'; +import useDataLoader from 'hooks/useDataLoader'; import { isEqual } from 'lodash-es'; import { useEffect, useMemo, useState } from 'react'; +import { ApiPaginationRequestOptions } from 'types/misc'; import { useSamplingSiteStaticLayer } from './components/map/useSamplingSiteStaticLayer'; import { useStudyAreaStaticLayer } from './components/map/useStudyAreaStaticLayer'; @@ -20,9 +23,15 @@ import { useStudyAreaStaticLayer } from './components/map/useStudyAreaStaticLaye * @returns {JSX.Element} The rendered component. */ export const SurveySpatialContainer = (): JSX.Element => { - const observationsContext = useObservationsContext(); + const surveyContext = useSurveyContext(); const taxonomyContext = useTaxonomyContext(); + const biohubApi = useBiohubApi(); + + const observationsDataLoader = useDataLoader((pagination?: ApiPaginationRequestOptions) => + biohubApi.observation.getObservationRecords(surveyContext.projectId, surveyContext.surveyId, pagination) + ); + const [activeView, setActiveView] = useState(SurveySpatialDatasetViewEnum.OBSERVATIONS); const studyAreaStaticLayer = useStudyAreaStaticLayer(); @@ -33,13 +42,18 @@ export const SurveySpatialContainer = (): JSX.Element => { [samplingSiteStaticLayer, studyAreaStaticLayer] ); + useEffect(() => { + // Load the observations data + observationsDataLoader.load(); + }, [observationsDataLoader]); + // Fetch and cache all taxonomic data required for the observations. useEffect(() => { const cacheTaxonomicData = async () => { - if (observationsContext.observationsDataLoader.data) { + if (observationsDataLoader.data) { // Fetch all unique ITIS TSNs from observations to retrieve taxonomic names const taxonomicIds = [ - ...new Set(observationsContext.observationsDataLoader.data.surveyObservations.map((item) => item.itis_tsn)) + ...new Set(observationsDataLoader.data.surveyObservations.map((item) => item.itis_tsn)) ].filter((tsn): tsn is number => tsn !== null); await taxonomyContext.cacheSpeciesTaxonomyByIds(taxonomicIds); @@ -49,7 +63,7 @@ export const SurveySpatialContainer = (): JSX.Element => { cacheTaxonomicData(); // Should not re-run this effect on `taxonomyContext` changes // eslint-disable-next-line react-hooks/exhaustive-deps - }, [observationsContext.observationsDataLoader.data]); + }, [observationsDataLoader.data]); return ( <> diff --git a/app/src/hooks/api/useTelemetryApi.ts b/app/src/hooks/api/useTelemetryApi.ts index 7953a4e96b..905232931b 100644 --- a/app/src/hooks/api/useTelemetryApi.ts +++ b/app/src/hooks/api/useTelemetryApi.ts @@ -2,6 +2,7 @@ import { AxiosInstance, AxiosProgressEvent, CancelTokenSource } from 'axios'; import { IAllTelemetryAdvancedFilters } from 'features/summary/tabular-data/telemetry/TelemetryListFilterForm'; import { IUploadAttachmentResponse } from 'interfaces/useProjectApi.interface'; import { + GetSurveyTelemetryResponse, IAllTelemetry, ICreateManualTelemetry, IFindTelemetryResponse, @@ -10,7 +11,7 @@ import { TelemetrySpatial } from 'interfaces/useTelemetryApi.interface'; import qs from 'qs'; -import { ApiPaginationRequestOptions, ApiPaginationResponseParams } from 'types/misc'; +import { ApiPaginationRequestOptions } from 'types/misc'; /** * Returns a set of supported api methods for working with telemetry. @@ -64,13 +65,13 @@ const useTelemetryApi = (axios: AxiosInstance) => { * @param {number} projectId * @param {number} surveyId * @param {ApiPaginationRequestOptions} [pagination] - * @return {*} {Promise<{ telemetry: IAllTelemetry[]; count: number; pagination: ApiPaginationResponseParams }>} + * @return {*} {Promise} */ const getTelemetryForSurvey = async ( projectId: number, surveyId: number, pagination?: ApiPaginationRequestOptions - ): Promise<{ telemetry: IAllTelemetry[]; count: number; pagination: ApiPaginationResponseParams }> => { + ): Promise => { const { data } = await axios.get(`/api/project/${projectId}/survey/${surveyId}/telemetry`, { params: { ...pagination diff --git a/app/src/hooks/api/useTelemetryDeploymentApi.ts b/app/src/hooks/api/useTelemetryDeploymentApi.ts index a564ab930f..c9e05361d4 100644 --- a/app/src/hooks/api/useTelemetryDeploymentApi.ts +++ b/app/src/hooks/api/useTelemetryDeploymentApi.ts @@ -1,11 +1,12 @@ import { AxiosInstance } from 'axios'; import { CreateTelemetryDeployment, + GetSurveyDeploymentsResponse, TelemetryDeployment, UpdateTelemetryDeployment } from 'interfaces/useTelemetryDeploymentApi.interface'; import qs from 'qs'; -import { ApiPaginationRequestOptions, ApiPaginationResponseParams } from 'types/misc'; +import { ApiPaginationRequestOptions } from 'types/misc'; /** * Returns a set of supported api methods for working with telemetry deployments. @@ -84,13 +85,13 @@ export const useTelemetryDeploymentApi = (axios: AxiosInstance) => { * @param {number} projectId * @param {number} surveyId * @param {ApiPaginationRequestOptions} [pagination] - * @return {*} {Promise<{ deployments: TelemetryDeployment[]; count: number; pagination: ApiPaginationResponseParams }>} + * @return {*} {Promise} */ const getDeploymentsInSurvey = async ( projectId: number, surveyId: number, pagination?: ApiPaginationRequestOptions - ): Promise<{ deployments: TelemetryDeployment[]; count: number; pagination: ApiPaginationResponseParams }> => { + ): Promise => { const { data } = await axios.get(`/api/project/${projectId}/survey/${surveyId}/deployments2`, { params: { ...pagination diff --git a/app/src/hooks/useContext.tsx b/app/src/hooks/useContext.tsx index d371a965b9..64b90c8054 100644 --- a/app/src/hooks/useContext.tsx +++ b/app/src/hooks/useContext.tsx @@ -8,6 +8,7 @@ import { IObservationsTableContext, ObservationsTableContext } from 'contexts/ob import { IProjectContext, ProjectContext } from 'contexts/projectContext'; import { ISurveyContext, SurveyContext } from 'contexts/surveyContext'; import { ITaxonomyContext, TaxonomyContext } from 'contexts/taxonomyContext'; +import { ITelemetryContext, TelemetryContext } from 'contexts/telemetryContext'; import { IAllTelemetryTableContext, TelemetryTableContext } from 'contexts/telemetryTableContext'; import { useContext } from 'react'; @@ -197,3 +198,20 @@ export const useAnimalPageContext = (): IAnimalPageContext => { return context; }; + +/** + * Returns an instance of `ITelemetryContext` from `TelemetryContext`. + * + * @return {*} {ITelemetryContext} + */ +export const useTelemetryContext = (): ITelemetryContext => { + const context = useContext(TelemetryContext); + + if (!context) { + throw Error( + 'ObservationsContext is undefined, please verify you are calling useObservationsContext() as child of an component.' + ); + } + + return context; +}; diff --git a/app/src/interfaces/useTelemetryApi.interface.ts b/app/src/interfaces/useTelemetryApi.interface.ts index 70d24de299..a8e8f73426 100644 --- a/app/src/interfaces/useTelemetryApi.interface.ts +++ b/app/src/interfaces/useTelemetryApi.interface.ts @@ -209,3 +209,9 @@ export type TelemetrySpatial = { */ geometry: Point | null; }; + +export type GetSurveyTelemetryResponse = { + telemetry: IAllTelemetry[]; + count: number; + pagination: ApiPaginationResponseParams; +}; diff --git a/app/src/interfaces/useTelemetryDeploymentApi.interface.ts b/app/src/interfaces/useTelemetryDeploymentApi.interface.ts index 4e4f22cd00..132423c931 100644 --- a/app/src/interfaces/useTelemetryDeploymentApi.interface.ts +++ b/app/src/interfaces/useTelemetryDeploymentApi.interface.ts @@ -1,3 +1,5 @@ +import { ApiPaginationResponseParams } from 'types/misc'; + /** * Create telemetry deployment record. */ @@ -53,8 +55,15 @@ export type TelemetryDeployment = { critterbase_end_capture_id: string | null; critterbase_end_mortality_id: string | null; // device data + serial: string; device_make_id: number; model: string | null; // critter data critterbase_critter_id: string; }; + +export type GetSurveyDeploymentsResponse = { + deployments: TelemetryDeployment[]; + count: number; + pagination: ApiPaginationResponseParams; +}; From f8a0dcede6a2dcf41bb9506ad6a35bab39aad1ac Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Fri, 13 Dec 2024 13:15:52 -0800 Subject: [PATCH 09/13] Disable telemetry import button --- .../surveys/telemetry/table/TelemetryTableContainer.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx b/app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx index 407b201ede..5007374ba3 100644 --- a/app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx +++ b/app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx @@ -149,6 +149,8 @@ export const TelemetryTableContainer = () => { variant="contained" color="primary" startIcon={} + // TODO: Disabled while the backend CSV Import code is being refactored (https://apps.nrs.gov.bc.ca/int/jira/browse/SIMSBIOHUB-652) + disabled={true} onClick={() => setShowImportDialog(true)}> Import From f82a406a568b513905c7030954896f985e3111a8 Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Fri, 13 Dec 2024 15:12:16 -0800 Subject: [PATCH 10/13] Fix Tests --- .../attachments/telemetry/index.test.ts | 53 ++-------------- .../{surveyId}/deployments2/index.test.ts | 5 +- .../deployments2/{deploymentId}/index.test.ts | 1 + .../telemetry-deployment-repository.test.ts | 61 ++++++++++--------- .../import-telemetry-strategy.test.ts | 18 +++--- .../telemetry-vendor-service.test.ts | 11 ++-- 6 files changed, 53 insertions(+), 96 deletions(-) diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/telemetry/index.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/telemetry/index.test.ts index d0db0b5aa2..a651ac688e 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/telemetry/index.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/telemetry/index.test.ts @@ -7,7 +7,6 @@ import * as db from '../../../../../../../database/db'; import { HTTPError } from '../../../../../../../errors/http-error'; import { SurveyTelemetryCredentialAttachment } from '../../../../../../../repositories/attachment-repository'; import { AttachmentService } from '../../../../../../../services/attachment-service'; -import { BctwKeyxService } from '../../../../../../../services/bctw-service/bctw-keyx-service'; import * as file_utils from '../../../../../../../utils/file-utils'; import { KeycloakUserInformation } from '../../../../../../../utils/keycloak-utils'; import { getMockDBConnection, getRequestHandlerMocks } from '../../../../../../../__mocks__/db'; @@ -53,7 +52,7 @@ describe('postSurveyTelemetryCredentialAttachment', () => { } }); - it('succeeds and uploads a KeyX file to BCTW', async () => { + it('successfully imports a credential file', async () => { const dbConnectionObj = getMockDBConnection(); sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); @@ -63,47 +62,6 @@ describe('postSurveyTelemetryCredentialAttachment', () => { const uploadFileToS3Stub = sinon.stub(file_utils, 'uploadFileToS3').resolves(); - const uploadKeyXStub = sinon.stub(BctwKeyxService.prototype, 'uploadKeyX').resolves(); - - const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - - mockReq.keycloak_token = {} as KeycloakUserInformation; - mockReq.params = { - projectId: '1', - surveyId: '2' - }; - mockReq.files = [ - { - fieldname: 'media', - originalname: 'test.keyx', - encoding: '7bit', - mimetype: 'text/plain', - size: 340 - } - ] as Express.Multer.File[]; - - const requestHandler = postSurveyTelemetryCredentialAttachment(); - - await requestHandler(mockReq, mockRes, mockNext); - - expect(mockRes.jsonValue).to.eql({ survey_telemetry_credential_attachment_id: 44 }); - expect(upsertSurveyTelemetryCredentialAttachmentStub).to.be.calledOnce; - expect(uploadKeyXStub).to.be.calledOnce; - expect(uploadFileToS3Stub).to.be.calledOnce; - }); - - it('succeeds and does not upload a Cfg file to BCTW', async () => { - const dbConnectionObj = getMockDBConnection(); - sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - - const upsertSurveyTelemetryCredentialAttachmentStub = sinon - .stub(AttachmentService.prototype, 'upsertSurveyTelemetryCredentialAttachment') - .resolves({ survey_telemetry_credential_attachment_id: 44, key: 'path/to/file/test.keyx' }); - - const uploadFileToS3Stub = sinon.stub(file_utils, 'uploadFileToS3').resolves(); - - const uploadKeyXStub = sinon.stub(BctwKeyxService.prototype, 'uploadKeyX').resolves(); - const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); mockReq.keycloak_token = {} as KeycloakUserInformation; @@ -127,7 +85,6 @@ describe('postSurveyTelemetryCredentialAttachment', () => { expect(mockRes.jsonValue).to.eql({ survey_telemetry_credential_attachment_id: 44 }); expect(upsertSurveyTelemetryCredentialAttachmentStub).to.be.calledOnce; - expect(uploadKeyXStub).not.to.be.called; // not called expect(uploadFileToS3Stub).to.be.calledOnce; }); @@ -135,12 +92,11 @@ describe('postSurveyTelemetryCredentialAttachment', () => { const dbConnectionObj = getMockDBConnection(); sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + const mockError = new Error('A test error'); + const upsertSurveyTelemetryCredentialAttachmentStub = sinon .stub(AttachmentService.prototype, 'upsertSurveyTelemetryCredentialAttachment') - .resolves({ survey_telemetry_credential_attachment_id: 44, key: 'path/to/file/test.keyx' }); - - const mockError = new Error('A test error'); - const uploadKeyXStub = sinon.stub(BctwKeyxService.prototype, 'uploadKeyX').rejects(mockError); + .rejects(mockError); const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); @@ -168,7 +124,6 @@ describe('postSurveyTelemetryCredentialAttachment', () => { expect((actualError as HTTPError).message).to.equal(mockError.message); expect(upsertSurveyTelemetryCredentialAttachmentStub).to.have.been.calledOnce; - expect(uploadKeyXStub).to.have.been.calledOnce; } }); }); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/index.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/index.test.ts index 0812df9173..a9871858ce 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/index.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/index.test.ts @@ -34,6 +34,7 @@ describe('getDeploymentsInSurvey', () => { critterbase_end_capture_id: null, critterbase_end_mortality_id: null, // device data + serial: '1234', device_make_id: 1, model: 'ModelX', // critter data @@ -41,7 +42,7 @@ describe('getDeploymentsInSurvey', () => { } ]; - sinon.stub(TelemetryDeploymentService.prototype, 'getDeploymentsForSurveyId').resolves(mockDeployments); + sinon.stub(TelemetryDeploymentService.prototype, 'getDeploymentsForSurvey').resolves(mockDeployments); sinon.stub(TelemetryDeploymentService.prototype, 'getDeploymentsCount').resolves(1); const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); @@ -78,7 +79,7 @@ describe('getDeploymentsInSurvey', () => { const mockError = new Error('Test error'); const getDeploymentsForSurveyIdStub = sinon - .stub(TelemetryDeploymentService.prototype, 'getDeploymentsForSurveyId') + .stub(TelemetryDeploymentService.prototype, 'getDeploymentsForSurvey') .rejects(mockError); const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/{deploymentId}/index.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/{deploymentId}/index.test.ts index 29d4bc5329..655f2bfb89 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/{deploymentId}/index.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/deployments2/{deploymentId}/index.test.ts @@ -31,6 +31,7 @@ describe('getDeploymentById', () => { critterbase_start_capture_id: '123-456-789', critterbase_end_capture_id: null, critterbase_end_mortality_id: null, + serial: '123', device_make_id: 1, critterbase_critter_id: 'uuid', model: 'model' diff --git a/api/src/repositories/telemetry-repositories/telemetry-deployment-repository.test.ts b/api/src/repositories/telemetry-repositories/telemetry-deployment-repository.test.ts index 135617e281..ca08297dd5 100644 --- a/api/src/repositories/telemetry-repositories/telemetry-deployment-repository.test.ts +++ b/api/src/repositories/telemetry-repositories/telemetry-deployment-repository.test.ts @@ -78,8 +78,8 @@ describe('TelemetryDeploymentRepository', () => { }); }); - describe('getDeploymentsByIds', () => { - it('should get a deployment by ID successfully', async () => { + describe('getDeploymentsForSurvey', () => { + it('should get deployments by survey ID successfully', async () => { const mockDeploymentRecord = { deployment2_id: 1, survey_id: 1, @@ -94,6 +94,7 @@ describe('TelemetryDeploymentRepository', () => { critterbase_start_capture_id: '123-456-789', critterbase_end_capture_id: null, critterbase_end_mortality_id: null, + serial: '1234', device_make_id: 1, model: 'Model', critterbase_critter_id: 1 @@ -104,42 +105,18 @@ describe('TelemetryDeploymentRepository', () => { rows: [mockDeploymentRecord] } as any as Promise>; - const mockDbConnection = getMockDBConnection({ sql: sinon.stub().resolves(mockResponse) }); + const mockDbConnection = getMockDBConnection({ knex: sinon.stub().resolves(mockResponse) }); const telemetryDeploymentRepository = new TelemetryDeploymentRepository(mockDbConnection); const surveyId = 1; - const deploymentId = 2; - const response = await telemetryDeploymentRepository.getDeploymentsByIds(surveyId, [deploymentId]); + const response = await telemetryDeploymentRepository.getDeploymentsForSurvey(surveyId); expect(response).to.eql([mockDeploymentRecord]); }); - it('should throw an error if the deployment is not found', async () => { - const mockResponse = { - rowCount: 0, - rows: [] - } as any as Promise>; - - const mockDbConnection = getMockDBConnection({ sql: sinon.stub().resolves(mockResponse) }); - - const telemetryDeploymentRepository = new TelemetryDeploymentRepository(mockDbConnection); - - const surveyId = 1; - const deploymentId = 2; - - try { - await telemetryDeploymentRepository.getDeploymentsByIds(surveyId, [deploymentId]); - } catch (error) { - expect(error).to.be.instanceOf(ApiExecuteSQLError); - expect((error as ApiExecuteSQLError).message).to.equal('Failed to get deployment'); - } - }); - }); - - describe('getDeploymentsForSurveyId', () => { - it('should get deployments by survey ID successfully', async () => { + it('should get a deployment by ID successfully', async () => { const mockDeploymentRecord = { deployment2_id: 1, survey_id: 1, @@ -154,6 +131,7 @@ describe('TelemetryDeploymentRepository', () => { critterbase_start_capture_id: '123-456-789', critterbase_end_capture_id: null, critterbase_end_mortality_id: null, + serial: '1234', device_make_id: 1, model: 'Model', critterbase_critter_id: 1 @@ -169,11 +147,33 @@ describe('TelemetryDeploymentRepository', () => { const telemetryDeploymentRepository = new TelemetryDeploymentRepository(mockDbConnection); const surveyId = 1; + const deploymentId = 2; - const response = await telemetryDeploymentRepository.getDeploymentsForSurveyId(surveyId); + const response = await telemetryDeploymentRepository.getDeploymentsForSurvey(surveyId, [deploymentId]); expect(response).to.eql([mockDeploymentRecord]); }); + + it('should throw an error if the deployment is not found', async () => { + const mockResponse = { + rowCount: 0, + rows: [] + } as any as Promise>; + + const mockDbConnection = getMockDBConnection({ knex: sinon.stub().resolves(mockResponse) }); + + const telemetryDeploymentRepository = new TelemetryDeploymentRepository(mockDbConnection); + + const surveyId = 1; + const deploymentId = 2; + + try { + await telemetryDeploymentRepository.getDeploymentsForSurvey(surveyId, [deploymentId]); + } catch (error) { + expect(error).to.be.instanceOf(ApiExecuteSQLError); + expect((error as ApiExecuteSQLError).message).to.equal('Failed to get deployment'); + } + }); }); describe('getDeploymentsForCritterId', () => { @@ -192,6 +192,7 @@ describe('TelemetryDeploymentRepository', () => { critterbase_start_capture_id: '123-456-789', critterbase_end_capture_id: null, critterbase_end_mortality_id: null, + serial: '1234', device_make_id: 1, model: 'Model', critterbase_critter_id: 1 diff --git a/api/src/services/import-services/telemetry/import-telemetry-strategy.test.ts b/api/src/services/import-services/telemetry/import-telemetry-strategy.test.ts index 5235da2103..5013245cb0 100644 --- a/api/src/services/import-services/telemetry/import-telemetry-strategy.test.ts +++ b/api/src/services/import-services/telemetry/import-telemetry-strategy.test.ts @@ -43,16 +43,14 @@ describe('import-telemetry-strategy', () => { sinon.stub(worksheetUtils, 'getDefaultWorksheet').returns(worksheet); - sinon - .stub(importTelemetryStrategy.telemetryVendorService.deploymentService, 'getDeploymentsForSurveyId') - .resolves([ - { - deployment2_id: 1, - device_key: 'lotek:1234', - attachment_start_timestamp: '2024-10-21 10:10:10', - attachment_end_timestamp: null - } - ] as any); + sinon.stub(importTelemetryStrategy.telemetryVendorService.deploymentService, 'getDeploymentsForSurvey').resolves([ + { + deployment2_id: 1, + device_key: 'lotek:1234', + attachment_start_timestamp: '2024-10-21 10:10:10', + attachment_end_timestamp: null + } + ] as any); sinon.stub(importTelemetryStrategy.telemetryVendorService, 'bulkCreateManualTelemetry').resolves(); diff --git a/api/src/services/telemetry-services/telemetry-vendor-service.test.ts b/api/src/services/telemetry-services/telemetry-vendor-service.test.ts index 8362f1774c..6ddc1e12c9 100644 --- a/api/src/services/telemetry-services/telemetry-vendor-service.test.ts +++ b/api/src/services/telemetry-services/telemetry-vendor-service.test.ts @@ -108,7 +108,7 @@ describe('TelemetryVendorService', () => { const service = new TelemetryVendorService(mockDBConnection); const deploymentServiceStub = sinon - .stub(service.deploymentService, 'getDeploymentsForSurveyId') + .stub(service.deploymentService, 'getDeploymentsForSurvey') .resolves([{ deployment2_id: 8 } as any]); const surveyId = 1; @@ -149,7 +149,7 @@ describe('TelemetryVendorService', () => { const service = new TelemetryVendorService(mockDBConnection); const deploymentServiceStub = sinon - .stub(service.deploymentService, 'getDeploymentsForSurveyId') + .stub(service.deploymentService, 'getDeploymentsForSurvey') .resolves([{ deployment2_id: 8 } as any]); const surveyId = 1; @@ -186,6 +186,7 @@ describe('TelemetryVendorService', () => { critterbase_start_capture_id: null, critterbase_end_capture_id: null, critterbase_end_mortality_id: null, + serial: '1234', device_make_id: 1, model: 'V2', critterbase_critter_id: '1111111111' @@ -203,7 +204,7 @@ describe('TelemetryVendorService', () => { ]; const getDeploymentsForSurveyIdStub = sinon - .stub(TelemetryDeploymentService.prototype, 'getDeploymentsForSurveyId') + .stub(TelemetryDeploymentService.prototype, 'getDeploymentsForSurvey') .resolves(mockDeployment); const getTelemetrySpatialByDeploymentIdsStub = sinon @@ -324,7 +325,7 @@ describe('TelemetryVendorService', () => { const service = new TelemetryVendorService(mockDBConnection); const repoStub = sinon.stub(TelemetryManualRepository.prototype, 'bulkCreateManualTelemetry'); - const validateStub = sinon.stub(service.deploymentService, 'getDeploymentsByIds').resolves([true] as any); + const validateStub = sinon.stub(service.deploymentService, 'getDeploymentsForSurvey').resolves([true] as any); await service.bulkCreateManualTelemetry(1, [ { @@ -352,7 +353,7 @@ describe('TelemetryVendorService', () => { const mockDBConnection = getMockDBConnection(); const service = new TelemetryVendorService(mockDBConnection); - sinon.stub(service.deploymentService, 'getDeploymentsByIds').resolves([]); + sinon.stub(service.deploymentService, 'getDeploymentsForSurvey').resolves([]); try { await service.bulkCreateManualTelemetry(1, [ From b7e5a9de0d9a51ac06ab16fa5aa18507aa4afd0c Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Mon, 16 Dec 2024 10:23:34 -0800 Subject: [PATCH 11/13] fix: pr tweaks --- .../telemetry-deployment-repository.ts | 6 ------ app/src/contexts/telemetryContext.tsx | 2 +- app/src/contexts/telemetryTableContext.tsx | 7 +------ 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/api/src/repositories/telemetry-repositories/telemetry-deployment-repository.ts b/api/src/repositories/telemetry-repositories/telemetry-deployment-repository.ts index 465f5fc254..088ffe3655 100644 --- a/api/src/repositories/telemetry-repositories/telemetry-deployment-repository.ts +++ b/api/src/repositories/telemetry-repositories/telemetry-deployment-repository.ts @@ -130,9 +130,6 @@ export class TelemetryDeploymentRepository extends BaseRepository { } } - console.log(queryBuilder.toSQL().toNative().sql); - console.log(queryBuilder.toSQL().toNative().bindings); - const response = await this.connection.knex(queryBuilder, ExtendedDeploymentRecord); return response.rows; @@ -240,9 +237,6 @@ export class TelemetryDeploymentRepository extends BaseRepository { } } - console.log(queryBuilder.toSQL().toNative().sql); - console.log(queryBuilder.toSQL().toNative().bindings); - const response = await this.connection.knex(queryBuilder, ExtendedDeploymentRecord); return response.rows; diff --git a/app/src/contexts/telemetryContext.tsx b/app/src/contexts/telemetryContext.tsx index fa149ddb86..f4dc592c40 100644 --- a/app/src/contexts/telemetryContext.tsx +++ b/app/src/contexts/telemetryContext.tsx @@ -26,7 +26,7 @@ export type ITelemetryContext = { export const TelemetryContext = createContext(undefined); -export const TelemetryContextProvider = (props: PropsWithChildren>) => { +export const TelemetryContextProvider = (props: PropsWithChildren) => { const { projectId, surveyId } = useContext(SurveyContext); const biohubApi = useBiohubApi(); diff --git a/app/src/contexts/telemetryTableContext.tsx b/app/src/contexts/telemetryTableContext.tsx index 646bc075ab..261da9a627 100644 --- a/app/src/contexts/telemetryTableContext.tsx +++ b/app/src/contexts/telemetryTableContext.tsx @@ -179,12 +179,7 @@ export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextPr const dialogContext = useDialogContext(); const { - telemetryDataLoader: { - data: telemetryData, - isLoading: isLoadingTelemetryData, - // hasLoaded: hasLoadedTelemetryData, - refresh: refreshTelemetryData - } + telemetryDataLoader: { data: telemetryData, isLoading: isLoadingTelemetryData, refresh: refreshTelemetryData } } = useTelemetryContext(); // The data grid rows From c94841b2005f103d6df2b3badab65194e1df937a Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Mon, 16 Dec 2024 11:30:53 -0800 Subject: [PATCH 12/13] chore: deprecating bctw related code --- api/.pipeline/config.js | 4 - api/.pipeline/lib/api.deploy.js | 3 +- api/.pipeline/templates/api.dc.yaml | 9 +- api/src/models/bctw.ts | 99 ------------------- .../critters/{critterId}/telemetry.ts | 2 +- .../buttons/BreadcrumbNavButton.tsx | 2 +- .../fields/TelemetrySelectField.tsx | 61 ------------ app/src/contexts/telemetryTableContext.tsx | 2 +- .../interfaces/useTelemetryApi.interface.ts | 1 + 9 files changed, 7 insertions(+), 176 deletions(-) delete mode 100644 api/src/models/bctw.ts delete mode 100644 app/src/components/fields/TelemetrySelectField.tsx diff --git a/api/.pipeline/config.js b/api/.pipeline/config.js index 4105d7b3f5..d6b243b1e6 100644 --- a/api/.pipeline/config.js +++ b/api/.pipeline/config.js @@ -85,7 +85,6 @@ const phases = { backboneArtifactIntakePath: '/api/artifact/intake', biohubTaxonPath: '/api/taxonomy/taxon', biohubTaxonTsnPath: '/api/taxonomy/taxon/tsn', - bctwApiHost: 'https://moe-bctw-api-dev.apps.silver.devops.gov.bc.ca', critterbaseApiHost: 'https://moe-critterbase-api-dev.apps.silver.devops.gov.bc.ca/api', nodeEnv: 'development', s3KeyPrefix: (isStaticDeployment && 'sims') || `local/${deployChangeId}/sims`, @@ -130,7 +129,6 @@ const phases = { backboneArtifactIntakePath: '/api/artifact/intake', biohubTaxonPath: '/api/taxonomy/taxon', biohubTaxonTsnPath: '/api/taxonomy/taxon/tsn', - bctwApiHost: 'https://moe-bctw-api-test.apps.silver.devops.gov.bc.ca', critterbaseApiHost: 'https://moe-critterbase-api-test.apps.silver.devops.gov.bc.ca/api', nodeEnv: 'production', s3KeyPrefix: 'sims', @@ -175,7 +173,6 @@ const phases = { backboneArtifactIntakePath: '/api/artifact/intake', biohubTaxonPath: '/api/taxonomy/taxon', biohubTaxonTsnPath: '/api/taxonomy/taxon/tsn', - bctwApiHost: 'https://moe-bctw-api-test.apps.silver.devops.gov.bc.ca', critterbaseApiHost: 'https://moe-critterbase-api-test.apps.silver.devops.gov.bc.ca/api', nodeEnv: 'production', s3KeyPrefix: 'sims', @@ -220,7 +217,6 @@ const phases = { backboneArtifactIntakePath: '/api/artifact/intake', biohubTaxonPath: '/api/taxonomy/taxon', biohubTaxonTsnPath: '/api/taxonomy/taxon/tsn', - bctwApiHost: 'https://moe-bctw-api-prod.apps.silver.devops.gov.bc.ca', critterbaseApiHost: 'https://moe-critterbase-api-prod.apps.silver.devops.gov.bc.ca/api', nodeEnv: 'production', s3KeyPrefix: 'sims', diff --git a/api/.pipeline/lib/api.deploy.js b/api/.pipeline/lib/api.deploy.js index ff8eaa088a..679f4aad23 100644 --- a/api/.pipeline/lib/api.deploy.js +++ b/api/.pipeline/lib/api.deploy.js @@ -46,8 +46,7 @@ const apiDeploy = async (settings) => { BACKBONE_ARTIFACT_INTAKE_PATH: phases[phase].backboneArtifactIntakePath, BIOHUB_TAXON_PATH: phases[phase].biohubTaxonPath, BIOHUB_TAXON_TSN_PATH: phases[phase].biohubTaxonTsnPath, - // BCTW / Critterbase - BCTW_API_HOST: phases[phase].bctwApiHost, + // Critterbase CB_API_HOST: phases[phase].critterbaseApiHost, // S3 S3_KEY_PREFIX: phases[phase].s3KeyPrefix, diff --git a/api/.pipeline/templates/api.dc.yaml b/api/.pipeline/templates/api.dc.yaml index 28287b6330..58ec2fb439 100644 --- a/api/.pipeline/templates/api.dc.yaml +++ b/api/.pipeline/templates/api.dc.yaml @@ -67,13 +67,10 @@ parameters: - name: BIOHUB_TAXON_PATH required: true description: API path for BioHub Platform Backbone taxon endpoint. Example "/api/path/to/taxon". - # BCTW / Critterbase + # Critterbase - name: CB_API_HOST description: API host for the Critterbase service, SIMS API will hit this to retrieve critter metadata. Example "https://critterbase.com". required: true - - name: BCTW_API_HOST - description: API host for the BC Telemetry Warehouse service. SIMS API will hit this for device deployments and other telemetry operations. Example "https://bctw.com". - required: true # Database - name: TZ description: Application timezone @@ -309,11 +306,9 @@ objects: value: ${BIOHUB_TAXON_TSN_PATH} - name: BIOHUB_TAXON_PATH value: ${BIOHUB_TAXON_PATH} - # BCTW / Critterbase + # Critterbase - name: CB_API_HOST value: ${CB_API_HOST} - - name: BCTW_API_HOST - value: ${BCTW_API_HOST} # Clamav - name: ENABLE_FILE_VIRUS_SCAN value: ${ENABLE_FILE_VIRUS_SCAN} diff --git a/api/src/models/bctw.ts b/api/src/models/bctw.ts deleted file mode 100644 index 2c7931f4cf..0000000000 --- a/api/src/models/bctw.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { z } from 'zod'; - -export const BctwDeployDevice = z.object({ - device_id: z.number(), - frequency: z.number().optional(), - frequency_unit: z.string().optional(), - device_make: z.string().optional(), - device_model: z.string().optional(), - attachment_start: z.string(), - attachment_end: z.string().nullable(), - critter_id: z.string(), - critterbase_start_capture_id: z.string().uuid(), - critterbase_end_capture_id: z.string().uuid().nullable(), - critterbase_end_mortality_id: z.string().uuid().nullable() -}); - -export type BctwDeployDevice = z.infer; - -export type BctwDevice = Omit & { - collar_id: string; -}; - -export const BctwDeploymentUpdate = z.object({ - deployment_id: z.string(), - attachment_start: z.string(), - attachment_end: z.string() -}); - -export type BctwDeploymentUpdate = z.infer; - -export const BctwUploadKeyxResponse = z.object({ - errors: z.array( - z.object({ - row: z.string(), - error: z.string(), - rownum: z.number() - }) - ), - results: z.array( - z.object({ - idcollar: z.number(), - comtype: z.string(), - idcom: z.string(), - collarkey: z.string(), - collartype: z.number(), - dtlast_fetch: z.string().nullable() - }) - ) -}); - -export type BctwUploadKeyxResponse = z.infer; - -export const BctwKeyXDetails = z.object({ - device_id: z.number(), - keyx: z - .object({ - idcom: z.string(), - comtype: z.string(), - idcollar: z.number(), - collarkey: z.string(), - collartype: z.number() - }) - .nullable() -}); - -export type BctwKeyXDetails = z.infer; - -export const IManualTelemetry = z.object({ - telemetry_manual_id: z.string().uuid(), - deployment_id: z.string().uuid(), - latitude: z.number(), - longitude: z.number(), - date: z.string() -}); - -export type IManualTelemetry = z.infer; - -export const BctwUser = z.object({ - keycloak_guid: z.string(), - username: z.string() -}); - -export interface ICodeResponse { - code_header_title: string; - code_header_name: string; - id: number; - code: string; - description: string; - long_description: string; -} - -export type BctwUser = z.infer; - -export interface ICreateManualTelemetry { - deployment_id: string; - latitude: number; - longitude: number; - acquisition_date: string; -} diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/critters/{critterId}/telemetry.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/critters/{critterId}/telemetry.ts index 199bcefbc7..bae5f98ce2 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/critters/{critterId}/telemetry.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/critters/{critterId}/telemetry.ts @@ -30,7 +30,7 @@ export const GET: Operation = [ GET.apiDoc = { description: 'Get telemetry points for a specific critter.', - tags: ['bctw'], + tags: ['telemetry'], security: [ { Bearer: [] diff --git a/app/src/components/buttons/BreadcrumbNavButton.tsx b/app/src/components/buttons/BreadcrumbNavButton.tsx index ba5df84178..5a65ecbaf0 100644 --- a/app/src/components/buttons/BreadcrumbNavButton.tsx +++ b/app/src/components/buttons/BreadcrumbNavButton.tsx @@ -45,7 +45,7 @@ export const BreadcrumbNavButton = (props: PropsWithChildren { handleMenuClose(); }}> - {item.icon && } + {item.icon ? : null} {item.label} ))} diff --git a/app/src/components/fields/TelemetrySelectField.tsx b/app/src/components/fields/TelemetrySelectField.tsx deleted file mode 100644 index 9defd91488..0000000000 --- a/app/src/components/fields/TelemetrySelectField.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import FormControl, { FormControlProps } from '@mui/material/FormControl'; -import FormHelperText from '@mui/material/FormHelperText'; -import InputLabel from '@mui/material/InputLabel'; -import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; -import { FormikContextType, useFormikContext } from 'formik'; -import useDataLoader from 'hooks/useDataLoader'; -import get from 'lodash-es/get'; -import React from 'react'; - -interface IAllTelemetrySelectField { - name: string; - label: string; - id: string; - fetchData: () => Promise<(string | number)[]>; - controlProps?: FormControlProps; - handleBlur?: FormikContextType['handleBlur']; - handleChange?: FormikContextType['handleChange']; -} - -interface ISelectOption { - value: string | number; - label: string; -} - -const TelemetrySelectField: React.FC = (props) => { - const bctwLookupLoader = useDataLoader(() => props.fetchData()); - const { values, touched, errors, handleChange, handleBlur } = useFormikContext(); - - const err = get(touched, props.name) && get(errors, props.name); - - if (!bctwLookupLoader.data) { - bctwLookupLoader.load(); - } - - const value = bctwLookupLoader.hasLoaded && get(values, props.name) ? get(values, props.name) : ''; - - return ( - - {props.label} - - {err} - - ); -}; - -export default TelemetrySelectField; diff --git a/app/src/contexts/telemetryTableContext.tsx b/app/src/contexts/telemetryTableContext.tsx index 261da9a627..b521263776 100644 --- a/app/src/contexts/telemetryTableContext.tsx +++ b/app/src/contexts/telemetryTableContext.tsx @@ -611,7 +611,7 @@ export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextPr }, [rows, _updateRowsMode, _modifiedRowIds]); /** - * Dispatches update and create requests to BCTW + * Dispatches update and create requests to SIMS * * @param {GridValidRowModel[]} createRows - Rows to create * @param {GridValidRowModel[]} updateRows - Rows to update diff --git a/app/src/interfaces/useTelemetryApi.interface.ts b/app/src/interfaces/useTelemetryApi.interface.ts index a8e8f73426..e034871663 100644 --- a/app/src/interfaces/useTelemetryApi.interface.ts +++ b/app/src/interfaces/useTelemetryApi.interface.ts @@ -67,6 +67,7 @@ export interface IAllTelemetry { temperature: number | null; } +// TODO: Update this type after telemetry migration export type IAnimalDeployment = { // BCTW properties From 1ef61b53f25efadd29cc20ed2bc9d3e0a2c81e50 Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Mon, 16 Dec 2024 12:39:12 -0800 Subject: [PATCH 13/13] fix: dropped telemetry context provider and custom hook --- app/src/contexts/telemetryContext.tsx | 48 ------------------- app/src/contexts/telemetryTableContext.tsx | 16 +++++-- app/src/features/surveys/SurveyRouter.tsx | 5 +- .../telemetry/list/SurveyDeploymentList.tsx | 11 +++-- app/src/hooks/useContext.tsx | 18 ------- 5 files changed, 20 insertions(+), 78 deletions(-) delete mode 100644 app/src/contexts/telemetryContext.tsx diff --git a/app/src/contexts/telemetryContext.tsx b/app/src/contexts/telemetryContext.tsx deleted file mode 100644 index f4dc592c40..0000000000 --- a/app/src/contexts/telemetryContext.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { SurveyContext } from 'contexts/surveyContext'; -import { useBiohubApi } from 'hooks/useBioHubApi'; -import useDataLoader, { DataLoader } from 'hooks/useDataLoader'; -import { GetSurveyTelemetryResponse } from 'interfaces/useTelemetryApi.interface'; -import { GetSurveyDeploymentsResponse } from 'interfaces/useTelemetryDeploymentApi.interface'; - -import { createContext, PropsWithChildren, useContext } from 'react'; -import { ApiPaginationRequestOptions } from 'types/misc'; - -/** - * Context object that stores information about survey telemetry - * - * @export - * @interface ITelemetryContext - */ -export type ITelemetryContext = { - /** - * Data Loader used for retrieving survey deployments records. - */ - deploymentDataLoader: DataLoader<[pagination?: ApiPaginationRequestOptions], GetSurveyDeploymentsResponse, unknown>; - /** - * Data Loader used for retrieving survey telemetry records. - */ - telemetryDataLoader: DataLoader<[pagination?: ApiPaginationRequestOptions], GetSurveyTelemetryResponse, unknown>; -}; - -export const TelemetryContext = createContext(undefined); - -export const TelemetryContextProvider = (props: PropsWithChildren) => { - const { projectId, surveyId } = useContext(SurveyContext); - - const biohubApi = useBiohubApi(); - - const deploymentDataLoader = useDataLoader((pagination?: ApiPaginationRequestOptions) => - biohubApi.telemetryDeployment.getDeploymentsInSurvey(projectId, surveyId, pagination) - ); - - const telemetryDataLoader = useDataLoader((pagination?: ApiPaginationRequestOptions) => - biohubApi.telemetry.getTelemetryForSurvey(projectId, surveyId, pagination) - ); - - const telemetryContext: ITelemetryContext = { - deploymentDataLoader, - telemetryDataLoader - }; - - return {props.children}; -}; diff --git a/app/src/contexts/telemetryTableContext.tsx b/app/src/contexts/telemetryTableContext.tsx index b521263776..4ad8ec81ce 100644 --- a/app/src/contexts/telemetryTableContext.tsx +++ b/app/src/contexts/telemetryTableContext.tsx @@ -17,10 +17,12 @@ import { SIMS_TELEMETRY_HIDDEN_COLUMNS } from 'constants/session-storage'; import { default as dayjs } from 'dayjs'; import { APIError } from 'hooks/api/useAxios'; import { useBiohubApi } from 'hooks/useBioHubApi'; -import { useDialogContext, useSurveyContext, useTelemetryContext } from 'hooks/useContext'; +import { useDialogContext, useSurveyContext } from 'hooks/useContext'; +import useDataLoader from 'hooks/useDataLoader'; import { usePersistentState } from 'hooks/usePersistentState'; import { GetSurveyTelemetryResponse } from 'interfaces/useTelemetryApi.interface'; import { createContext, PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { ApiPaginationRequestOptions } from 'types/misc'; import { firstOrNull } from 'utils/Utils'; import { v4 as uuidv4 } from 'uuid'; import { RowValidationError, TableValidationModel } from '../components/data-grid/DataGridValidationAlert'; @@ -178,9 +180,15 @@ export const TelemetryTableContextProvider = (props: IAllTelemetryTableContextPr const surveyContext = useSurveyContext(); const dialogContext = useDialogContext(); - const { - telemetryDataLoader: { data: telemetryData, isLoading: isLoadingTelemetryData, refresh: refreshTelemetryData } - } = useTelemetryContext(); + const telemetryDataLoader = useDataLoader((pagination?: ApiPaginationRequestOptions) => + biohubApi.telemetry.getTelemetryForSurvey(surveyContext.projectId, surveyContext.surveyId, pagination) + ); + + useEffect(() => { + telemetryDataLoader.load(); + }, [telemetryDataLoader]); + + const { data: telemetryData, isLoading: isLoadingTelemetryData, refresh: refreshTelemetryData } = telemetryDataLoader; // The data grid rows const [rows, setRows] = useState([]); diff --git a/app/src/features/surveys/SurveyRouter.tsx b/app/src/features/surveys/SurveyRouter.tsx index 99cb7b9a94..ef2e9716e3 100644 --- a/app/src/features/surveys/SurveyRouter.tsx +++ b/app/src/features/surveys/SurveyRouter.tsx @@ -3,7 +3,6 @@ import { PROJECT_PERMISSION, SYSTEM_ROLE } from 'constants/roles'; import { AnimalPageContextProvider } from 'contexts/animalPageContext'; import { DialogContextProvider } from 'contexts/dialogContext'; import { ObservationsContextProvider } from 'contexts/observationsContext'; -import { TelemetryContextProvider } from 'contexts/telemetryContext'; import { AnimalRouter } from 'features/surveys/animals/AnimalRouter'; import EditSurveyPage from 'features/surveys/edit/EditSurveyPage'; import { SurveyObservationPage } from 'features/surveys/observations/SurveyObservationPage'; @@ -70,9 +69,7 @@ const SurveyRouter: React.FC = () => { PROJECT_PERMISSION.OBSERVER ]} validSystemRoles={[SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.DATA_ADMINISTRATOR]}> - - - + diff --git a/app/src/features/surveys/telemetry/list/SurveyDeploymentList.tsx b/app/src/features/surveys/telemetry/list/SurveyDeploymentList.tsx index fb12ae3f97..c19b95e09c 100644 --- a/app/src/features/surveys/telemetry/list/SurveyDeploymentList.tsx +++ b/app/src/features/surveys/telemetry/list/SurveyDeploymentList.tsx @@ -19,9 +19,11 @@ import { LoadingGuard } from 'components/loading/LoadingGuard'; import { SkeletonList } from 'components/loading/SkeletonLoaders'; import { SurveyDeploymentListItem } from 'features/surveys/telemetry/list/SurveyDeploymentListItem'; import { useBiohubApi } from 'hooks/useBioHubApi'; -import { useCodesContext, useDialogContext, useSurveyContext, useTelemetryContext } from 'hooks/useContext'; +import { useCodesContext, useDialogContext, useSurveyContext } from 'hooks/useContext'; +import useDataLoader from 'hooks/useDataLoader'; import { useEffect, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; +import { ApiPaginationRequestOptions } from 'types/misc'; /** * Renders a list of all deployments in the survey @@ -32,18 +34,19 @@ export const SurveyDeploymentList = () => { const dialogContext = useDialogContext(); const codesContext = useCodesContext(); const surveyContext = useSurveyContext(); - const telemetryContext = useTelemetryContext(); const biohubApi = useBiohubApi(); - const deploymentDataLoader = telemetryContext.deploymentDataLoader; - const [bulkDeploymentAnchorEl, setBulkDeploymentAnchorEl] = useState(null); const [deploymentAnchorEl, setDeploymentAnchorEl] = useState(null); const [checkboxSelectedIds, setCheckboxSelectedIds] = useState([]); const [selectedDeploymentId, setSelectedDeploymentId] = useState(); + const deploymentDataLoader = useDataLoader((pagination?: ApiPaginationRequestOptions) => + biohubApi.telemetryDeployment.getDeploymentsInSurvey(surveyContext.projectId, surveyContext.surveyId, pagination) + ); + const deployments = deploymentDataLoader.data?.deployments ?? []; const deploymentsCount = deploymentDataLoader.data?.count ?? 0; diff --git a/app/src/hooks/useContext.tsx b/app/src/hooks/useContext.tsx index 64b90c8054..d371a965b9 100644 --- a/app/src/hooks/useContext.tsx +++ b/app/src/hooks/useContext.tsx @@ -8,7 +8,6 @@ import { IObservationsTableContext, ObservationsTableContext } from 'contexts/ob import { IProjectContext, ProjectContext } from 'contexts/projectContext'; import { ISurveyContext, SurveyContext } from 'contexts/surveyContext'; import { ITaxonomyContext, TaxonomyContext } from 'contexts/taxonomyContext'; -import { ITelemetryContext, TelemetryContext } from 'contexts/telemetryContext'; import { IAllTelemetryTableContext, TelemetryTableContext } from 'contexts/telemetryTableContext'; import { useContext } from 'react'; @@ -198,20 +197,3 @@ export const useAnimalPageContext = (): IAnimalPageContext => { return context; }; - -/** - * Returns an instance of `ITelemetryContext` from `TelemetryContext`. - * - * @return {*} {ITelemetryContext} - */ -export const useTelemetryContext = (): ITelemetryContext => { - const context = useContext(TelemetryContext); - - if (!context) { - throw Error( - 'ObservationsContext is undefined, please verify you are calling useObservationsContext() as child of an component.' - ); - } - - return context; -};