From d5757705112ca01e5488ff8ff75e6536c4f6af5e Mon Sep 17 00:00:00 2001 From: koji Date: Tue, 18 Jun 2024 13:11:15 -0400 Subject: [PATCH] feat(api-client,react-api-client): add new hook for csv file (#15451) * feat(api-client,react-api-client): add new hook for csv file --- api-client/src/dataFiles/index.ts | 3 + api-client/src/dataFiles/types.ts | 24 +++++ api-client/src/dataFiles/uploadCsvFile.ts | 36 ++++++++ api-client/src/index.ts | 1 + api-client/src/protocols/getCsvFiles.ts | 47 ++++++++++ api-client/src/protocols/index.ts | 9 +- .../useUploadCsvFileMutation.test.tsx | 87 +++++++++++++++++++ react-api-client/src/dataFiles/index.ts | 1 + .../src/dataFiles/useUploadCsvFileMutation.ts | 66 ++++++++++++++ .../__tests__/useAllCsvFilesQuery.test.tsx | 86 ++++++++++++++++++ react-api-client/src/protocols/index.ts | 13 +-- .../src/protocols/useAllCsvFilesQuery.ts | 30 +++++++ 12 files changed, 393 insertions(+), 10 deletions(-) create mode 100644 api-client/src/dataFiles/index.ts create mode 100644 api-client/src/dataFiles/types.ts create mode 100644 api-client/src/dataFiles/uploadCsvFile.ts create mode 100644 api-client/src/protocols/getCsvFiles.ts create mode 100644 react-api-client/src/dataFiles/__tests__/useUploadCsvFileMutation.test.tsx create mode 100644 react-api-client/src/dataFiles/index.ts create mode 100644 react-api-client/src/dataFiles/useUploadCsvFileMutation.ts create mode 100644 react-api-client/src/protocols/__tests__/useAllCsvFilesQuery.test.tsx create mode 100644 react-api-client/src/protocols/useAllCsvFilesQuery.ts diff --git a/api-client/src/dataFiles/index.ts b/api-client/src/dataFiles/index.ts new file mode 100644 index 00000000000..03cba1330b9 --- /dev/null +++ b/api-client/src/dataFiles/index.ts @@ -0,0 +1,3 @@ +export { uploadCsvFile } from './uploadCsvFile' + +export * from './types' diff --git a/api-client/src/dataFiles/types.ts b/api-client/src/dataFiles/types.ts new file mode 100644 index 00000000000..204307c128e --- /dev/null +++ b/api-client/src/dataFiles/types.ts @@ -0,0 +1,24 @@ +/** + * Represents the parameters for uploading a CSV file. + * + * @interface UploadCsvFileParams + * @property {File | string} [fileData] - File object for Desktop app and string for USB drive on ODD + */ + +export type FileData = File | string + +interface CsvFileData { + id: string + createdAt: string + name: string +} + +export interface UploadedCsvFileResponse { + data: CsvFileData +} + +export interface UploadedCsvFilesResponse { + data: { + files: CsvFileData[] + } +} diff --git a/api-client/src/dataFiles/uploadCsvFile.ts b/api-client/src/dataFiles/uploadCsvFile.ts new file mode 100644 index 00000000000..249bd83605f --- /dev/null +++ b/api-client/src/dataFiles/uploadCsvFile.ts @@ -0,0 +1,36 @@ +import { v4 as uuidv4 } from 'uuid' +// import { POST, request } from '../request' + +// import type { ResponsePromise } from '../request' +import type { HostConfig } from '../types' +import type { FileData /** UploadedCsvFileResponse */ } from './types' + +// export function uploadCsvFile( +// config: HostConfig, +// data FileData +// ): ResponsePromise { +// return request( +// POST, +// '/dataFiles', +// null, +// config, +// data +// ) +// } + +// ToDo (kk:06/14/2024) remove when activate the above code +export function uploadCsvFile( + config: HostConfig, + data: FileData + // Note (kk: 06/14/2024) temporary using any for useUploadCsvFileMutation +): Promise { + const fileId = uuidv4() + const stub = { + data: { + id: fileId, + createdAt: '2024-06-07T19:19:56.268029+00:00', + name: 'rtp_mock_file.csv', + }, + } + return Promise.resolve(stub) +} diff --git a/api-client/src/index.ts b/api-client/src/index.ts index f6957a49e8f..5eb2e960b9b 100644 --- a/api-client/src/index.ts +++ b/api-client/src/index.ts @@ -1,5 +1,6 @@ // api client entry point export * from './calibration' +export * from './dataFiles' export * from './deck_configuration' export * from './health' export * from './instruments' diff --git a/api-client/src/protocols/getCsvFiles.ts b/api-client/src/protocols/getCsvFiles.ts new file mode 100644 index 00000000000..8cb962f795a --- /dev/null +++ b/api-client/src/protocols/getCsvFiles.ts @@ -0,0 +1,47 @@ +import { v4 as uuidv4 } from 'uuid' + +// import { GET, request } from '../request' + +// import type { ResponsePromise } from '../request' +import type { HostConfig } from '../types' +import type { UploadedCsvFilesResponse } from '../dataFiles/types' + +/** +export function getCsvFiles( + config: HostConfig, + protocolId: string +): ResponsePromise { + return request( + GET, + `/protocols/${protocolId}/dataFiles`, + null, + config + ) +} + */ + +// ToDo (kk:06/14/2024) remove when activate the above code +export function getCsvFiles( + config: HostConfig, + protocolId: string +): Promise<{ data: UploadedCsvFilesResponse }> { + const fileIdOne = uuidv4() + const fileIdTwo = uuidv4() + const stub = { + data: { + files: [ + { + id: fileIdOne, + createdAt: '2024-06-07T19:19:56.268029+00:00', + name: 'rtp_mock_file1.csv', + }, + { + id: fileIdTwo, + createdAt: '2024-06-17T19:19:56.268029+00:00', + name: 'rtp_mock_file2.csv', + }, + ], + }, + } + return Promise.resolve({ data: stub }) +} diff --git a/api-client/src/protocols/index.ts b/api-client/src/protocols/index.ts index f035fa000e1..dd1d544f24a 100644 --- a/api-client/src/protocols/index.ts +++ b/api-client/src/protocols/index.ts @@ -1,11 +1,12 @@ +export { createProtocol } from './createProtocol' +export { createProtocolAnalysis } from './createProtocolAnalysis' +export { deleteProtocol } from './deleteProtocol' +export { getCsvFiles } from './getCsvFiles' export { getProtocol } from './getProtocol' export { getProtocolAnalyses } from './getProtocolAnalyses' export { getProtocolAnalysisAsDocument } from './getProtocolAnalysisAsDocument' -export { deleteProtocol } from './deleteProtocol' -export { createProtocol } from './createProtocol' -export { createProtocolAnalysis } from './createProtocolAnalysis' -export { getProtocols } from './getProtocols' export { getProtocolIds } from './getProtocolIds' +export { getProtocols } from './getProtocols' export * from './types' export * from './utils' diff --git a/react-api-client/src/dataFiles/__tests__/useUploadCsvFileMutation.test.tsx b/react-api-client/src/dataFiles/__tests__/useUploadCsvFileMutation.test.tsx new file mode 100644 index 00000000000..9fc947b0ed8 --- /dev/null +++ b/react-api-client/src/dataFiles/__tests__/useUploadCsvFileMutation.test.tsx @@ -0,0 +1,87 @@ +import * as React from 'react' +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { QueryClient, QueryClientProvider } from 'react-query' +import { act, renderHook, waitFor } from '@testing-library/react' +import { uploadCsvFile } from '@opentrons/api-client' +import { useHost } from '../../api' +import { useUploadCsvFileMutation } from '../useUploadCsvFileMutation' + +import type { + HostConfig, + UploadedCsvFileResponse, + Response, +} from '@opentrons/api-client' + +vi.mock('@opentrons/api-client') +vi.mock('../../api/useHost') + +const HOST_CONFIG: HostConfig = { hostname: 'localhost' } +const mockFilePath = 'media/mock-usb-drive/mock.csv' +const mockUploadResponse = { + data: { + id: '1', + createdAt: '2024-06-07T19:19:56.268029+00:00', + name: 'rtp_mock_file.csv', + }, +} as UploadedCsvFileResponse + +describe('useUploadCsvFileMutation', () => { + 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( + () => useUploadCsvFileMutation(mockFilePath), + { + wrapper, + } + ) + expect(result.current.data).toBeUndefined() + }) + + it('should return no data if the request fails', async () => { + vi.mocked(useHost).mockReturnValue(HOST_CONFIG) + vi.mocked(uploadCsvFile).mockRejectedValue('oh no') + + const { result } = renderHook( + () => useUploadCsvFileMutation(mockFilePath), + { + wrapper, + } + ) + expect(result.current.data).toBeUndefined() + result.current.uploadCsvFile(mockFilePath) + await waitFor(() => { + expect(result.current.data).toBeUndefined() + }) + }) + + it('should return data when calling uploadCsvFile successfully', async () => { + vi.mocked(useHost).mockReturnValue(HOST_CONFIG) + vi.mocked(uploadCsvFile).mockResolvedValue({ + data: mockUploadResponse, + } as Response) + + const { result } = renderHook( + () => useUploadCsvFileMutation(mockFilePath), + { wrapper } + ) + act(() => result.current.uploadCsvFile(mockFilePath)) + + await waitFor(() => { + expect(result.current.data).toEqual(mockUploadResponse) + }) + }) +}) diff --git a/react-api-client/src/dataFiles/index.ts b/react-api-client/src/dataFiles/index.ts new file mode 100644 index 00000000000..3ff92db8497 --- /dev/null +++ b/react-api-client/src/dataFiles/index.ts @@ -0,0 +1 @@ +export { useUploadCsvFileMutation } from './useUploadCsvFileMutation' diff --git a/react-api-client/src/dataFiles/useUploadCsvFileMutation.ts b/react-api-client/src/dataFiles/useUploadCsvFileMutation.ts new file mode 100644 index 00000000000..efe6ed58b66 --- /dev/null +++ b/react-api-client/src/dataFiles/useUploadCsvFileMutation.ts @@ -0,0 +1,66 @@ +import { useMutation, useQueryClient } from 'react-query' +import { uploadCsvFile } from '@opentrons/api-client' +import { useHost } from '../api' +import type { AxiosError } from 'axios' +import type { + UseMutationResult, + UseMutationOptions, + UseMutateFunction, +} from 'react-query' +import type { + ErrorResponse, + HostConfig, + FileData, + UploadedCsvFileResponse, +} from '@opentrons/api-client' + +export type UseUploadCsvFileMutationResult = UseMutationResult< + UploadedCsvFileResponse, + AxiosError, + FileData +> & { + uploadCsvFile: UseMutateFunction< + UploadedCsvFileResponse, + AxiosError, + FileData + > +} + +export type UseUploadCsvFileMutationOption = UseMutationOptions< + UploadedCsvFileResponse, + AxiosError, + FileData +> + +export function useUploadCsvFileMutation( + fileData: FileData, + options: UseUploadCsvFileMutationOption = {}, + hostOverride?: HostConfig | null +): UseUploadCsvFileMutationResult { + const host = useHost() + const queryClient = useQueryClient() + + const mutation = useMutation< + UploadedCsvFileResponse, + AxiosError, + FileData + >( + (fileData: FileData) => + uploadCsvFile(host as HostConfig, fileData).then(response => { + queryClient + .invalidateQueries([host, 'dataFiles']) + .then(() => + queryClient.setQueryData([host, 'dataFiles'], response.data) + ) + .catch((e: Error) => { + throw e + }) + return response.data + }), + options + ) + return { + ...mutation, + uploadCsvFile: mutation.mutate, + } +} diff --git a/react-api-client/src/protocols/__tests__/useAllCsvFilesQuery.test.tsx b/react-api-client/src/protocols/__tests__/useAllCsvFilesQuery.test.tsx new file mode 100644 index 00000000000..15e4ceed542 --- /dev/null +++ b/react-api-client/src/protocols/__tests__/useAllCsvFilesQuery.test.tsx @@ -0,0 +1,86 @@ +import * as React from 'react' +import { describe, it, vi, beforeEach, expect } from 'vitest' +import { QueryClient, QueryClientProvider } from 'react-query' +import { renderHook /** waitFor */ } from '@testing-library/react' +// import { getCsvFiles } from '@opentrons/api-client' +import { useHost } from '../../api' +import { useAllCsvFilesQuery } from '../useAllCsvFilesQuery' + +// import type { +// HostConfig, +// Response, +// UploadedCsvFilesResponse, +// } from '@opentrons/api-client' + +vi.mock('@oopentrons/api-client') +vi.mock('../../api/useHost') + +// const HOST_CONFIG: HostConfig = { hostname: 'localhost' } +// const CSV_FILES_RESPONSE = { +// data: { +// files: [ +// { +// id: '1', +// createdAt: '2024-06-07T19:19:56.268029+00:00', +// name: 'rtp_mock_file1.csv', +// }, +// { +// id: '2', +// createdAt: '2024-06-17T19:19:56.268029+00:00', +// name: 'rtp_mock_file2.csv', +// }, +// ], +// }, +// } as UploadedCsvFilesResponse +const PROTOCOL_ID = '1' + +describe('useAllCsvFilesQuery', () => { + 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(() => useAllCsvFilesQuery(PROTOCOL_ID), { + wrapper, + }) + expect(result.current.data).toBeUndefined() + }) + + // ToDo (kk:06/14/2024) remove comment when remove stub + // it('should return no data if the get csv files request fails', () => { + // vi.mocked(useHost).mockReturnValue(HOST_CONFIG) + // vi.mocked(getCsvFiles).mockRejectedValue('oh no') + + // const { result } = renderHook(() => useAllCsvFilesQuery(PROTOCOL_ID), { + // wrapper, + // }) + + // expect(result.current.data).toBeUndefined() + // }) + + // it('should return csv files data', async () => { + // vi.mocked(useHost).mockReturnValue(HOST_CONFIG) + // vi.mocked(getCsvFiles).mockResolvedValue({ + // data: CSV_FILES_RESPONSE, + // } as Response) + + // const { result } = renderHook(() => useAllCsvFilesQuery(PROTOCOL_ID), { + // wrapper, + // }) + + // await waitFor(() => { + // expect(result.current.data).toEqual(CSV_FILES_RESPONSE) + // }) + // }) +}) diff --git a/react-api-client/src/protocols/index.ts b/react-api-client/src/protocols/index.ts index 706e92aae1b..34df956765e 100644 --- a/react-api-client/src/protocols/index.ts +++ b/react-api-client/src/protocols/index.ts @@ -1,9 +1,10 @@ -export { useAllProtocolsQuery } from './useAllProtocolsQuery' +export { useAllCsvFilesQuery } from './useAllCsvFilesQuery' export { useAllProtocolIdsQuery } from './useAllProtocolIdsQuery' -export { useProtocolQuery } from './useProtocolQuery' -export { useProtocolAnalysesQuery } from './useProtocolAnalysesQuery' -export { useMostRecentSuccessfulAnalysisAsDocumentQuery } from './useMostRecentSuccessfulAnalysisAsDocumentQuery' -export { useProtocolAnalysisAsDocumentQuery } from './useProtocolAnalysisAsDocumentQuery' -export { useCreateProtocolMutation } from './useCreateProtocolMutation' +export { useAllProtocolsQuery } from './useAllProtocolsQuery' export { useCreateProtocolAnalysisMutation } from './useCreateProtocolAnalysisMutation' +export { useCreateProtocolMutation } from './useCreateProtocolMutation' export { useDeleteProtocolMutation } from './useDeleteProtocolMutation' +export { useMostRecentSuccessfulAnalysisAsDocumentQuery } from './useMostRecentSuccessfulAnalysisAsDocumentQuery' +export { useProtocolAnalysesQuery } from './useProtocolAnalysesQuery' +export { useProtocolAnalysisAsDocumentQuery } from './useProtocolAnalysisAsDocumentQuery' +export { useProtocolQuery } from './useProtocolQuery' diff --git a/react-api-client/src/protocols/useAllCsvFilesQuery.ts b/react-api-client/src/protocols/useAllCsvFilesQuery.ts new file mode 100644 index 00000000000..b681b81d82c --- /dev/null +++ b/react-api-client/src/protocols/useAllCsvFilesQuery.ts @@ -0,0 +1,30 @@ +import { useQuery } from 'react-query' +import { getCsvFiles } from '@opentrons/api-client' +import { useHost } from '../api' + +import type { UseQueryOptions, UseQueryResult } from 'react-query' +import type { + HostConfig, + UploadedCsvFilesResponse, +} from '@opentrons/api-client' + +export function useAllCsvFilesQuery( + protocolId: string, + options?: UseQueryOptions +): UseQueryResult { + const host = useHost() + const allOptions: UseQueryOptions = { + ...options, + enabled: host !== null && protocolId !== null, + } + + const query = useQuery( + [host, `protocols/${protocolId}/dataFiles`], + () => + getCsvFiles(host as HostConfig, protocolId as string).then( + response => response.data + ), + allOptions + ) + return query +}