diff --git a/api-client/src/runs/constants.ts b/api-client/src/runs/constants.ts new file mode 100644 index 00000000000..9f0d8293ef6 --- /dev/null +++ b/api-client/src/runs/constants.ts @@ -0,0 +1,11 @@ +import { + RUN_STATUS_FAILED, + RUN_STATUS_STOPPED, + RUN_STATUS_SUCCEEDED, +} from './types' + +export const RUN_STATUSES_TERMINAL = [ + RUN_STATUS_SUCCEEDED, + RUN_STATUS_FAILED, + RUN_STATUS_STOPPED, +] diff --git a/api-client/src/runs/index.ts b/api-client/src/runs/index.ts index fa38dade02f..1d62755d4c5 100644 --- a/api-client/src/runs/index.ts +++ b/api-client/src/runs/index.ts @@ -10,6 +10,6 @@ export { getCommands } from './commands/getCommands' export { createRunAction } from './createRunAction' export * from './createLabwareOffset' export * from './createLabwareDefinition' - +export * from './constants' export * from './types' export type { CreateRunData } from './createRun' diff --git a/api-client/src/runs/types.ts b/api-client/src/runs/types.ts index 7e6ec2b0ee7..36c5f9a3a20 100644 --- a/api-client/src/runs/types.ts +++ b/api-client/src/runs/types.ts @@ -4,6 +4,7 @@ import type { LoadedPipette, ModuleModel, RunTimeCommand, + RunTimeParameter, } from '@opentrons/shared-data' import type { ResourceLink, ErrorDetails } from '../types' export * from './commands/types' @@ -47,7 +48,7 @@ export interface LegacyGoodRunData { modules: LoadedModule[] protocolId?: string labwareOffsets?: LabwareOffset[] - runTimeParameterValues?: RunTimeParameterCreateData + runTimeParameters: RunTimeParameter[] } export interface KnownGoodRunData extends LegacyGoodRunData { diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx index ea7ec478415..6580a1fd7f1 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx @@ -1,6 +1,10 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' +import { + RUN_STATUS_STOPPED, + RUN_STATUSES_TERMINAL, +} from '@opentrons/api-client' import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { ALIGN_CENTER, @@ -25,6 +29,8 @@ import { Tooltip } from '../../../atoms/Tooltip' import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import type { RunTimeParameter } from '@opentrons/shared-data' +import { useRunStatus } from '../../RunTimeControl/hooks' +import { useNotifyRunQuery } from '../../../resources/runs' interface ProtocolRunRuntimeParametersProps { runId: string @@ -34,7 +40,15 @@ export function ProtocolRunRuntimeParameters({ }: ProtocolRunRuntimeParametersProps): JSX.Element { const { t } = useTranslation('protocol_setup') const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) - const runTimeParameters = mostRecentAnalysis?.runTimeParameters ?? [] + const runStatus = useRunStatus(runId) + const isRunTerminal = + // @ts-expect-error: expected that runStatus may not be a terminal status + runStatus == null ? false : RUN_STATUSES_TERMINAL.includes(runStatus) + const run = useNotifyRunQuery(runId).data + const runTimeParameters = + (isRunTerminal + ? run?.data?.runTimeParameters + : mostRecentAnalysis?.runTimeParameters) ?? [] const hasParameter = runTimeParameters.length > 0 const hasCustomValues = runTimeParameters.some( @@ -79,7 +93,11 @@ export function ProtocolRunRuntimeParameters({ {!hasParameter ? ( - + ) : ( <> diff --git a/app/src/organisms/InterventionModal/__fixtures__/index.ts b/app/src/organisms/InterventionModal/__fixtures__/index.ts index b6d631f4c97..2611fe19b03 100644 --- a/app/src/organisms/InterventionModal/__fixtures__/index.ts +++ b/app/src/organisms/InterventionModal/__fixtures__/index.ts @@ -188,6 +188,7 @@ export const mockRunData: RunData = { pipettes: [], labware: [mockLabwareOnModule, mockLabwareOnSlot, mockLabwareOffDeck], modules: [mockModule], + runTimeParameters: [], } export const mockLabwareRenderInfo = [ diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx b/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx index 85e956ed977..8bc3a481843 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx +++ b/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx @@ -26,6 +26,7 @@ const mockRun = { pipettes: [], protocolId: 'mockSortedProtocolID', status: 'stopped', + runTimeParameters: [], } const render = ( diff --git a/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts b/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts index 0858544d93c..fe6e3ab3649 100644 --- a/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts +++ b/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts @@ -1,10 +1,13 @@ import { useQueryClient } from 'react-query' -import { useHost, useCreateRunMutation } from '@opentrons/react-api-client' - +import { + useHost, + useCreateRunMutation, + useCreateProtocolAnalysisMutation, +} from '@opentrons/react-api-client' import { useNotifyRunQuery } from '../../../resources/runs' -import type { Run } from '@opentrons/api-client' +import type { Run, RunTimeParameterCreateData } from '@opentrons/api-client' interface UseCloneRunResult { cloneRun: () => void @@ -13,28 +16,45 @@ interface UseCloneRunResult { export function useCloneRun( runId: string | null, - onSuccessCallback?: (createRunResponse: Run) => unknown + onSuccessCallback?: (createRunResponse: Run) => unknown, + triggerAnalysis: boolean = false ): UseCloneRunResult { const host = useHost() const queryClient = useQueryClient() const { data: runRecord } = useNotifyRunQuery(runId) + const protocolKey = runRecord?.data.protocolId ?? null + const { createRun, isLoading } = useCreateRunMutation({ onSuccess: response => { - queryClient - .invalidateQueries([host, 'runs']) - .catch((e: Error) => - console.error(`error invalidating runs query: ${e.message}`) - ) + const invalidateRuns = queryClient.invalidateQueries([host, 'runs']) + const invalidateProtocols = queryClient.invalidateQueries([ + host, + 'protocols', + protocolKey, + ]) + Promise.all([invalidateRuns, invalidateProtocols]).catch((e: Error) => + console.error(`error invalidating runs query: ${e.message}`) + ) if (onSuccessCallback != null) onSuccessCallback(response) }, }) + const { createProtocolAnalysis } = useCreateProtocolAnalysisMutation( + protocolKey, + host + ) const cloneRun = (): void => { if (runRecord != null) { - const { - protocolId, - labwareOffsets, - runTimeParameterValues, - } = runRecord.data + const { protocolId, labwareOffsets, runTimeParameters } = runRecord.data + const runTimeParameterValues = runTimeParameters.reduce( + (acc, param) => + param.value !== param.default + ? { ...acc, [param.variableName]: param.value } + : acc, + {} + ) + if (triggerAnalysis && protocolKey != null) { + createProtocolAnalysis({ protocolKey, runTimeParameterValues }) + } createRun({ protocolId, labwareOffsets, runTimeParameterValues }) } else { console.info('failed to clone run record, source run record not found') diff --git a/app/src/organisms/RunPreview/index.tsx b/app/src/organisms/RunPreview/index.tsx index a75257c1952..48ea808064d 100644 --- a/app/src/organisms/RunPreview/index.tsx +++ b/app/src/organisms/RunPreview/index.tsx @@ -3,6 +3,8 @@ import { css } from 'styled-components' import { useTranslation } from 'react-i18next' import { ViewportList, ViewportListRef } from 'react-viewport-list' +import { RUN_STATUSES_TERMINAL } from '@opentrons/api-client' +import { useAllCommandsQuery } from '@opentrons/react-api-client' import { ALIGN_CENTER, BORDERS, @@ -24,6 +26,8 @@ import { CommandText } from '../CommandText' import { Divider } from '../../atoms/structure' import { NAV_BAR_WIDTH } from '../../App/constants' import { CommandIcon } from './CommandIcon' +import { useRunStatus } from '../RunTimeControl/hooks' + import type { RobotType } from '@opentrons/shared-data' const COLOR_FADE_MS = 500 @@ -41,6 +45,13 @@ export const RunPreviewComponent = ( ): JSX.Element | null => { const { t } = useTranslation('run_details') const robotSideAnalysis = useMostRecentCompletedAnalysis(runId) + const runStatus = useRunStatus(runId) + // @ts-expect-error: expected that runStatus may not be a terminal status + const isRunTerminal = RUN_STATUSES_TERMINAL.includes(runStatus) + const commandsFromQuery = useAllCommandsQuery(runId, null, { + staleTime: Infinity, + cacheTime: Infinity, + }).data?.data const viewPortRef = React.useRef(null) const currentRunCommandKey = useNotifyLastRunCommandKey(runId, { refetchInterval: LIVE_RUN_COMMANDS_POLL_MS, @@ -50,7 +61,9 @@ export const RunPreviewComponent = ( setIsCurrentCommandVisible, ] = React.useState(true) if (robotSideAnalysis == null) return null - const currentRunCommandIndex = robotSideAnalysis.commands.findIndex( + const commands = + (isRunTerminal ? commandsFromQuery : robotSideAnalysis.commands) ?? [] + const currentRunCommandIndex = commands.findIndex( c => c.key === currentRunCommandKey ) @@ -69,7 +82,7 @@ export const RunPreviewComponent = ( {t('run_preview')} - {t('steps_total', { count: robotSideAnalysis.commands.length })} + {t('steps_total', { count: commands.length })} @@ -79,7 +92,7 @@ export const RunPreviewComponent = ( ) : null} - {currentRunCommandIndex === robotSideAnalysis.commands.length - 1 ? ( + {currentRunCommandIndex === commands.length - 1 ? ( {t('end_of_protocol')} diff --git a/app/src/organisms/RunTimeControl/__fixtures__/index.ts b/app/src/organisms/RunTimeControl/__fixtures__/index.ts index 1a18a9a6bcf..33f2e0c4393 100644 --- a/app/src/organisms/RunTimeControl/__fixtures__/index.ts +++ b/app/src/organisms/RunTimeControl/__fixtures__/index.ts @@ -41,6 +41,7 @@ export const mockPausedRun: RunData = { pipettes: [], labware: [], modules: [], + runTimeParameters: [], } export const mockPauseRequestedRun: RunData = { @@ -65,6 +66,7 @@ export const mockPauseRequestedRun: RunData = { pipettes: [], labware: [], modules: [], + runTimeParameters: [], } export const mockRunningRun: RunData = { @@ -94,6 +96,7 @@ export const mockRunningRun: RunData = { pipettes: [], labware: [], modules: [], + runTimeParameters: [], } export const mockFailedRun: RunData = { @@ -133,6 +136,7 @@ export const mockFailedRun: RunData = { pipettes: [], labware: [], modules: [], + runTimeParameters: [], } export const mockStopRequestedRun: RunData = { @@ -167,6 +171,7 @@ export const mockStopRequestedRun: RunData = { pipettes: [], labware: [], modules: [], + runTimeParameters: [], } export const mockStoppedRun: RunData = { @@ -201,6 +206,7 @@ export const mockStoppedRun: RunData = { pipettes: [], labware: [], modules: [], + runTimeParameters: [], } export const mockSucceededRun: RunData = { @@ -230,6 +236,7 @@ export const mockSucceededRun: RunData = { pipettes: [], labware: [], modules: [], + runTimeParameters: [], } export const mockIdleUnstartedRun: RunData = { @@ -243,6 +250,7 @@ export const mockIdleUnstartedRun: RunData = { pipettes: [], labware: [], modules: [], + runTimeParameters: [], } export const mockIdleStartedRun: RunData = { @@ -272,6 +280,7 @@ export const mockIdleStartedRun: RunData = { pipettes: [], labware: [], modules: [], + runTimeParameters: [], } export const mockCommand = { diff --git a/app/src/organisms/RunTimeControl/hooks.ts b/app/src/organisms/RunTimeControl/hooks.ts index db042a2ce65..fee33ed29c9 100644 --- a/app/src/organisms/RunTimeControl/hooks.ts +++ b/app/src/organisms/RunTimeControl/hooks.ts @@ -52,7 +52,8 @@ export function useRunControls( const { cloneRun, isLoading: isResetRunLoading } = useCloneRun( runId ?? null, - onCloneRunSuccess + onCloneRunSuccess, + true ) return { diff --git a/components/src/molecules/ParametersTable/InfoScreen.tsx b/components/src/molecules/ParametersTable/InfoScreen.tsx index b9798f828e3..9affc1472b6 100644 --- a/components/src/molecules/ParametersTable/InfoScreen.tsx +++ b/components/src/molecules/ParametersTable/InfoScreen.tsx @@ -8,14 +8,25 @@ import { Flex } from '../../primitives' import { ALIGN_CENTER, DIRECTION_COLUMN } from '../../styles' interface InfoScreenProps { - contentType: 'parameters' | 'moduleControls' + contentType: 'parameters' | 'moduleControls' | 'runNotStarted' } export function InfoScreen({ contentType }: InfoScreenProps): JSX.Element { - const bodyText = - contentType === 'parameters' - ? 'No parameters specified in this protocol' - : 'Connect modules to see controls' + let bodyText: string = '' + switch (contentType) { + case 'parameters': + bodyText = 'No parameters specified in this protocol' + break + case 'moduleControls': + bodyText = 'Connect modules to see controls' + break + case 'runNotStarted': + bodyText = 'Run was never started' + break + default: + bodyText = contentType + } + return ( ( runId: string | null, - params: GetCommandsParams = DEFAULT_PARAMS, + params?: GetCommandsParams | null, options: UseQueryOptions = {} ): UseQueryResult { const host = useHost() + const nullCheckedParams = params ?? DEFAULT_PARAMS + const allOptions: UseQueryOptions = { ...options, enabled: host !== null && runId != null && options.enabled !== false, } - const { cursor, pageLength } = params + const { cursor, pageLength } = nullCheckedParams const query = useQuery( [host, 'runs', runId, 'commands', cursor, pageLength], () => { - return getCommands(host as HostConfig, runId as string, params).then( - response => response.data - ) + return getCommands( + host as HostConfig, + runId as string, + nullCheckedParams + ).then(response => response.data) }, allOptions )