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

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
runtimeparameter variable name, and send that key:value pair along with the other value
runtimeparameters.
  • Loading branch information
ncdiehl11 committed Jun 26, 2024
1 parent 1ecc05b commit c797e53
Show file tree
Hide file tree
Showing 15 changed files with 208 additions and 105 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
11 changes: 4 additions & 7 deletions api-client/src/runs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,10 @@ export interface LabwareOffsetCreateData {
vector: VectorOffset
}

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

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

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

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
57 changes: 51 additions & 6 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]: { file_id: response.data.id },
}
},
runTimeParameterValues
)
createRunFromProtocolSource({
files: srcFileObjects,
protocolKey: selectedProtocol.protocolKey,
runTimeParameterValues: runTimeParameterValuesWithFiles,
})
})
} else {
logger.warn('failed to create protocol, no protocol selected')
Expand Down
4 changes: 1 addition & 3 deletions app/src/organisms/ChooseRobotSlideout/index.tsx
Original file line number Diff line number Diff line change
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 c797e53

Please sign in to comment.