From 57eed21702561ff83d948ef752e968b319b7a746 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:01:50 -0400 Subject: [PATCH] feat(app, api-client, react-api-client): add api-client function for `getCsvFile` (#15926) Add new api-client function for `getCsvFile`, which takes a fileId and returns `CsvFileData`: ``` { id: string createdAt: string name: string } ``` Wrap in react query hook and implement in `HistoricalProtocolRunDrawer`. --- api-client/src/dataFiles/getCsvFile.ts | 12 +++ api-client/src/dataFiles/index.ts | 1 + api-client/src/dataFiles/types.ts | 4 +- .../Devices/HistoricalProtocolRun.tsx | 9 +- .../Devices/HistoricalProtocolRunDrawer.tsx | 98 +++++++++++-------- .../__tests__/HistoricalProtocolRun.test.tsx | 3 + .../__tests__/useCsvFileQuery.test.tsx | 78 +++++++++++++++ react-api-client/src/dataFiles/index.ts | 1 + .../src/dataFiles/useCsvFileQuery.ts | 25 +++++ .../src/dataFiles/useCsvFileRawQuery.ts | 2 +- 10 files changed, 185 insertions(+), 48 deletions(-) create mode 100644 api-client/src/dataFiles/getCsvFile.ts create mode 100644 react-api-client/src/dataFiles/__tests__/useCsvFileQuery.test.tsx create mode 100644 react-api-client/src/dataFiles/useCsvFileQuery.ts diff --git a/api-client/src/dataFiles/getCsvFile.ts b/api-client/src/dataFiles/getCsvFile.ts new file mode 100644 index 00000000000..93d28bca7ee --- /dev/null +++ b/api-client/src/dataFiles/getCsvFile.ts @@ -0,0 +1,12 @@ +import { GET, request } from '../request' + +import type { CsvFileDataResponse } from './types' +import type { ResponsePromise } from '../request' +import type { HostConfig } from '../types' + +export function getCsvFile( + config: HostConfig, + fileId: string +): ResponsePromise { + return request(GET, `/dataFiles/${fileId}`, null, config) +} diff --git a/api-client/src/dataFiles/index.ts b/api-client/src/dataFiles/index.ts index 3496c8acaa0..01733e38291 100644 --- a/api-client/src/dataFiles/index.ts +++ b/api-client/src/dataFiles/index.ts @@ -1,3 +1,4 @@ +export { getCsvFile } from './getCsvFile' export { getCsvFileRaw } from './getCsvFileRaw' export { uploadCsvFile } from './uploadCsvFile' diff --git a/api-client/src/dataFiles/types.ts b/api-client/src/dataFiles/types.ts index 41029bc4380..011fe9dabb3 100644 --- a/api-client/src/dataFiles/types.ts +++ b/api-client/src/dataFiles/types.ts @@ -13,10 +13,12 @@ export interface CsvFileData { name: string } -export interface UploadedCsvFileResponse { +export interface CsvFileDataResponse { data: CsvFileData } +export type UploadedCsvFileResponse = CsvFileDataResponse + export interface UploadedCsvFilesResponse { data: CsvFileData[] } diff --git a/app/src/organisms/Devices/HistoricalProtocolRun.tsx b/app/src/organisms/Devices/HistoricalProtocolRun.tsx index d9fe2e823a4..0b8102f20a7 100644 --- a/app/src/organisms/Devices/HistoricalProtocolRun.tsx +++ b/app/src/organisms/Devices/HistoricalProtocolRun.tsx @@ -13,7 +13,6 @@ import { SPACING, LegacyStyledText, } from '@opentrons/components' -import { useAllCsvFilesQuery } from '@opentrons/react-api-client' import { formatInterval } from '../RunTimeControl/utils' import { formatTimestamp } from './utils' import { EMPTY_TIMESTAMP } from './constants' @@ -41,9 +40,9 @@ export function HistoricalProtocolRun( const { t } = useTranslation('run_details') const { run, protocolName, robotIsBusy, robotName, protocolKey } = props const [drawerOpen, setDrawerOpen] = React.useState(false) - const { data: protocolFileData } = useAllCsvFilesQuery(run.protocolId ?? '') - const allProtocolDataFiles = - protocolFileData != null ? protocolFileData.data : [] + const countRunDataFiles = run.runTimeParameters.filter( + parameter => parameter.type === 'csv_file' + ).length const runStatus = run.status const runDisplayName = formatTimestamp(run.createdAt) let duration = EMPTY_TIMESTAMP @@ -92,7 +91,7 @@ export function HistoricalProtocolRun( width="5%" data-testid={`RecentProtocolRuns_Files_${protocolKey}`} > - {allProtocolDataFiles.length} + {countRunDataFiles} new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ) - const { data } = useAllCsvFilesQuery(run.protocolId ?? '') - const allProtocolDataFiles = data != null ? data.data : [] + const runDataFileIds = run.runTimeParameters.reduce( + (acc, parameter) => { + if (parameter.type === 'csv_file') { + return parameter.file?.id != null ? [...acc, parameter.file?.id] : acc + } + return acc + }, + [] + ) const uniqueLabwareOffsets = allLabwareOffsets?.filter( (offset, index, array) => { return ( @@ -94,7 +101,7 @@ export function HistoricalProtocolRunDrawer( ) : null const protocolFilesData = - allProtocolDataFiles.length === 1 ? ( + runDataFileIds.length === 0 ? ( ) : ( @@ -133,43 +140,8 @@ export function HistoricalProtocolRunDrawer( - {allProtocolDataFiles.map((fileData, index) => { - const { createdAt, name: fileName, id: fileId } = fileData - return ( - - - - {fileName} - - - - - {format(new Date(createdAt), 'M/d/yy HH:mm:ss')} - - - - - - - ) + {runDataFileIds.map((fileId, index) => { + return })} @@ -293,3 +265,47 @@ export function HistoricalProtocolRunDrawer( ) } + +interface CsvFileDataRowProps { + fileId: string +} + +function CsvFileDataRow(props: CsvFileDataRowProps): JSX.Element | null { + const { fileId } = props + + const { data: fileData } = useCsvFileQuery(fileId) + if (fileData == null) { + return null + } + const { name, createdAt } = fileData.data + return ( + + + + {name} + + + + + {format(new Date(createdAt), 'M/d/yy HH:mm:ss')} + + + + + + + ) +} diff --git a/app/src/organisms/Devices/__tests__/HistoricalProtocolRun.test.tsx b/app/src/organisms/Devices/__tests__/HistoricalProtocolRun.test.tsx index 730677ae842..883abd15a28 100644 --- a/app/src/organisms/Devices/__tests__/HistoricalProtocolRun.test.tsx +++ b/app/src/organisms/Devices/__tests__/HistoricalProtocolRun.test.tsx @@ -9,7 +9,9 @@ import { storedProtocolData as storedProtocolDataFixture } from '../../../redux/ import { useRunStatus, useRunTimestamps } from '../../RunTimeControl/hooks' import { HistoricalProtocolRun } from '../HistoricalProtocolRun' import { HistoricalProtocolRunOverflowMenu } from '../HistoricalProtocolRunOverflowMenu' + import type { RunStatus, RunData } from '@opentrons/api-client' +import type { RunTimeParameter } from '@opentrons/shared-data' vi.mock('../../../redux/protocol-storage') vi.mock('../../RunTimeControl/hooks') @@ -20,6 +22,7 @@ const run = { id: 'test_id', protocolId: 'test_protocol_id', status: 'succeeded' as RunStatus, + runTimeParameters: [] as RunTimeParameter[], } as RunData const render = (props: React.ComponentProps) => { diff --git a/react-api-client/src/dataFiles/__tests__/useCsvFileQuery.test.tsx b/react-api-client/src/dataFiles/__tests__/useCsvFileQuery.test.tsx new file mode 100644 index 00000000000..83e7e0be504 --- /dev/null +++ b/react-api-client/src/dataFiles/__tests__/useCsvFileQuery.test.tsx @@ -0,0 +1,78 @@ +import * as React from 'react' +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { QueryClient, QueryClientProvider } from 'react-query' +import { renderHook, waitFor } from '@testing-library/react' +import { getCsvFile } from '@opentrons/api-client' +import { useHost } from '../../api' +import { useCsvFileQuery } from '..' + +import type { + CsvFileData, + CsvFileDataResponse, + HostConfig, + Response, +} from '@opentrons/api-client' + +vi.mock('@opentrons/api-client') +vi.mock('../../api/useHost') + +const HOST_CONFIG: HostConfig = { hostname: 'localhost' } +const FILE_ID = 'file123' +const FILE_NAME = 'my_file.csv' +const FILE_CONTENT_RESPONSE = { + data: { + name: FILE_NAME, + id: FILE_ID, + createdAt: '2024-06-07T19:19:56.268029+00:00', + } as CsvFileData, +} as CsvFileDataResponse + +describe('useCsvFileQuery hook', () => { + let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + + beforeEach(() => { + const queryClient = new QueryClient() + const clientProvider: React.FunctionComponent<{ + children: React.ReactNode + }> = ({ children }) => ( + {children} + ) + + wrapper = clientProvider + }) + + it('should return no data if no host', () => { + vi.mocked(useHost).mockReturnValue(null) + + const { result } = renderHook(() => useCsvFileQuery(FILE_ID), { + wrapper, + }) + + expect(result.current.data).toBeUndefined() + }) + + it('should return no data if the get file request fails', () => { + vi.mocked(useHost).mockReturnValue(HOST_CONFIG) + vi.mocked(getCsvFile).mockRejectedValue('oh no') + + const { result } = renderHook(() => useCsvFileQuery(FILE_ID), { + wrapper, + }) + expect(result.current.data).toBeUndefined() + }) + + it('should return file data if successful request', async () => { + vi.mocked(useHost).mockReturnValue(HOST_CONFIG) + vi.mocked(getCsvFile).mockResolvedValue({ + data: FILE_CONTENT_RESPONSE, + } as Response) + + const { result } = renderHook(() => useCsvFileQuery(FILE_ID), { + wrapper, + }) + + await waitFor(() => { + expect(result.current.data).toEqual(FILE_CONTENT_RESPONSE) + }) + }) +}) diff --git a/react-api-client/src/dataFiles/index.ts b/react-api-client/src/dataFiles/index.ts index cd6fe47daf0..267fe7fe8d9 100644 --- a/react-api-client/src/dataFiles/index.ts +++ b/react-api-client/src/dataFiles/index.ts @@ -1,2 +1,3 @@ +export { useCsvFileQuery } from './useCsvFileQuery' export { useCsvFileRawQuery } from './useCsvFileRawQuery' export { useUploadCsvFileMutation } from './useUploadCsvFileMutation' diff --git a/react-api-client/src/dataFiles/useCsvFileQuery.ts b/react-api-client/src/dataFiles/useCsvFileQuery.ts new file mode 100644 index 00000000000..808551cecbf --- /dev/null +++ b/react-api-client/src/dataFiles/useCsvFileQuery.ts @@ -0,0 +1,25 @@ +import { useQuery } from 'react-query' +import { getCsvFile } from '@opentrons/api-client' +import { useHost } from '../api' + +import type { UseQueryOptions, UseQueryResult } from 'react-query' +import type { HostConfig, CsvFileDataResponse } from '@opentrons/api-client' + +export function useCsvFileQuery( + fileId: string, + options?: UseQueryOptions +): UseQueryResult { + const host = useHost() + const allOptions: UseQueryOptions = { + ...options, + enabled: host !== null && fileId !== null, + } + + const query = useQuery( + [host, 'dataFiles', fileId], + () => + getCsvFile(host as HostConfig, fileId).then(response => response.data), + allOptions + ) + return query +} diff --git a/react-api-client/src/dataFiles/useCsvFileRawQuery.ts b/react-api-client/src/dataFiles/useCsvFileRawQuery.ts index 22cae3ad920..3114dc9d5fc 100644 --- a/react-api-client/src/dataFiles/useCsvFileRawQuery.ts +++ b/react-api-client/src/dataFiles/useCsvFileRawQuery.ts @@ -19,7 +19,7 @@ export function useCsvFileRawQuery( } const query = useQuery( - [host, `/dataFiles/${fileId}/download`], + [host, 'dataFiles', fileId, 'download'], () => getCsvFileRaw(host as HostConfig, fileId).then(response => response.data), allOptions