From fc620165858b37fefb4a5262d7375959678e91fd Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:56:54 -0400 Subject: [PATCH] feat(app): add runtime parameters to ChooseProtocolSlideout (#14781) closes [AUTH-246](https://opentrons.atlassian.net/browse/AUTH-246) --- app/src/assets/localization/en/shared.json | 1 + .../ChooseProtocolSlideout/index.tsx | 318 ++++++++++++++++-- 2 files changed, 292 insertions(+), 27 deletions(-) diff --git a/app/src/assets/localization/en/shared.json b/app/src/assets/localization/en/shared.json index 8c8bed0a5af..adb939134f8 100644 --- a/app/src/assets/localization/en/shared.json +++ b/app/src/assets/localization/en/shared.json @@ -6,6 +6,7 @@ "before_you_begin": "Before you begin", "browse": "browse", "cancel": "cancel", + "change_protocol": "Change protocol", "change_robot": "Change robot", "clear_data": "clear data", "close_robot_door": "Close the robot door before starting the run.", diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index b6d1d2805ff..859b1ac4cd9 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -12,30 +12,42 @@ import { Box, COLORS, DIRECTION_COLUMN, + DIRECTION_ROW, DISPLAY_BLOCK, + DropdownOption, Flex, Icon, + Link as LinkComponent, JUSTIFY_CENTER, + JUSTIFY_END, + JUSTIFY_FLEX_START, OVERFLOW_WRAP_ANYWHERE, PrimaryButton, ProtocolDeck, - SIZE_1, SPACING, + SecondaryButton, StyledText, TYPOGRAPHY, + useHoverTooltip, } from '@opentrons/components' import { useLogger } from '../../logger' import { OPENTRONS_USB } from '../../redux/discovery' import { getStoredProtocols } from '../../redux/protocol-storage' import { appShellRequestor } from '../../redux/shell/remote' -import { Slideout } from '../../atoms/Slideout' +import { useFeatureFlag } from '../../redux/config' +import { MultiSlideout } from '../../atoms/Slideout/MultiSlideout' +import { Tooltip } from '../../atoms/Tooltip' +import { ToggleButton } from '../../atoms/buttons' +import { InputField } from '../../atoms/InputField' +import { DropdownMenu } from '../../atoms/MenuList/DropdownMenu' import { MiniCard } from '../../molecules/MiniCard' import { useTrackCreateProtocolRunEvent } from '../Devices/hooks' import { useCreateRunFromProtocol } from '../ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol' import { ApplyHistoricOffsets } from '../ApplyHistoricOffsets' import { useOffsetCandidatesForAnalysis } from '../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { getAnalysisStatus } from '../ProtocolsLanding/utils' +import type { RunTimeParameter } from '@opentrons/shared-data' import type { Robot } from '../../redux/discovery/types' import type { StoredProtocolData } from '../../redux/protocol-storage' import type { State } from '../../redux/types' @@ -65,6 +77,8 @@ export function ChooseProtocolSlideoutComponent( const { t } = useTranslation(['device_details', 'shared']) const history = useHistory() const logger = useLogger(new URL('', import.meta.url).pathname) + const [targetProps, tooltipProps] = useHoverTooltip() + const { robot, showSlideout, onCloseClick } = props const { name } = robot @@ -72,6 +86,24 @@ export function ChooseProtocolSlideoutComponent( selectedProtocol, setSelectedProtocol, ] = React.useState(null) + const [ + runTimeParametersOverrides, + setRunTimeParametersOverrides, + ] = React.useState([]) + const [currentPage, setCurrentPage] = React.useState(1) + const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') + + React.useEffect(() => { + setRunTimeParametersOverrides( + selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] + ) + }, [selectedProtocol]) + const runTimeParametersFromAnalysis = + selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] + + const hasRunTimeParameters = + enableRunTimeParametersFF && runTimeParametersFromAnalysis.length > 0 + const analysisStatus = getAnalysisStatus( false, selectedProtocol?.mostRecentAnalysis @@ -128,7 +160,14 @@ export function ChooseProtocolSlideoutComponent( location, definitionUri, })) - : [] + : [], + runTimeParametersOverrides.reduce( + (acc, param) => + param.value !== param.default + ? { ...acc, [param.variableName]: param.value } + : acc, + {} + ) ) const handleProceed: React.MouseEventHandler = () => { if (selectedProtocol != null) { @@ -141,10 +180,226 @@ export function ChooseProtocolSlideoutComponent( logger.warn('failed to create protocol, no protocol selected') } } + + const isRestoreDefaultsLinkEnabled = + runTimeParametersOverrides?.some( + parameter => parameter.value !== parameter.default + ) ?? false + + const runTimeParametersInputs = + runTimeParametersOverrides?.map((runtimeParam, index) => { + if ('choices' in runtimeParam) { + const dropdownOptions = runtimeParam.choices.map(choice => { + return { name: choice.displayName, value: choice.value } + }) as DropdownOption[] + return ( + { + return choice.value === runtimeParam.value + }) ?? dropdownOptions[0] + } + onClick={choice => { + const clone = runTimeParametersOverrides.map((parameter, i) => { + if (i === index) { + return { + ...parameter, + value: + dropdownOptions.find(option => option.value === choice) + ?.value ?? parameter.default, + } + } + return parameter + }) + setRunTimeParametersOverrides(clone) + }} + title={runtimeParam.displayName} + caption={runtimeParam.description} + width="100%" + dropdownType="neutral" + /> + ) + } else if (runtimeParam.type === 'int' || runtimeParam.type === 'float') { + const value = runtimeParam.value as number + const id = `InputField_${runtimeParam.variableName}_${index.toString()}` + const error = + Number.isNaN(value) || + value < runtimeParam.min || + value > runtimeParam.max + ? t(`protocol_details:value_out_of_range`, { + min: + runtimeParam.type === 'int' + ? runtimeParam.min + : runtimeParam.min.toFixed(1), + max: + runtimeParam.type === 'int' + ? runtimeParam.max + : runtimeParam.max.toFixed(1), + }) + : null + return ( + { + const clone = runTimeParametersOverrides.map((parameter, i) => { + if (i === index) { + return { + ...parameter, + value: + runtimeParam.type === 'int' + ? Math.round(e.target.valueAsNumber) + : e.target.valueAsNumber, + } + } + return parameter + }) + setRunTimeParametersOverrides(clone) + }} + /> + ) + } else if (runtimeParam.type === 'bool') { + return ( + + + {runtimeParam.displayName} + + + { + const clone = runTimeParametersOverrides.map( + (parameter, i) => { + if (i === index) { + return { + ...parameter, + value: !parameter.value, + } + } + return parameter + } + ) + setRunTimeParametersOverrides(clone) + }} + height="0.813rem" + label={ + runtimeParam.value + ? t('protocol_details:on') + : t('protocol_details:off') + } + paddingTop={SPACING.spacing2} // manual alignment of SVG with value label + /> + + {runtimeParam.value + ? t('protocol_details:on') + : t('protocol_details:off')} + + + + {runtimeParam.description} + + + ) + } + }) ?? null + + const pageTwoBody = ( + + + { + const clone = runTimeParametersOverrides.map(parameter => ({ + ...parameter, + value: parameter.default, + })) + setRunTimeParametersOverrides(clone) + }} + paddingBottom={SPACING.spacing10} + {...targetProps} + > + {t('protocol_details:restore_defaults')} + + {!isRestoreDefaultsLinkEnabled && ( + + {t('protocol_details:no_custom_values')} + + )} + + + {runTimeParametersInputs} + + + ) + + const singlePageFooter = ( + + {isCreatingRun ? ( + + ) : ( + t('shared:proceed_to_setup') + )} + + ) + + const multiPageFooter = + currentPage === 1 ? ( + setCurrentPage(2)} + width="100%" + disabled={isCreatingRun || selectedProtocol == null} + > + {t('shared:continue_to_param')} + + ) : ( + + setCurrentPage(1)} width="51%"> + {t('shared:change_protocol')} + + + {isCreatingRun ? ( + + ) : ( + t('shared:confirm_values') + )} + + + ) + return ( - - - {isCreatingRun ? ( - - ) : ( - t('shared:proceed_to_setup') - )} - + {hasRunTimeParameters ? multiPageFooter : singlePageFooter} } > {showSlideout ? ( - { - if (!isCreatingRun) { - resetCreateRun() - setSelectedProtocol(storedProtocol) - } - }} - robotName={robot.name} - {...{ selectedProtocol, runCreationError, runCreationErrorCode }} - /> + currentPage === 1 ? ( + { + if (!isCreatingRun) { + resetCreateRun() + setSelectedProtocol(storedProtocol) + } + }} + robotName={robot.name} + {...{ selectedProtocol, runCreationError, runCreationErrorCode }} + /> + ) : ( + pageTwoBody + ) ) : null} - + ) } @@ -225,7 +474,7 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { runCreationErrorCode, robotName, } = props - const { t } = useTranslation(['device_details', 'shared']) + const { t } = useTranslation(['device_details', 'protocol_details', 'shared']) const storedProtocols = useSelector((state: State) => getStoredProtocols(state) ) @@ -401,3 +650,18 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { ) } + +const ENABLED_LINK_CSS = css` + ${TYPOGRAPHY.linkPSemiBold} + cursor: pointer; +` + +const DISABLED_LINK_CSS = css` + ${TYPOGRAPHY.linkPSemiBold} + color: ${COLORS.grey40}; + cursor: default; + + &:hover { + color: ${COLORS.grey40}; + } +`