Skip to content

Commit

Permalink
feat(app, api-client, react-api-client): send CSV RTP file to robot b…
Browse files Browse the repository at this point in the history
…efore creating protocol and run (#15527)

The new RTP paradigm for utilizing CSV files as runtime parameters
requires sending the ID of the file to be used as the value in the
`runTimeParameterValues` object sent with `createProtocol` and
`createRun`. Here, we send any selected RTP files to the new
`/dataFiles` endpoint and await the response containing an ID for each
file. We then associate the returned ID with its respective runtime
parameter variable name, and send that key:value pair along with the
other value runtime parameters.
  • Loading branch information
ncdiehl11 authored Jun 27, 2024
1 parent fbebf9e commit c874331
Show file tree
Hide file tree
Showing 15 changed files with 253 additions and 122 deletions.
8 changes: 5 additions & 3 deletions api-client/src/dataFiles/uploadCsvFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ export function uploadCsvFile(
const fileId = uuidv4()
const stub = {
data: {
id: fileId,
createdAt: '2024-06-07T19:19:56.268029+00:00',
name: 'rtp_mock_file.csv',
data: {
id: fileId,
createdAt: '2024-06-07T19:19:56.268029+00:00',
name: 'rtp_mock_file.csv',
},
},
}
return Promise.resolve(stub)
Expand Down
12 changes: 5 additions & 7 deletions api-client/src/runs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,11 @@ export interface LabwareOffsetCreateData {
vector: VectorOffset
}

type FileRunTimeParameterCreateData = Record<string, string | number | boolean>

type ValueRunTimeParameterCreateData = Record<string, { id: string }>

export type RunTimeParameterCreateData =
| FileRunTimeParameterCreateData
| ValueRunTimeParameterCreateData
type RunTimeParameterValueType = string | number | boolean | { fileId: string }
export type RunTimeParameterCreateData = Record<
string,
RunTimeParameterValueType
>

export interface CommandData {
data: RunTimeCommand
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react'
import { vi, it, describe, expect, beforeEach } from 'vitest'
import { StaticRouter } from 'react-router-dom'
import { fireEvent, screen } from '@testing-library/react'
import { fireEvent, screen, waitFor } from '@testing-library/react'

import { simpleAnalysisFileFixture } from '@opentrons/api-client'
import { OT2_ROBOT_TYPE } from '@opentrons/shared-data'
Expand Down Expand Up @@ -106,7 +106,7 @@ describe('ChooseProtocolSlideout', () => {
).toBeInTheDocument()
})

it('calls createRunFromProtocolSource if CTA clicked', () => {
it('calls createRunFromProtocolSource if CTA clicked', async () => {
const protocolDataWithoutRunTimeParameter = {
...storedProtocolDataWithoutRunTimeParameters,
}
Expand All @@ -122,10 +122,13 @@ describe('ChooseProtocolSlideout', () => {
name: 'Proceed to setup',
})
fireEvent.click(proceedButton)
expect(mockCreateRunFromProtocol).toHaveBeenCalledWith({
files: [expect.any(File)],
protocolKey: storedProtocolDataFixture.protocolKey,
})
await waitFor(() =>
expect(mockCreateRunFromProtocol).toHaveBeenCalledWith({
files: [expect.any(File)],
protocolKey: storedProtocolDataFixture.protocolKey,
runTimeParameterValues: expect.any(Object),
})
)
expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled()
})

Expand Down
64 changes: 54 additions & 10 deletions app/src/organisms/ChooseProtocolSlideout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ import {
useTooltip,
useHoverTooltip,
} from '@opentrons/components'
import { ApiHostProvider } from '@opentrons/react-api-client'
import {
ApiHostProvider,
useUploadCsvFileMutation,
} from '@opentrons/react-api-client'
import { sortRuntimeParameters } from '@opentrons/shared-data'

import { useLogger } from '../../logger'
Expand Down Expand Up @@ -147,6 +150,17 @@ export function ChooseProtocolSlideoutComponent(
robot.ip
)

const { uploadCsvFile } = useUploadCsvFileMutation(
{},
robot != null
? {
hostname: robot.ip,
requestor:
robot?.ip === OPENTRONS_USB ? appShellRequestor : undefined,
}
: null
)

const srcFileObjects =
selectedProtocol != null
? selectedProtocol.srcFiles.map((srcFileBuffer, index) => {
Expand Down Expand Up @@ -189,15 +203,46 @@ export function ChooseProtocolSlideoutComponent(
location,
definitionUri,
}))
: [],
getRunTimeParameterValuesForRun(runTimeParametersOverrides)
: []
)
const handleProceed: React.MouseEventHandler<HTMLButtonElement> = () => {
if (selectedProtocol != null) {
trackCreateProtocolRunEvent({ name: 'createProtocolRecordRequest' })
createRunFromProtocolSource({
files: srcFileObjects,
protocolKey: selectedProtocol.protocolKey,
const dataFilesForProtocolMap = runTimeParametersOverrides.reduce<
Record<string, File>
>(
(acc, parameter) =>
parameter.type === 'csv_file' && parameter.file?.file != null
? { ...acc, [parameter.variableName]: parameter.file.file }
: acc,
{}
)
Promise.all(
Object.entries(dataFilesForProtocolMap).map(([key, file]) => {
const fileResponse = uploadCsvFile(file)
const varName = Promise.resolve(key)
return Promise.all([fileResponse, varName])
})
).then(responseTuples => {
const runTimeParameterValues = getRunTimeParameterValuesForRun(
runTimeParametersOverrides
)

const runTimeParameterValuesWithFiles = responseTuples.reduce(
(acc, responseTuple) => {
const [response, varName] = responseTuple
return {
...acc,
[varName]: { fileId: response.data.id },
}
},
runTimeParameterValues
)
createRunFromProtocolSource({
files: srcFileObjects,
protocolKey: selectedProtocol.protocolKey,
runTimeParameterValues: runTimeParameterValuesWithFiles,
})
})
} else {
logger.warn('failed to create protocol, no protocol selected')
Expand Down Expand Up @@ -679,8 +724,7 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element {
const missingAnalysisData =
analysisStatus === 'error' || analysisStatus === 'stale'
const requiresCsvRunTimeParameter =
storedProtocol.mostRecentAnalysis?.result ===
'parameter-value-required'
analysisStatus === 'parameterRequired'
return (
<React.Fragment key={storedProtocol.protocolKey}>
<Flex flexDirection={DIRECTION_COLUMN}>
Expand Down Expand Up @@ -784,7 +828,7 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element {
color={COLORS.yellow60}
overflowWrap="anywhere"
display={DISPLAY_BLOCK}
marginTop={`-${SPACING.spacing8}`}
marginTop={`-${SPACING.spacing4}`}
marginBottom={SPACING.spacing8}
>
{t('csv_required_for_analysis')}
Expand All @@ -796,7 +840,7 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element {
color={COLORS.yellow60}
overflowWrap="anywhere"
display={DISPLAY_BLOCK}
marginTop={`-${SPACING.spacing8}`}
marginTop={`-${SPACING.spacing4}`}
marginBottom={SPACING.spacing8}
>
{analysisStatus === 'stale'
Expand Down
6 changes: 2 additions & 4 deletions app/src/organisms/ChooseRobotSlideout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ export function ChooseRobotSlideout(
color={COLORS.red60}
overflowWrap={OVERFLOW_WRAP_ANYWHERE}
display={DISPLAY_INLINE_BLOCK}
marginTop={`-${SPACING.spacing8}`}
marginTop={`-${SPACING.spacing4}`}
marginBottom={SPACING.spacing8}
>
{runCreationErrorCode === 409 ? (
Expand Down Expand Up @@ -405,9 +405,7 @@ export function ChooseRobotSlideout(
runtimeParam.type === 'float'
) {
const value = runtimeParam.value as number
const id = `InputField_${
runtimeParam.variableName
}_${index.toString()}`
const id = `InputField_${runtimeParam.variableName}_${index}`
const error =
(Number.isNaN(value) && !isInputFocused) ||
value < runtimeParam.min ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,20 +94,14 @@ describe('ChooseRobotToRunProtocolSlideout', () => {
.calledWith(
expect.any(Object),
{ hostname: expect.any(String) },
expect.any(Array),
expect.any(Object)
expect.any(Array)
)
.thenReturn({
createRunFromProtocolSource: mockCreateRunFromProtocolSource,
reset: mockResetCreateRun,
} as any)
when(vi.mocked(useCreateRunFromProtocol))
.calledWith(
expect.any(Object),
null,
expect.any(Array),
expect.any(Object)
)
.calledWith(expect.any(Object), null, expect.any(Array))
.thenReturn({
createRunFromProtocolSource: mockCreateRunFromProtocolSource,
reset: mockResetCreateRun,
Expand All @@ -124,6 +118,12 @@ describe('ChooseRobotToRunProtocolSlideout', () => {
expect.any(String)
)
.thenReturn([])
when(vi.mocked(useOffsetCandidatesForAnalysis))
.calledWith(
storedProtocolDataWithCsvRunTimeParameter.mostRecentAnalysis,
null
)
.thenReturn([])
when(vi.mocked(useOffsetCandidatesForAnalysis))
.calledWith(
storedProtocolDataWithCsvRunTimeParameter.mostRecentAnalysis,
Expand Down Expand Up @@ -186,7 +186,7 @@ describe('ChooseRobotToRunProtocolSlideout', () => {
expect(vi.mocked(startDiscovery)).toHaveBeenCalled()
expect(dispatch).toHaveBeenCalledWith({ type: 'mockStartDiscovery' })
})
it('defaults to first available robot and allows an available robot to be selected', () => {
it('defaults to first available robot and allows an available robot to be selected', async () => {
vi.mocked(getConnectableRobots).mockReturnValue([
{ ...mockConnectableRobot, name: 'otherRobot', ip: 'otherIp' },
mockConnectableRobot,
Expand All @@ -208,10 +208,13 @@ describe('ChooseRobotToRunProtocolSlideout', () => {
const confirm = screen.getByRole('button', { name: 'Confirm values' })
expect(confirm).not.toBeDisabled()
fireEvent.click(confirm)
expect(mockCreateRunFromProtocolSource).toHaveBeenCalledWith({
files: [expect.any(File)],
protocolKey: storedProtocolDataFixture.protocolKey,
})
await waitFor(() =>
expect(mockCreateRunFromProtocolSource).toHaveBeenCalledWith({
files: [expect.any(File)],
protocolKey: storedProtocolDataFixture.protocolKey,
runTimeParameterValues: expect.any(Object),
})
)
expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled()
})
it('if selected robot is on a different version of the software than the app, disable CTA and show link to device details in options', () => {
Expand All @@ -236,7 +239,7 @@ describe('ChooseRobotToRunProtocolSlideout', () => {
fireEvent.click(linkToRobotDetails)
})

it('renders error state when there is a run creation error', () => {
it('renders error state when there is a run creation error', async () => {
vi.mocked(useCreateRunFromProtocol).mockReturnValue({
runCreationError: 'run creation error',
createRunFromProtocolSource: mockCreateRunFromProtocolSource,
Expand All @@ -254,16 +257,19 @@ describe('ChooseRobotToRunProtocolSlideout', () => {
})
fireEvent.click(proceedButton)
fireEvent.click(screen.getByRole('button', { name: 'Confirm values' }))
expect(mockCreateRunFromProtocolSource).toHaveBeenCalledWith({
files: [expect.any(File)],
protocolKey: storedProtocolDataFixture.protocolKey,
})
await waitFor(() =>
expect(mockCreateRunFromProtocolSource).toHaveBeenCalledWith({
files: [expect.any(File)],
protocolKey: storedProtocolDataFixture.protocolKey,
runTimeParameterValues: expect.any(Object),
})
)
expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled()
// TODO( jr, 3.13.24): fix this when page 2 is completed of the multislideout
// expect(screen.getByText('run creation error')).toBeInTheDocument()
})

it('renders error state when run creation error code is 409', () => {
it('renders error state when run creation error code is 409', async () => {
vi.mocked(useCreateRunFromProtocol).mockReturnValue({
runCreationError: 'Current run is not idle or stopped.',
createRunFromProtocolSource: mockCreateRunFromProtocolSource,
Expand All @@ -284,18 +290,21 @@ describe('ChooseRobotToRunProtocolSlideout', () => {
expect(link.getAttribute('href')).toEqual('/devices/opentrons-robot-name')
fireEvent.click(proceedButton)
fireEvent.click(screen.getByRole('button', { name: 'Confirm values' }))
expect(mockCreateRunFromProtocolSource).toHaveBeenCalledWith({
files: [expect.any(File)],
protocolKey: storedProtocolDataFixture.protocolKey,
})
await waitFor(() =>
expect(mockCreateRunFromProtocolSource).toHaveBeenCalledWith({
files: [expect.any(File)],
protocolKey: storedProtocolDataFixture.protocolKey,
runTimeParameterValues: expect.any(Object),
})
)
expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled()
// TODO( jr, 3.13.24): fix this when page 2 is completed of the multislideout
// screen.getByText(
// 'This robot is busy and can’t run this protocol right now.'
// )
})

it('renders apply historic offsets as determinate if candidates available', () => {
it('renders apply historic offsets as determinate if candidates available', async () => {
const mockOffsetCandidate = {
id: 'third_offset_id',
labwareDisplayName: 'Third Fake Labware Display Name',
Expand Down Expand Up @@ -326,18 +335,20 @@ describe('ChooseRobotToRunProtocolSlideout', () => {
location: mockOffsetCandidate.location,
definitionUri: mockOffsetCandidate.definitionUri,
},
],
{}
]
)
expect(screen.getByRole('checkbox')).toBeChecked()
const proceedButton = screen.getByRole('button', {
name: 'Continue to parameters',
})
fireEvent.click(proceedButton)
fireEvent.click(screen.getByRole('button', { name: 'Confirm values' }))
expect(mockCreateRunFromProtocolSource).toHaveBeenCalledWith({
files: [expect.any(File)],
protocolKey: storedProtocolDataFixture.protocolKey,
await waitFor(() => {
expect(mockCreateRunFromProtocolSource).toHaveBeenCalledWith({
files: [expect.any(File)],
protocolKey: storedProtocolDataFixture.protocolKey,
runTimeParameterValues: expect.any(Object),
})
})
})

Expand Down Expand Up @@ -385,14 +396,12 @@ describe('ChooseRobotToRunProtocolSlideout', () => {
location: mockOffsetCandidate.location,
definitionUri: mockOffsetCandidate.definitionUri,
},
],
{}
]
)
expect(vi.mocked(useCreateRunFromProtocol)).toHaveBeenLastCalledWith(
expect.any(Object),
{ hostname: 'otherIp' },
[],
{}
[]
)
})

Expand Down
Loading

0 comments on commit c874331

Please sign in to comment.