Skip to content

Commit

Permalink
feat(api-client, app): implement download CSV file (#15861)
Browse files Browse the repository at this point in the history
Here, I create api-client functions and types for downloading CSV file
content from the robot server's `dataFiles/{fileId}/download` endpoint.
I also add a react-api-client wrapper hook and implement in the
`HistoricalProtocolRunDrawer` component.
  • Loading branch information
ncdiehl11 authored Jul 31, 2024
1 parent a25a964 commit 4497300
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 23 deletions.
17 changes: 17 additions & 0 deletions api-client/src/dataFiles/getCsvFileRaw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { GET, request } from '../request'

import type { DownloadedCsvFileResponse } from './types'
import type { ResponsePromise } from '../request'
import type { HostConfig } from '../types'

export function getCsvFileRaw(
config: HostConfig,
fileId: string
): ResponsePromise<DownloadedCsvFileResponse> {
return request<DownloadedCsvFileResponse>(
GET,
`/dataFiles/${fileId}/download`,
null,
config
)
}
1 change: 1 addition & 0 deletions api-client/src/dataFiles/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { getCsvFileRaw } from './getCsvFileRaw'
export { uploadCsvFile } from './uploadCsvFile'

export * from './types'
2 changes: 2 additions & 0 deletions api-client/src/dataFiles/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ export interface UploadedCsvFileResponse {
export interface UploadedCsvFilesResponse {
data: CsvFileData[]
}

export type DownloadedCsvFileResponse = string
47 changes: 47 additions & 0 deletions app/src/organisms/Devices/DownloadCsvFileLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'

import {
ALIGN_CENTER,
Flex,
Icon,
LegacyStyledText,
Link,
SPACING,
TYPOGRAPHY,
} from '@opentrons/components'
import { useCsvFileRawQuery } from '@opentrons/react-api-client'
import { downloadFile } from './utils'

interface DownloadCsvFileLinkProps {
fileId: string
fileName: string
}
export function DownloadCsvFileLink(
props: DownloadCsvFileLinkProps
): JSX.Element {
const { fileId, fileName } = props
const { t } = useTranslation('run_details')
const { data: csvFileRaw } = useCsvFileRawQuery(fileId)

return (
<Link
role="button"
css={
csvFileRaw == null
? TYPOGRAPHY.darkLinkLabelSemiBoldDisabled
: TYPOGRAPHY.linkPSemiBold
}
onClick={() => {
if (csvFileRaw != null) {
downloadFile(csvFileRaw, fileName)
}
}}
>
<Flex alignItems={ALIGN_CENTER} gridGap={SPACING.spacing4}>
<LegacyStyledText as="p">{t('download')}</LegacyStyledText>
<Icon name="download" size="1rem" />
</Flex>
</Link>
)
}
26 changes: 5 additions & 21 deletions app/src/organisms/Devices/HistoricalProtocolRunDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@ import {
COLORS,
DIRECTION_COLUMN,
Flex,
Icon,
InfoScreen,
JUSTIFY_FLEX_START,
LegacyStyledText,
Link,
LocationIcon,
OVERFLOW_HIDDEN,
SPACING,
Expand All @@ -28,6 +26,7 @@ import {
getModuleDisplayName,
} from '@opentrons/shared-data'
import { useAllCsvFilesQuery } from '@opentrons/react-api-client'
import { DownloadCsvFileLink } from './DownloadCsvFileLink'
import { useFeatureFlag } from '../../redux/config'
import { Banner } from '../../atoms/Banner'
import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis'
Expand Down Expand Up @@ -97,7 +96,7 @@ export function HistoricalProtocolRunDrawer(
) : null

const protocolFilesData =
allProtocolDataFiles.length === 0 ? (
allProtocolDataFiles.length === 1 ? (
<InfoScreen contentType="noFiles" t={t} backgroundColor={COLORS.grey35} />
) : (
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing4}>
Expand Down Expand Up @@ -137,7 +136,7 @@ export function HistoricalProtocolRunDrawer(
</Flex>
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing4}>
{allProtocolDataFiles.map((fileData, index) => {
const { createdAt, name } = fileData
const { createdAt, name: fileName, id: fileId } = fileData
return (
<Flex
key={`csv_file_${index}`}
Expand All @@ -160,7 +159,7 @@ export function HistoricalProtocolRunDrawer(
text-overflow: ellipsis;
`}
>
{name}
{fileName}
</LegacyStyledText>
</Flex>
<Box width="33%">
Expand All @@ -169,22 +168,7 @@ export function HistoricalProtocolRunDrawer(
</LegacyStyledText>
</Box>
<Box width="34%">
<Link
role="button"
css={TYPOGRAPHY.linkPSemiBold}
onClick={() => {}} // TODO (nd: 06/18/2024) get file and download
>
<Flex alignItems={ALIGN_CENTER}>
<LegacyStyledText as="p">
{t('download')}
</LegacyStyledText>
<Icon
name="download"
size="1rem"
marginLeft="0.4375rem"
/>
</Flex>
</Link>
<DownloadCsvFileLink fileId={fileId} fileName={fileName} />
</Box>
</Flex>
)
Expand Down
5 changes: 3 additions & 2 deletions app/src/organisms/Devices/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ export function onDeviceDisplayFormatTimestamp(timestamp: string): string {
: timestamp
}

export function downloadFile(data: object, fileName: string): void {
export function downloadFile(data: object | string, fileName: string): void {
// Create a blob with the data we want to download as a file
const blob = new Blob([JSON.stringify(data)], { type: 'text/json' })
const blobContent = typeof data === 'string' ? data : JSON.stringify(data)
const blob = new Blob([blobContent], { type: 'text/json' })
// Create an anchor element and dispatch a click event on it
// to trigger a download
const a = document.createElement('a')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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 { getCsvFileRaw } from '@opentrons/api-client'
import { useHost } from '../../api'
import { useCsvFileRawQuery } from '..'

import type {
HostConfig,
Response,
DownloadedCsvFileResponse,
} 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_CONTENT_RESPONSE = 'content,of,my,csv\nfile,' as DownloadedCsvFileResponse

describe('useCsvFileRawQuery hook', () => {
let wrapper: React.FunctionComponent<{ children: React.ReactNode }>

beforeEach(() => {
const queryClient = new QueryClient()
const clientProvider: React.FunctionComponent<{
children: React.ReactNode
}> = ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)

wrapper = clientProvider
})

it('should return no data if no host', () => {
vi.mocked(useHost).mockReturnValue(null)

const { result } = renderHook(() => useCsvFileRawQuery(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(getCsvFileRaw).mockRejectedValue('oh no')

const { result } = renderHook(() => useCsvFileRawQuery(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(getCsvFileRaw).mockResolvedValue({
data: FILE_CONTENT_RESPONSE,
} as Response<DownloadedCsvFileResponse>)

const { result } = renderHook(() => useCsvFileRawQuery(FILE_ID), {
wrapper,
})

await waitFor(() => {
expect(result.current.data).toEqual(FILE_CONTENT_RESPONSE)
})
})
})
1 change: 1 addition & 0 deletions react-api-client/src/dataFiles/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { useCsvFileRawQuery } from './useCsvFileRawQuery'
export { useUploadCsvFileMutation } from './useUploadCsvFileMutation'
28 changes: 28 additions & 0 deletions react-api-client/src/dataFiles/useCsvFileRawQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useQuery } from 'react-query'
import { getCsvFileRaw } from '@opentrons/api-client'
import { useHost } from '../api'

import type { UseQueryOptions, UseQueryResult } from 'react-query'
import type {
HostConfig,
DownloadedCsvFileResponse,
} from '@opentrons/api-client'

export function useCsvFileRawQuery(
fileId: string,
options?: UseQueryOptions<DownloadedCsvFileResponse>
): UseQueryResult<DownloadedCsvFileResponse> {
const host = useHost()
const allOptions: UseQueryOptions<DownloadedCsvFileResponse> = {
...options,
enabled: host !== null && fileId !== null,
}

const query = useQuery<DownloadedCsvFileResponse>(
[host, `/dataFiles/${fileId}/download`],
() =>
getCsvFileRaw(host as HostConfig, fileId).then(response => response.data),
allOptions
)
return query
}

0 comments on commit 4497300

Please sign in to comment.