From 7c3c12e597af9031a43e3e643c1e590f49ad77d1 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Fri, 12 Apr 2024 13:19:11 -0400 Subject: [PATCH] feat(app, api-client, react-api-client): add api-client method for protocol reanalysis (#14878) closes AUTH-118 --- .../src/protocols/createProtocolAnalysis.ts | 28 ++++++ api-client/src/protocols/index.ts | 1 + .../ProtocolSetupParameters.test.tsx | 16 ++-- .../ProtocolSetupParameters/index.tsx | 25 +++++- app/src/pages/ProtocolDetails/index.tsx | 5 +- app/src/pages/ProtocolSetup/index.tsx | 26 +++--- ...useCreateProtocolAnalysisMutation.test.tsx | 77 +++++++++++++++++ react-api-client/src/protocols/index.ts | 1 + .../useCreateProtocolAnalysisMutation.ts | 86 +++++++++++++++++++ 9 files changed, 241 insertions(+), 24 deletions(-) create mode 100644 api-client/src/protocols/createProtocolAnalysis.ts create mode 100644 react-api-client/src/protocols/__tests__/useCreateProtocolAnalysisMutation.test.tsx create mode 100644 react-api-client/src/protocols/useCreateProtocolAnalysisMutation.ts diff --git a/api-client/src/protocols/createProtocolAnalysis.ts b/api-client/src/protocols/createProtocolAnalysis.ts new file mode 100644 index 00000000000..81ab83c11af --- /dev/null +++ b/api-client/src/protocols/createProtocolAnalysis.ts @@ -0,0 +1,28 @@ +import { POST, request } from '../request' + +import type { ProtocolAnalysisSummary } from '@opentrons/shared-data' +import type { ResponsePromise } from '../request' +import type { HostConfig } from '../types' +import type { RunTimeParameterCreateData } from '../runs' + +interface CreateProtocolAnalysisData { + runTimeParameterValues: RunTimeParameterCreateData + forceReAnalyze: boolean +} + +export function createProtocolAnalysis( + config: HostConfig, + protocolKey: string, + runTimeParameterValues?: RunTimeParameterCreateData, + forceReAnalyze?: boolean +): ResponsePromise { + const data = { + runTimeParameterValues: runTimeParameterValues ?? {}, + forceReAnalyze: forceReAnalyze ?? false, + } + const response = request< + ProtocolAnalysisSummary[], + { data: CreateProtocolAnalysisData } + >(POST, `/protocols/${protocolKey}/analyses`, { data }, config) + return response +} diff --git a/api-client/src/protocols/index.ts b/api-client/src/protocols/index.ts index 6febd0795cf..f035fa000e1 100644 --- a/api-client/src/protocols/index.ts +++ b/api-client/src/protocols/index.ts @@ -3,6 +3,7 @@ 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' diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx b/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx index 1dc55314d59..4871eeaa379 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx +++ b/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx @@ -2,7 +2,11 @@ import * as React from 'react' import { when } from 'vitest-when' import { it, describe, beforeEach, vi, expect } from 'vitest' import { fireEvent, screen } from '@testing-library/react' -import { useCreateRunMutation, useHost } from '@opentrons/react-api-client' +import { + useCreateProtocolAnalysisMutation, + useCreateRunMutation, + useHost, +} from '@opentrons/react-api-client' import { i18n } from '../../../i18n' import { renderWithProviders } from '../../../__testing-utils__' import { ProtocolSetupParameters } from '..' @@ -24,6 +28,7 @@ vi.mock('react-router-dom', async importOriginal => { } }) const MOCK_HOST_CONFIG: HostConfig = { hostname: 'MOCK_HOST' } +const mockCreateProtocolAnalysis = vi.fn() const mockCreateRun = vi.fn() const render = ( props: React.ComponentProps @@ -43,6 +48,9 @@ describe('ProtocolSetupParameters', () => { } vi.mocked(ChooseEnum).mockReturnValue(
mock ChooseEnum
) vi.mocked(useHost).mockReturnValue(MOCK_HOST_CONFIG) + when(vi.mocked(useCreateProtocolAnalysisMutation)) + .calledWith(expect.anything(), expect.anything()) + .thenReturn({ createProtocolAnalysis: mockCreateProtocolAnalysis } as any) when(vi.mocked(useCreateRunMutation)) .calledWith(expect.anything()) .thenReturn({ createRun: mockCreateRun } as any) @@ -62,10 +70,9 @@ describe('ProtocolSetupParameters', () => { }) it('renders the other setting when boolean param is selected', () => { render(props) - screen.getByText('Off') - expect(screen.getAllByText('On')).toHaveLength(3) + expect(screen.getAllByText('On')).toHaveLength(2) fireEvent.click(screen.getByText('Dry Run')) - expect(screen.getAllByText('On')).toHaveLength(4) + expect(screen.getAllByText('On')).toHaveLength(3) }) it('renders the back icon and calls useHistory', () => { render(props) @@ -88,6 +95,5 @@ describe('ProtocolSetupParameters', () => { const title = screen.getByText('Reset parameter values?') fireEvent.click(screen.getByRole('button', { name: 'Go back' })) expect(title).not.toBeInTheDocument() - // TODO(jr, 3/19/24): wire up the confirm button }) }) diff --git a/app/src/organisms/ProtocolSetupParameters/index.tsx b/app/src/organisms/ProtocolSetupParameters/index.tsx index ac1f3fd700f..5dae07260f6 100644 --- a/app/src/organisms/ProtocolSetupParameters/index.tsx +++ b/app/src/organisms/ProtocolSetupParameters/index.tsx @@ -1,7 +1,11 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { useHistory } from 'react-router-dom' -import { useCreateRunMutation, useHost } from '@opentrons/react-api-client' +import { + useCreateProtocolAnalysisMutation, + useCreateRunMutation, + useHost, +} from '@opentrons/react-api-client' import { useQueryClient } from 'react-query' import { ALIGN_CENTER, @@ -51,7 +55,12 @@ export function ProtocolSetupParameters({ const [ runTimeParametersOverrides, setRunTimeParametersOverrides, - ] = React.useState(runTimeParameters) + ] = React.useState( + // present defaults rather than last-set value + runTimeParameters.map(param => { + return { ...param, value: param.default } + }) + ) const updateParameters = ( value: boolean | string | number, @@ -85,6 +94,14 @@ export function ProtocolSetupParameters({ } } + const runTimeParameterValues = getRunTimeParameterValuesForRun( + runTimeParametersOverrides + ) + const { createProtocolAnalysis } = useCreateProtocolAnalysisMutation( + protocolId, + host + ) + const { createRun, isLoading } = useCreateRunMutation({ onSuccess: data => { queryClient @@ -96,6 +113,10 @@ export function ProtocolSetupParameters({ }) const handleConfirmValues = (): void => { setStartSetup(true) + createProtocolAnalysis({ + protocolKey: protocolId, + runTimeParameterValues: runTimeParameterValues, + }) createRun({ protocolId, labwareOffsets, diff --git a/app/src/pages/ProtocolDetails/index.tsx b/app/src/pages/ProtocolDetails/index.tsx index 0503c0eae54..850fd0a8016 100644 --- a/app/src/pages/ProtocolDetails/index.tsx +++ b/app/src/pages/ProtocolDetails/index.tsx @@ -346,13 +346,12 @@ export function ProtocolDetails(): JSX.Element | null { let pinnedProtocolIds = useSelector(getPinnedProtocolIds) ?? [] const pinned = pinnedProtocolIds.includes(protocolId) - const { data: protocolData } = useProtocolQuery(protocolId) const { data: mostRecentAnalysis, } = useProtocolAnalysisAsDocumentQuery( protocolId, - last(protocolData?.data.analysisSummaries)?.id ?? null, - { enabled: protocolData != null } + last(protocolRecord?.data.analysisSummaries)?.id ?? null, + { enabled: protocolRecord != null } ) const shouldApplyOffsets = useSelector(getApplyHistoricOffsets) diff --git a/app/src/pages/ProtocolSetup/index.tsx b/app/src/pages/ProtocolSetup/index.tsx index 97499316f27..14b871f839c 100644 --- a/app/src/pages/ProtocolSetup/index.tsx +++ b/app/src/pages/ProtocolSetup/index.tsx @@ -69,7 +69,6 @@ import { getProtocolUsesGripper, } from '../../organisms/ProtocolSetupInstruments/utils' import { - useProtocolHasRunTimeParameters, useRunControls, useRunStatus, } from '../../organisms/RunTimeControl/hooks' @@ -257,9 +256,6 @@ function PrepareToRun({ const { t, i18n } = useTranslation(['protocol_setup', 'shared']) const history = useHistory() const { makeSnackbar } = useToaster() - const hasRunTimeParameters = useProtocolHasRunTimeParameters(runId) - console.log(hasRunTimeParameters) - // Watch for scrolling to toggle dropshadow const scrollRef = React.useRef(null) const [isScrolled, setIsScrolled] = React.useState(false) const observer = new IntersectionObserver(([entry]) => { @@ -366,6 +362,12 @@ function PrepareToRun({ }) const moduleCalibrationStatus = useModuleCalibrationStatus(robotName, runId) + const runTimeParameters = mostRecentAnalysis?.runTimeParameters ?? [] + const hasRunTimeParameters = runTimeParameters.length > 0 + const hasCustomRunTimeParameters = runTimeParameters.some( + parameter => parameter.value !== parameter.default + ) + const [ showConfirmCancelModal, setShowConfirmCancelModal, @@ -623,11 +625,11 @@ function PrepareToRun({ doorStatus?.data.status === 'open' && doorStatus?.data.doorRequiredClosedForProtocol - // TODO(Jr, 3/20/24): wire up custom values - const hasCustomValues = false - const parametersDetail = hasCustomValues - ? t('custom_values') - : t('default_values') + const parametersDetail = hasRunTimeParameters + ? hasCustomRunTimeParameters + ? t('custom_values') + : t('default_values') + : t('no_parameters_specified') return ( <> @@ -733,11 +735,7 @@ function PrepareToRun({ setSetupScreen('view only parameters')} title={t('parameters')} - detail={t( - hasRunTimeParameters - ? parametersDetail - : t('no_parameters_specified') - )} + detail={parametersDetail} subDetail={null} status="general" disabled={!hasRunTimeParameters} diff --git a/react-api-client/src/protocols/__tests__/useCreateProtocolAnalysisMutation.test.tsx b/react-api-client/src/protocols/__tests__/useCreateProtocolAnalysisMutation.test.tsx new file mode 100644 index 00000000000..e04c020fb1d --- /dev/null +++ b/react-api-client/src/protocols/__tests__/useCreateProtocolAnalysisMutation.test.tsx @@ -0,0 +1,77 @@ +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 { createProtocolAnalysis } from '@opentrons/api-client' +import { useHost } from '../../api' +import { useCreateProtocolAnalysisMutation } from '..' +import type { HostConfig, Response } from '@opentrons/api-client' +import type { ProtocolAnalysisSummary } from '@opentrons/shared-data' + +vi.mock('@opentrons/api-client') +vi.mock('../../api/useHost') + +const HOST_CONFIG: HostConfig = { hostname: 'localhost' } +const ANALYSIS_SUMMARY_RESPONSE = [ + { id: 'fakeAnalysis1', status: 'completed' }, + { id: 'fakeAnalysis2', status: 'pending' }, +] as ProtocolAnalysisSummary[] + +describe('useCreateProtocolAnalysisMutation hook', () => { + 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 when calling createProtocolAnalysis if the request fails', async () => { + vi.mocked(useHost).mockReturnValue(HOST_CONFIG) + vi.mocked(createProtocolAnalysis).mockRejectedValue('oh no') + + const { result } = renderHook( + () => useCreateProtocolAnalysisMutation('fake-protocol-key'), + { + wrapper, + } + ) + + expect(result.current.data).toBeUndefined() + result.current.createProtocolAnalysis({ + protocolKey: 'fake-protocol-key', + runTimeParameterValues: {}, + }) + await waitFor(() => { + expect(result.current.data).toBeUndefined() + }) + }) + + it('should create an array of ProtocolAnalysisSummaries when calling the createProtocolAnalysis callback', async () => { + vi.mocked(useHost).mockReturnValue(HOST_CONFIG) + vi.mocked(createProtocolAnalysis).mockResolvedValue({ + data: ANALYSIS_SUMMARY_RESPONSE, + } as Response) + + const { result } = renderHook( + () => useCreateProtocolAnalysisMutation('fake-protocol-key'), + { + wrapper, + } + ) + act(() => + result.current.createProtocolAnalysis({ + protocolKey: 'fake-protocol-key', + runTimeParameterValues: {}, + }) + ) + + await waitFor(() => { + expect(result.current.data).toEqual(ANALYSIS_SUMMARY_RESPONSE) + }) + }) +}) diff --git a/react-api-client/src/protocols/index.ts b/react-api-client/src/protocols/index.ts index ddf7c3eeaac..561dee01e8b 100644 --- a/react-api-client/src/protocols/index.ts +++ b/react-api-client/src/protocols/index.ts @@ -4,4 +4,5 @@ export { useProtocolQuery } from './useProtocolQuery' export { useProtocolAnalysesQuery } from './useProtocolAnalysesQuery' export { useProtocolAnalysisAsDocumentQuery } from './useProtocolAnalysisAsDocumentQuery' export { useCreateProtocolMutation } from './useCreateProtocolMutation' +export { useCreateProtocolAnalysisMutation } from './useCreateProtocolAnalysisMutation' export { useDeleteProtocolMutation } from './useDeleteProtocolMutation' diff --git a/react-api-client/src/protocols/useCreateProtocolAnalysisMutation.ts b/react-api-client/src/protocols/useCreateProtocolAnalysisMutation.ts new file mode 100644 index 00000000000..f8ba6e10586 --- /dev/null +++ b/react-api-client/src/protocols/useCreateProtocolAnalysisMutation.ts @@ -0,0 +1,86 @@ +import { createProtocolAnalysis } from '@opentrons/api-client' +import { useMutation, useQueryClient } from 'react-query' +import { useHost } from '../api' +import type { + ErrorResponse, + HostConfig, + RunTimeParameterCreateData, +} from '@opentrons/api-client' +import type { ProtocolAnalysisSummary } from '@opentrons/shared-data' +import type { AxiosError } from 'axios' +import type { + UseMutationResult, + UseMutationOptions, + UseMutateFunction, +} from 'react-query' + +export interface CreateProtocolAnalysisVariables { + protocolKey: string + runTimeParameterValues?: RunTimeParameterCreateData + forceReAnalyze?: boolean +} +export type UseCreateProtocolMutationResult = UseMutationResult< + ProtocolAnalysisSummary[], + AxiosError, + CreateProtocolAnalysisVariables +> & { + createProtocolAnalysis: UseMutateFunction< + ProtocolAnalysisSummary[], + AxiosError, + CreateProtocolAnalysisVariables + > +} + +export type UseCreateProtocolAnalysisMutationOptions = UseMutationOptions< + ProtocolAnalysisSummary[], + AxiosError, + CreateProtocolAnalysisVariables +> + +export function useCreateProtocolAnalysisMutation( + protocolId: string | null, + hostOverride?: HostConfig | null, + options: UseCreateProtocolAnalysisMutationOptions | undefined = {} +): UseCreateProtocolMutationResult { + const contextHost = useHost() + const host = + hostOverride != null ? { ...contextHost, ...hostOverride } : contextHost + const queryClient = useQueryClient() + + const mutation = useMutation< + ProtocolAnalysisSummary[], + AxiosError, + CreateProtocolAnalysisVariables + >( + [host, 'protocols', protocolId, 'analyses'], + ({ protocolKey, runTimeParameterValues, forceReAnalyze }) => + createProtocolAnalysis( + host as HostConfig, + protocolKey, + runTimeParameterValues, + forceReAnalyze + ) + .then(response => { + queryClient + .invalidateQueries([host, 'protocols', protocolId, 'analyses']) + .then(() => + queryClient.setQueryData( + [host, 'protocols', protocolId, 'analyses'], + response.data + ) + ) + .catch((e: Error) => { + throw e + }) + return response.data + }) + .catch((e: Error) => { + throw e + }), + options + ) + return { + ...mutation, + createProtocolAnalysis: mutation.mutate, + } +}