From 3003a1da92d5ec362a867b7e206a4ddbe633143c Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Thu, 29 Aug 2024 13:56:55 -0400 Subject: [PATCH] chore(merge): Merge head of `chore_release` into `edge` (#16161) --- api/docs/static/override_sphinx.css | 30 ++++- .../v2/parameters/use_case_cherrypicking.rst | 1 + .../protocol_api/core/engine/deck_conflict.py | 57 ++++++--- .../protocol_api/instrument_context.py | 1 - .../protocol_api/protocol_context.py | 1 + .../state/addressable_areas.py | 14 +++ .../protocol_engine/state/geometry.py | 13 +- .../protocol_engine/state/pipettes.py | 2 - .../core/engine/test_deck_conflict.py | 12 +- .../test_pipette_movement_deck_conflicts.py | 7 -- .../state/test_addressable_area_state.py | 6 + .../state/test_addressable_area_store.py | 18 +++ .../state/test_addressable_area_view.py | 6 + .../state/test_geometry_view.py | 6 + .../state/test_module_store.py | 6 + .../protocol_engine/state/test_module_view.py | 6 + .../protocol_engine/state/test_state_store.py | 6 + app-shell/build/release-notes.md | 1 + .../localization/en/protocol_setup.json | 111 +++++++++--------- .../localization/en/quick_transfer.json | 2 +- .../NumericalKeyboard/index.tsx | 5 + app/src/molecules/UploadInput/index.tsx | 3 +- .../organisms/CalibrationTaskList/index.tsx | 1 + .../ChooseProtocolSlideout/index.tsx | 11 +- .../index.tsx | 13 +- .../HistoricalProtocolRunOverflowMenu.tsx | 3 +- .../Devices/ProtocolRun/ProtocolRunHeader.tsx | 29 ++--- .../ProtocolRunRunTimeParameters.tsx | 43 ++++--- .../SetupFlexPipetteCalibrationItem.tsx | 9 +- .../SetupLabware/SetupLabwareMap.tsx | 5 + .../ProtocolRunRuntimeParameters.test.tsx | 52 +++++++- .../organisms/Devices/RecentProtocolRuns.tsx | 7 +- .../hooks/useFailedLabwareUtils.ts | 5 + .../ErrorRecoveryFlows/shared/SelectTips.tsx | 2 + .../shared/TipSelectionModal.tsx | 15 ++- .../shared/__tests__/SelectTips.test.tsx | 22 ++++ .../__tests__/TipSelectionModal.test.tsx | 18 ++- .../ModuleCard/__tests__/ModuleCard.test.tsx | 8 +- app/src/organisms/ModuleCard/index.tsx | 11 +- app/src/organisms/Navigation/index.tsx | 73 +++++++----- .../RobotDashboard/RecentRunProtocolCard.tsx | 3 +- .../RunningProtocol/ConfirmCancelRunModal.tsx | 23 ++-- .../__tests__/ConfirmCancelRunModal.test.tsx | 23 +--- .../ProtocolAnalysisFailure/index.tsx | 2 +- app/src/organisms/ProtocolDetails/index.tsx | 19 ++- .../ProtocolSetupLabware/LabwareMapView.tsx | 3 + .../organisms/ProtocolSetupLabware/index.tsx | 1 + .../organisms/ProtocolSetupLiquids/index.tsx | 1 + .../organisms/ProtocolSetupOffsets/index.tsx | 33 ++++-- .../QuickTransferAdvancedSettings/AirGap.tsx | 15 +-- .../QuickTransferAdvancedSettings/BlowOut.tsx | 34 +++--- .../QuickTransferAdvancedSettings/Delay.tsx | 15 +-- .../FlowRate.tsx | 1 + .../QuickTransferAdvancedSettings/Mix.tsx | 15 +-- .../PipettePath.tsx | 29 ++--- .../TipPosition.tsx | 1 + .../TouchTip.tsx | 17 ++- .../QuickTransferFlow/SelectDestLabware.tsx | 4 +- .../QuickTransferFlow/SelectDestWells.tsx | 3 +- .../QuickTransferFlow/SelectSourceWells.tsx | 3 +- .../__tests__/SelectDestLabware.test.tsx | 4 +- .../utils/generateQuickTransferArgs.ts | 21 ++-- app/src/organisms/RunPreview/index.tsx | 2 - .../Devices/ProtocolRunDetails/index.tsx | 7 +- .../pages/ProtocolDashboard/ProtocolCard.tsx | 15 ++- app/src/pages/ProtocolDetails/index.tsx | 2 +- app/src/pages/QuickTransferDetails/index.tsx | 4 +- app/src/pages/RunSummary/index.tsx | 13 +- app/src/redux/robot-api/http.ts | 21 +++- .../src/atoms/buttons/SecondaryButton.tsx | 1 + .../src/hardware-sim/BaseDeck/BaseDeck.tsx | 80 ++++++++++++- .../hardware-sim/DeckConfigurator/index.tsx | 7 +- .../Labware/LabwareAdapter/index.tsx | 22 +++- .../hardware-sim/Labware/LabwareRender.tsx | 4 + .../labwareInternals/LabwareOutline.tsx | 36 ++++-- .../labwareInternals/StaticLabware.tsx | 4 + .../src/hardware-sim/ProtocolDeck/index.tsx | 5 + .../molecules/ParametersTable/InfoScreen.tsx | 59 +++++++--- .../service/legacy/routers/networking.py | 18 ++- .../notifications/publisher_notifier.py | 26 ++-- .../tests/runs/test_run_controller.py | 6 + .../tests/runs/test_run_data_manager.py | 4 + .../service/legacy/routers/test_networking.py | 29 +++++ .../opentrons_shared_data/robot/types.py | 10 ++ shared-data/robot/definitions/1/ot2.json | 6 + shared-data/robot/definitions/1/ot3.json | 6 + shared-data/robot/schemas/1.json | 23 ++++ 87 files changed, 953 insertions(+), 368 deletions(-) diff --git a/api/docs/static/override_sphinx.css b/api/docs/static/override_sphinx.css index 20e923f16c4..10a1d091b94 100644 --- a/api/docs/static/override_sphinx.css +++ b/api/docs/static/override_sphinx.css @@ -1,11 +1,12 @@ -@import url('https://fonts.googleapis.com/css?family=Open+Sans:300,400,400i,600,700'); +@import url('https://fonts.googleapis.com/css?family=Public+Sans:300,400,400i,600,700'); +@import url('https://fonts.googleapis.com/css2?family=Reddit+Mono:wght@200..900&display=swap'); /* OT NAV */ body { padding: 0; margin: 0; - font-family: "Open Sans", "sans-serif"; + font-family: "Public Sans", "sans-serif"; } .highlight-none, .mi, .literal { @@ -35,7 +36,18 @@ div.document [id] { div.body p { line-height: 20pt; - font-family: "Open Sans", "sans-serif"; + font-family: "Public Sans", "sans-serif"; +} + +pre, tt, code { + font-size: 0.9em; + font-family: "Reddit Mono", "Consolas", "Lucida Console", monospace; +} + +/* classes for API Reference docstring signatures */ +.sig, .sig-name, code.descname, .sig-prename, .optional, .sig-paren { + font-size: 1em; + font-family: "Reddit Mono", "Consolas", "Lucida Console", monospace; } div.body h1 { @@ -90,7 +102,13 @@ div.body h2, div.body h3, div.body h4, div.body h5 { - font-family: "Open Sans", "sans-serif"; + font-family: "Public Sans", "sans-serif"; +} + +/* Links need an extra two pixels of padding to compensate between body font height +being 1em and code font height being 0.9em */ +a.reference { + padding-bottom: 2px; } /* Suppressing the display of the toctrees rendered in the doc body means we @@ -193,13 +211,13 @@ div.body p.caption { ul { /* margin-left: 0; */ - font-family: "Open Sans", "sans-serif"; + font-family: "Public Sans", "sans-serif"; } ul ul { list-style-type: circle; margin-left: 30px; - font-family: "Open Sans", "sans-serif"; + font-family: "Public Sans", "sans-serif"; } @media screen and (min-device-width: 320px)and (max-device-width: 640px) { diff --git a/api/docs/v2/parameters/use_case_cherrypicking.rst b/api/docs/v2/parameters/use_case_cherrypicking.rst index 75d2345edde..1a95980b6ae 100644 --- a/api/docs/v2/parameters/use_case_cherrypicking.rst +++ b/api/docs/v2/parameters/use_case_cherrypicking.rst @@ -123,6 +123,7 @@ The entire start of the ``run()`` function, including a pipette and fixed labwar instrument_name="flex_1channel_1000", mount="left", tip_racks=[tiprack] + ) # load trash bin trash = protocol.load_trash_bin("A3") ) diff --git a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py index 405aa2256a7..6ebb47f0ac8 100644 --- a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py +++ b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py @@ -416,23 +416,48 @@ def _is_within_pipette_extents( pipette_bounding_box_at_loc: Tuple[Point, Point, Point, Point], ) -> bool: """Whether a given point is within the extents of a configured pipette on the specified robot.""" - mount = engine_state.pipettes.get_mount(pipette_id) - robot_extent_per_mount = engine_state.geometry.absolute_deck_extents - pip_back_left_bound, pip_front_right_bound, _, _ = pipette_bounding_box_at_loc - pipette_bounds_offsets = engine_state.pipettes.get_pipette_bounding_box(pipette_id) - from_back_right = ( - robot_extent_per_mount.back_right[mount] - + pipette_bounds_offsets.back_right_corner - ) - from_front_left = ( - robot_extent_per_mount.front_left[mount] - + pipette_bounds_offsets.front_left_corner - ) + channels = engine_state.pipettes.get_channels(pipette_id) + robot_extents = engine_state.geometry.absolute_deck_extents + ( + pip_back_left_bound, + pip_front_right_bound, + pip_back_right_bound, + pip_front_left_bound, + ) = pipette_bounding_box_at_loc + + # Given the padding values accounted for against the deck extents, + # a pipette is within extents when all of the following are true: + + # Each corner slot full pickup case: + # A1: Front right nozzle is within the rear and left-side padding limits + # D1: Back right nozzle is within the front and left-side padding limits + # A3 Front left nozzle is within the rear and right-side padding limits + # D3: Back left nozzle is within the front and right-side padding limits + # Thermocycler Column A2: Front right nozzle is within padding limits + + if channels == 96: + return ( + pip_front_right_bound.y + <= robot_extents.deck_extents.y + robot_extents.padding_rear + and pip_front_right_bound.x >= robot_extents.padding_left_side + and pip_back_right_bound.y >= robot_extents.padding_front + and pip_back_right_bound.x >= robot_extents.padding_left_side + and pip_front_left_bound.y + <= robot_extents.deck_extents.y + robot_extents.padding_rear + and pip_front_left_bound.x + <= robot_extents.deck_extents.x + robot_extents.padding_right_side + and pip_back_left_bound.y >= robot_extents.padding_front + and pip_back_left_bound.x + <= robot_extents.deck_extents.x + robot_extents.padding_right_side + ) + # For 8ch pipettes we only check the rear and front extents return ( - from_back_right.x >= pip_back_left_bound.x >= from_front_left.x - and from_back_right.y >= pip_back_left_bound.y >= from_front_left.y - and from_back_right.x >= pip_front_right_bound.x >= from_front_left.x - and from_back_right.y >= pip_front_right_bound.y >= from_front_left.y + pip_front_right_bound.y + <= robot_extents.deck_extents.y + robot_extents.padding_rear + and pip_back_right_bound.y >= robot_extents.padding_front + and pip_front_left_bound.y + <= robot_extents.deck_extents.y + robot_extents.padding_rear + and pip_back_left_bound.y >= robot_extents.padding_front ) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 56dff7f425e..7121567c3c4 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -263,7 +263,6 @@ def aspirate( and self._96_tip_config_valid() ): self.require_liquid_presence(well=well) - self.prepare_to_aspirate() with publisher.publish_context( broker=self.broker, diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index 687c2277c0a..0fca3fdc8f3 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -932,6 +932,7 @@ def load_instrument( from the Opentrons App or touchscreen. :param bool liquid_presence_detection: If ``True``, enable automatic :ref:`liquid presence detection ` for Flex 1-, 8-, or 96-channel pipettes. + .. versionadded:: 2.20 """ instrument_name = validation.ensure_lowercase_name(instrument_name) diff --git a/api/src/opentrons/protocol_engine/state/addressable_areas.py b/api/src/opentrons/protocol_engine/state/addressable_areas.py index cd921ffa816..afd076380f7 100644 --- a/api/src/opentrons/protocol_engine/state/addressable_areas.py +++ b/api/src/opentrons/protocol_engine/state/addressable_areas.py @@ -352,6 +352,20 @@ def mount_offsets(self) -> Dict[str, Point]: "right": Point(x=right_offset[0], y=right_offset[1], z=right_offset[2]), } + @cached_property + def padding_offsets(self) -> Dict[str, float]: + """The padding offsets to be applied to the deck extents of the robot.""" + rear_offset = self.state.robot_definition["paddingOffsets"]["rear"] + front_offset = self.state.robot_definition["paddingOffsets"]["front"] + left_side_offset = self.state.robot_definition["paddingOffsets"]["leftSide"] + right_side_offset = self.state.robot_definition["paddingOffsets"]["rightSide"] + return { + "rear": rear_offset, + "front": front_offset, + "left_side": left_side_offset, + "right_side": right_side_offset, + } + def get_addressable_area(self, addressable_area_name: str) -> AddressableArea: """Get addressable area.""" if not self._state.use_simulated_deck_config: diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 9be6f7e5952..b7c6f950362 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -77,6 +77,11 @@ class _GripperMoveType(enum.Enum): class _AbsoluteRobotExtents: front_left: Dict[MountType, Point] back_right: Dict[MountType, Point] + deck_extents: Point + padding_rear: float + padding_front: float + padding_left_side: float + padding_right_side: float _LabwareLocation = TypeVar("_LabwareLocation", bound=LabwareLocation) @@ -118,7 +123,13 @@ def absolute_deck_extents(self) -> _AbsoluteRobotExtents: MountType.RIGHT: self._addressable_areas.deck_extents + right_offset, } return _AbsoluteRobotExtents( - front_left=front_left_abs, back_right=back_right_abs + front_left=front_left_abs, + back_right=back_right_abs, + deck_extents=self._addressable_areas.deck_extents, + padding_rear=self._addressable_areas.padding_offsets["rear"], + padding_front=self._addressable_areas.padding_offsets["front"], + padding_left_side=self._addressable_areas.padding_offsets["left_side"], + padding_right_side=self._addressable_areas.padding_offsets["right_side"], ) def get_labware_highest_z(self, labware_id: str) -> float: diff --git a/api/src/opentrons/protocol_engine/state/pipettes.py b/api/src/opentrons/protocol_engine/state/pipettes.py index d6ded2bb6eb..e6a407ef562 100644 --- a/api/src/opentrons/protocol_engine/state/pipettes.py +++ b/api/src/opentrons/protocol_engine/state/pipettes.py @@ -845,8 +845,6 @@ def get_pipette_bounds_at_specified_move_to_position( - primary_nozzle_offset + pipette_bounds_offsets.front_right_corner ) - # TODO (spp, 2024-02-27): remove back right & front left; - # return only back left and front right points. pip_back_right_bound = Point( pip_front_right_bound.x, pip_back_left_bound.y, pip_front_right_bound.z ) diff --git a/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py b/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py index d0171bff798..147368e0734 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py @@ -510,6 +510,11 @@ def test_deck_conflict_raises_for_bad_pipette_move( MountType.LEFT: Point(463.7, 433.3, 0.0), MountType.RIGHT: Point(517.7, 433.3), }, + deck_extents=Point(477.2, 493.8, 0.0), + padding_rear=-181.21, + padding_front=55.8, + padding_left_side=31.88, + padding_right_side=-80.32, ) ) decoy.when( @@ -677,6 +682,11 @@ def test_deck_conflict_raises_for_collision_with_tc_lid( MountType.LEFT: Point(463.7, 433.3, 0.0), MountType.RIGHT: Point(517.7, 433.3), }, + deck_extents=Point(477.2, 493.8, 0.0), + padding_rear=-181.21, + padding_front=55.8, + padding_left_side=31.88, + padding_right_side=-80.32, ) ) @@ -696,7 +706,7 @@ def test_deck_conflict_raises_for_collision_with_tc_lid( ) with pytest.raises( deck_conflict.PartialTipMovementNotAllowedError, - match="collision with thermocycler lid in deck slot A1.", + match="Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.", ): deck_conflict.check_safe_for_pipette_movement( engine_state=mock_state_view, diff --git a/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py b/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py index 59523fd2c91..1d3388d3d97 100644 --- a/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py +++ b/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py @@ -61,13 +61,6 @@ def test_deck_conflicts_for_96_ch_a12_column_configuration() -> None: ): instrument.pick_up_tip(badly_placed_tiprack.wells_by_name()["A1"]) - with pytest.raises( - PartialTipMovementNotAllowedError, match="outside of robot bounds" - ): - # Picking up from A1 in an east-most slot using a configuration with column 12 would - # result in a collision with the side of the robot. - instrument.pick_up_tip(well_placed_tiprack.wells_by_name()["A1"]) - instrument.pick_up_tip(well_placed_tiprack.wells_by_name()["A12"]) instrument.aspirate(50, well_placed_labware.wells_by_name()["A4"]) diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_state.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_state.py index 987db0dcba3..da3e0f3d156 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_state.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_state.py @@ -33,6 +33,12 @@ def test_deck_configuration_setting( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py index 0ec4e749aff..b259e6a3f96 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py @@ -74,6 +74,12 @@ def simulated_subject( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], @@ -101,6 +107,12 @@ def subject( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], @@ -127,6 +139,12 @@ def test_initial_state_simulated( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py index 07552aa4273..30ca1b9e7c4 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py @@ -69,6 +69,12 @@ def get_addressable_area_view( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index a3be9ec5c9b..377a95f961e 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -184,6 +184,12 @@ def addressable_area_store( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], diff --git a/api/tests/opentrons/protocol_engine/state/test_module_store.py b/api/tests/opentrons/protocol_engine/state/test_module_store.py index 5a26fc97d1a..4f94ed314d5 100644 --- a/api/tests/opentrons/protocol_engine/state/test_module_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_module_store.py @@ -79,6 +79,12 @@ def get_addressable_area_view( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], diff --git a/api/tests/opentrons/protocol_engine/state/test_module_view.py b/api/tests/opentrons/protocol_engine/state/test_module_view.py index 95b868497d2..3a5f14f1516 100644 --- a/api/tests/opentrons/protocol_engine/state/test_module_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_module_view.py @@ -92,6 +92,12 @@ def get_addressable_area_view( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], diff --git a/api/tests/opentrons/protocol_engine/state/test_state_store.py b/api/tests/opentrons/protocol_engine/state/test_state_store.py index 56b4b808086..9cebd0a80d2 100644 --- a/api/tests/opentrons/protocol_engine/state/test_state_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_state_store.py @@ -49,6 +49,12 @@ def placeholder_error_recovery_policy(*args: object, **kwargs: object) -> Any: "robotType": "OT-2 Standard", "models": ["OT-2 Standard", "OT-2 Refresh"], "extents": [446.75, 347.5, 0.0], + "paddingOffsets": { + "rear": -35.91, + "front": 31.89, + "leftSide": 0, + "rightSide": 0, + }, "mountOffsets": {"left": [-34.0, 0.0, 0.0], "right": [0.0, 0.0, 0.0]}, }, deck_fixed_labware=[], diff --git a/app-shell/build/release-notes.md b/app-shell/build/release-notes.md index ffdb26485e8..5ab9009bebc 100644 --- a/app-shell/build/release-notes.md +++ b/app-shell/build/release-notes.md @@ -28,6 +28,7 @@ Welcome to the v8.0.0 release of the Opentrons App! ### Known Issues - Labware offsets can't be applied to protocols that require selecting a CSV file as a runtime parameter value. Write the protocol in such a way that it passes analysis with or without the CSV file, or run Labware Position Check after confirming parameter values. +- Error recovery can't perform partial tip pickup, because it doesn't account for the pipette nozzle configuration of 8- and 96-channel pipettes. If a recovery step requires partial tip pickup, cancel the protocol instead. --- diff --git a/app/src/assets/localization/en/protocol_setup.json b/app/src/assets/localization/en/protocol_setup.json index ee00797352e..91091dd3a0f 100644 --- a/app/src/assets/localization/en/protocol_setup.json +++ b/app/src/assets/localization/en/protocol_setup.json @@ -1,26 +1,29 @@ { "96_mount": "left + right mount", "action_needed": "Action needed", - "adapter_slot_location_module": "Slot {{slotName}}, {{adapterName}} on {{moduleName}}", "adapter_slot_location": "Slot {{slotName}}, {{adapterName}}", + "adapter_slot_location_module": "Slot {{slotName}}, {{adapterName}} on {{moduleName}}", "add_fixture": "Add {{fixtureName}} to {{locationName}}", "add_this_deck_hardware": "Add this hardware to your deck configuration. It will be referenced during protocol analysis.", "add_to_slot": "Add to slot {{slotName}}", "additional_labware": "{{count}} additional labware", "additional_off_deck_labware": "Additional Off-Deck Labware", + "all_files_associated": "All files associated with the protocol run are available on the robot detail screen.", + "applied_labware_offset_data": "Applied labware offset data", "applied_labware_offsets": "applied labware offsets", "are_you_sure_you_want_to_proceed": "Are you sure you want to proceed to run?", - "attach_gripper_failure_reason": "Attach the required gripper to continue", + "attach": "attach", "attach_gripper": "attach gripper", + "attach_gripper_failure_reason": "Attach the required gripper to continue", "attach_module": "Attach module before calibrating", "attach_pipette_before_module_calibration": "Attach a pipette before running module calibration", "attach_pipette_calibration": "Attach pipette to see calibration information", "attach_pipette_cta": "Attach Pipette", "attach_pipette_failure_reason": "Attach the required pipette(s) to continue", "attach_pipette_tip_length_calibration": "Attach pipette to see tip length calibration information", - "attach": "attach", "back_to_top": "Back to top", "cal_all_pip": "Calibrate pipettes first", + "calibrate": "calibrate", "calibrate_deck_failure_reason": "Calibrate the deck to continue", "calibrate_deck_to_proceed_to_pipette_calibration": "Calibrate your deck in order to proceed to pipette calibration", "calibrate_deck_to_proceed_to_tip_length_calibration": "Calibrate your deck in order to proceed to tip length calibration", @@ -30,16 +33,15 @@ "calibrate_pipette_before_module_calibration": "Calibrate pipette before running module calibration", "calibrate_pipette_failure_reason": "Calibrate the required pipette(s) to continue", "calibrate_tiprack_failure_reason": "Calibrate the required tip lengths to continue", - "calibrate": "calibrate", "calibrated": "calibrated", + "calibration": "Calibration", "calibration_data_not_available": "Calibration data not available once run has started", "calibration_needed": "Calibration needed", "calibration_ready": "Calibration ready", + "calibration_required": "Calibration required", "calibration_required_attach_pipette_first": "Calibration required Attach pipette first", "calibration_required_calibrate_pipette_first": "Calibration required Calibrate pipette first", - "calibration_required": "Calibration required", "calibration_status": "calibration status", - "calibration": "Calibration", "cancel_and_restart_to_edit": "Cancel the run and restart setup to edit", "choose_csv_file": "Choose CSV file", "choose_enum": "Choose {{displayName}}", @@ -49,9 +51,9 @@ "configured": "configured", "confirm_heater_shaker_module_modal_description": "Before the run begins, module should have both anchors fully extended for a firm attachment. The thermal adapter should be attached to the module. ", "confirm_heater_shaker_module_modal_title": "Confirm Heater-Shaker Module is attached", - "confirm_offsets": "Confirm offsets", "confirm_liquids": "Confirm liquids", "confirm_locations_and_volumes": "Confirm locations and volumes", + "confirm_offsets": "Confirm offsets", "confirm_placements": "Confirm placements", "confirm_selection": "Confirm selection", "confirm_values": "Confirm values", @@ -66,79 +68,80 @@ "currently_configured": "Currently configured", "currently_unavailable": "Currently unavailable", "custom_values": "Custom values", + "deck_cal_description": "This measures the deck X and Y values relative to the gantry. Deck Calibration is the foundation for Tip Length Calibration and Pipette Offset Calibration.", "deck_cal_description_bullet_1": "Perform Deck Calibration during new robot setup.", "deck_cal_description_bullet_2": "Redo Deck Calibration if you relocate your robot.", - "deck_cal_description": "This measures the deck X and Y values relative to the gantry. Deck Calibration is the foundation for Tip Length Calibration and Pipette Offset Calibration.", "deck_calibration_title": "Deck Calibration", - "deck_conflict_info_thermocycler": "Update the deck configuration by removing the fixtures in locations A1 and B1. Either remove the fixtures from the deck configuration or update the protocol.", - "deck_conflict_info": "Update the deck configuration by removing the {{currentFixture}} in location {{cutout}}. Either remove the fixture from the deck configuration or update the protocol.", "deck_conflict": "Deck location conflict", + "deck_conflict_info": "Update the deck configuration by removing the {{currentFixture}} in location {{cutout}}. Either remove the fixture from the deck configuration or update the protocol.", + "deck_conflict_info_thermocycler": "Update the deck configuration by removing the fixtures in locations A1 and B1. Either remove the fixtures from the deck configuration or update the protocol.", "deck_hardware": "Deck hardware", "deck_hardware_ready": "Deck hardware ready", "deck_map": "Deck Map", "default_values": "Default values", + "download_files": "Download files", "example": "Example", "exit_to_deck_configuration": "Exit to deck configuration", "extension_mount": "extension mount", "extra_attention_warning_title": "Secure labware and modules before proceeding to run", "extra_module_attached": "Extra module attached", "feedback_form_link": "Let us know!", - "fixture_name": "fixture", "fixture": "Fixture", - "fixtures_connected_plural": "{{count}} fixtures attached", + "fixture_name": "fixture", "fixtures_connected": "{{count}} fixture attached", + "fixtures_connected_plural": "{{count}} fixtures attached", "get_labware_offset_data": "Get Labware Offset Data", "hardware_missing": "Missing hardware", "heater_shaker_extra_attention": "Use latch controls for easy placement of labware.", "heater_shaker_labware_list_view": "To add labware, use the toggle to control the latch", "how_offset_data_works": "How labware offsets work", "individiual_well_volume": "Individual well volume", - "initial_liquids_num_plural": "{{count}} initial liquids", "initial_liquids_num": "{{count}} initial liquid", + "initial_liquids_num_plural": "{{count}} initial liquids", "initial_location": "Initial Location", + "install_modules": "Install the required module.", "install_modules_and_fixtures": "Install and calibrate the required modules. Install the required fixtures.", "install_modules_plural": "Install the required modules.", - "install_modules": "Install the required module.", - "instrument_calibrations_missing_plural": "Missing {{count}} calibrations", "instrument_calibrations_missing": "Missing {{count}} calibration", - "instruments_connected_plural": "{{count}} instruments attached", - "instruments_connected": "{{count}} instrument attached", + "instrument_calibrations_missing_plural": "Missing {{count}} calibrations", "instruments": "Instruments", - "labware_latch_instructions": "Use latch control for easy placement of labware.", + "instruments_connected": "{{count}} instrument attached", + "instruments_connected_plural": "{{count}} instruments attached", + "labware": "Labware", "labware_latch": "Labware Latch", + "labware_latch_instructions": "Use latch control for easy placement of labware.", "labware_location": "Labware Location", "labware_name": "Labware name", "labware_placement": "labware placement", + "labware_position_check": "Labware Position Check", + "labware_position_check_not_available": "Labware Position Check is not available after run has started", "labware_position_check_not_available_analyzing_on_robot": "Labware Position Check is not available while protocol is analyzing on robot", "labware_position_check_not_available_empty_protocol": "Labware Position Check requires that the protocol loads labware and pipettes", - "labware_position_check_not_available": "Labware Position Check is not available after run has started", "labware_position_check_step_description": "Recommended workflow that helps you verify the position of each labware on the deck.", "labware_position_check_step_title": "Labware Position Check", "labware_position_check_text": "Labware Position Check is a recommended workflow that helps you verify the position of each labware on the deck. During this check, you can create Labware Offsets that adjust how the robot moves to each labware in the X, Y and Z directions.", - "labware_position_check": "Labware Position Check", "labware_setup_step_description": "Gather the following labware and full tip racks. To run your protocol without Labware Position Check, place and secure labware in their initial locations.", "labware_setup_step_title": "Labware", - "labware": "Labware", "last_calibrated": "Last calibrated: {{date}}", "learn_how_it_works": "Learn how it works", + "learn_more": "Learn more", "learn_more_about_offset_data": "Learn more about Labware Offset Data", "learn_more_about_robot_cal_link": "Learn more about robot calibration", - "learn_more": "Learn more", "liquid_information": "Liquid information", "liquid_name": "Liquid name", - "liquids": "liquids", "liquid_setup_step_description": "View liquid starting locations and volumes", "liquid_setup_step_title": "Liquids", + "liquids": "liquids", + "liquids_confirmed": "Liquids confirmed", "liquids_not_in_setup": "No liquids used in this protocol", "liquids_not_in_the_protocol": "no liquids are specified for this protocol.", "liquids_ready": "Liquids ready", - "liquids_confirmed": "Liquids confirmed", "list_view": "List View", "loading_data": "Loading data...", "loading_labware_offsets": "Loading labware offsets", "loading_protocol_details": "Loading details...", - "location_conflict": "Location conflict", "location": "Location", + "location_conflict": "Location conflict", "lpc_and_offset_data_title": "Labware Position Check and Labware Offset Data", "lpc_disabled_calibration_not_complete": "Make sure robot calibration is complete before running Labware Position Check", "lpc_disabled_modules_and_calibration_not_complete": "Make sure robot calibration is complete and all modules are connected before running Labware Position Check", @@ -146,36 +149,36 @@ "lpc_disabled_no_tipracks_loaded": "Labware Position Check requires that the protocol loads a tip rack", "lpc_disabled_no_tipracks_used": "Labware Position Check requires that the protocol has at least one pipette that picks up a tip", "map_view": "Map View", + "missing": "Missing", "missing_gripper": "Missing gripper", "missing_instruments": "Missing {{count}}", - "missing_pipettes_plural": "Missing {{count}} pipettes", "missing_pipettes": "Missing {{count}} pipette", - "missing": "Missing", + "missing_pipettes_plural": "Missing {{count}} pipettes", "modal_instructions_title": "{{moduleName}} Setup Instructions", + "module": "Module", "module_connected": "Connected", "module_disconnected": "Disconnected", "module_instructions_link": "{{moduleName}} setup instructions", "module_mismatch_body": "Check that the modules connected to this robot are of the right type and generation", "module_name": "Module", "module_not_connected": "Not connected", - "module_setup_step_title": "Deck hardware", "module_setup_step_ready": "Calibration ready", + "module_setup_step_title": "Deck hardware", "module_slot_location": "Slot {{slotName}}, {{moduleName}}", - "module": "Module", - "modules_connected_plural": "{{count}} modules attached", + "modules": "Modules", "modules_connected": "{{count}} module attached", + "modules_connected_plural": "{{count}} modules attached", "modules_setup_step_title": "Module Setup", - "modules": "Modules", - "mount_title": "{{mount}} MOUNT:", "mount": "{{mount}} mount", + "mount_title": "{{mount}} MOUNT:", "multiple_fixtures_missing": "{{count}} fixtures missing", + "multiple_modules": "Multiple modules of the same type", "multiple_modules_example": "Your protocol has two Temperature Modules. The Temperature Module attached to the first port starting from the left will be related to the first Temperature Module in your protocol while the second Temperature Module loaded would be related to the Temperature Module connected to the next port to the right. If using a hub, follow the same logic with the port ordering.", "multiple_modules_explanation": "To use more than one of the same module in a protocol, you first need to plug in the module that’s called first in your protocol to the lowest numbered USB port on the robot. Continue in the same manner with additional modules.", "multiple_modules_help_link_title": "See How To Set Up Modules of the Same Type", "multiple_modules_learn_more": "Learn more about using multiple modules of the same type", "multiple_modules_missing_plural": "Missing {{count}} modules", "multiple_modules_modal": "Setting up multiple modules of the same type", - "multiple_modules": "Multiple modules of the same type", "multiple_of_most_modules": "You can use multiples of most module types within a single Python protocol by connecting and loading the modules in a specific order. The robot will initialize the matching module attached to the lowest numbered port first, regardless of what deck slot it occupies.", "must_have_labware_and_pip": "Protocol must load labware and a pipette", "n_a": "N/A", @@ -188,8 +191,8 @@ "no_modules_or_fixtures": "No modules or fixtures are specified for this protocol.", "no_modules_specified": "no modules are specified for this protocol.", "no_modules_used_in_this_protocol": "No hardware used in this protocol", - "no_parameters_specified_in_protocol": "No parameters specified in this protocol", "no_parameters_specified": "No parameters specified", + "no_parameters_specified_in_protocol": "No parameters specified in this protocol", "no_tiprack_loaded": "Protocol must load a tip rack", "no_tiprack_used": "Protocol must pick up a tip", "no_usb_connection_required": "No USB connection required", @@ -197,32 +200,32 @@ "no_usb_required": "No USB required", "not_calibrated": "Not calibrated yet", "not_configured": "not configured", - "off_deck": "Off deck", "off": "Off", + "off_deck": "Off deck", "offset_data": "Offset Data", - "offsets_applied_plural": "{{count}} offsets applied", "offsets_applied": "{{count}} offset applied", + "offsets_applied_plural": "{{count}} offsets applied", "offsets_ready": "Offsets ready", - "on_adapter_in_mod": "on {{adapterName}} in {{moduleName}}", + "on": "On", + "on-deck_labware": "{{count}} on-deck labware", "on_adapter": "on {{adapterName}}", + "on_adapter_in_mod": "on {{adapterName}} in {{moduleName}}", "on_deck": "On deck", - "on-deck_labware": "{{count}} on-deck labware", - "on": "On", "opening": "Opening...", "parameters": "Parameters", "pipette_mismatch": "Pipette generation mismatch.", "pipette_missing": "Pipette missing", + "pipette_offset_cal": "Pipette Offset Calibration", + "pipette_offset_cal_description": "This measures a pipette’s X, Y and Z values in relation to the pipette mount and the deck. Pipette Offset Calibration relies on Deck Calibration and Tip Length Calibration. ", "pipette_offset_cal_description_bullet_1": "Perform Pipette Offset calibration the first time you attach a pipette to a new mount.", "pipette_offset_cal_description_bullet_2": "Redo Pipette Offset Calibration after performing Deck Calibration.", "pipette_offset_cal_description_bullet_3": "Redo Pipette Offset Calibration after performing Tip Length Calibration for the tip you used to calibrate the pipette.", - "pipette_offset_cal_description": "This measures a pipette’s X, Y and Z values in relation to the pipette mount and the deck. Pipette Offset Calibration relies on Deck Calibration and Tip Length Calibration. ", - "pipette_offset_cal": "Pipette Offset Calibration", "placement": "Placement", - "placements_ready": "Placements ready", "placements_confirmed": "Placements confirmed", + "placements_ready": "Placements ready", "plug_in_module_to_configure": "Plug in a {{module}} to add it to the slot", - "plug_in_required_module_plural": "Plug in and power up the required modules to continue", "plug_in_required_module": "Plug in and power up the required module to continue", + "plug_in_required_module_plural": "Plug in and power up the required modules to continue", "prepare_to_run": "Prepare to run", "proceed_to_labware_position_check": "Proceed to labware position check", "proceed_to_labware_setup_step": "Proceed to labware", @@ -244,34 +247,34 @@ "recalibrating_not_available": "Recalibrating Tip Length calibrations and Labware Position Check is not available.", "recalibrating_tip_length_not_available": "Recalibrating a tip length is not available once a run has started", "recommended": "Recommended", + "required": "Required", "required_instrument_calibrations": "required instrument calibrations", "required_tip_racks_title": "Required Tip Length Calibrations", - "required": "Required", - "reset_parameter_values_body": "This will discard any changes you have made. All parameters will have their default values.", "reset_parameter_values": "Reset parameter values?", + "reset_parameter_values_body": "This will discard any changes you have made. All parameters will have their default values.", "reset_setup": "Restart setup to edit", "reset_values": "Reset values", "resolve": "Resolve", - "restart_setup_and_try": "Restart setup and try using different parameter values.", "restart_setup": "Restart setup", + "restart_setup_and_try": "Restart setup and try using different parameter values.", "restore_default": "Restore default value", "restore_defaults": "Restore default values", "robot_cal_description": "Robot calibration establishes how the robot knows where it is in relation to the deck. Accurate Robot calibration is essential to run protocols successfully. Robot calibration has 3 parts: Deck calibration, Tip Length calibration and Pipette Offset calibration.", "robot_cal_help_title": "How Robot Calibration Works", - "robot_calibration_step_description_pipettes_only": "Review required instruments and calibrations for this protocol.", "robot_calibration_step_description": "Review required pipettes and tip length calibrations for this protocol.", - "robot_calibration_step_title": "Instruments", + "robot_calibration_step_description_pipettes_only": "Review required instruments and calibrations for this protocol.", "robot_calibration_step_ready": "Calibration ready", + "robot_calibration_step_title": "Instruments", + "run": "Run", "run_disabled_calibration_not_complete": "Make sure robot calibration is complete before proceeding to run", "run_disabled_modules_and_calibration_not_complete": "Make sure robot calibration is complete and all modules are connected before proceeding to run", "run_disabled_modules_not_connected": "Make sure all modules are connected before proceeding to run", "run_labware_position_check": "run labware position check", "run_labware_position_check_to_get_offsets": "Run Labware Position Check to get your labware offset data.", "run_never_started": "Run was never started", - "run": "Run", + "secure": "Secure", "secure_labware_instructions": "Secure labware instructions", "secure_labware_modal": "Securing labware to the {{name}}", - "secure": "Secure", "setup_for_run": "Setup for Run", "setup_instructions": "setup instructions", "setup_is_view_only": "Setup is view-only once run has started", @@ -283,22 +286,22 @@ "step": "STEP {{index}}", "there_are_no_unconfigured_modules": "No {{module}} is connected. Attach one and place it in {{slot}}.", "there_are_other_configured_modules": "A {{module}} is already configured in a different slot. Exit run setup and update your deck configuration to move to an already connected module. Or attach another {{module}} to continue setup.", - "tip_length_cal_description_bullet": "Perform Tip Length Calibration for each new tip type used on a pipette.", "tip_length_cal_description": "This measures the Z distance between the bottom of the tip and the pipette’s nozzle. If you redo the tip length calibration for the tip you used to calibrate a pipette, you will also have to redo that Pipette Offset Calibration.", + "tip_length_cal_description_bullet": "Perform Tip Length Calibration for each new tip type used on a pipette.", "tip_length_cal_title": "Tip Length Calibration", "tip_length_calibration": "tip length calibration", "total_liquid_volume": "Total volume", - "update_deck_config": "Update deck configuration", "update_deck": "Update deck", + "update_deck_config": "Update deck configuration", "update_offsets": "Update offsets", "updated": "Updated", "usb_connected_no_port_info": "USB Port Connected", "usb_drive_notification": "Leave USB drive attached until run starts", "usb_port_connected": "USB Port {{port}}", "usb_port_number": "USB-{{port}}", - "value_out_of_range_generic": "Value must be in range", - "value_out_of_range": "Value must be between {{min}}-{{max}}", "value": "Value", + "value_out_of_range": "Value must be between {{min}}-{{max}}", + "value_out_of_range_generic": "Value must be in range", "values_are_view_only": "Values are view-only", "variable_well_amount": "Variable well amount", "view_current_offsets": "View current offsets", diff --git a/app/src/assets/localization/en/quick_transfer.json b/app/src/assets/localization/en/quick_transfer.json index 99e7bb0d6ad..d80586cba53 100644 --- a/app/src/assets/localization/en/quick_transfer.json +++ b/app/src/assets/localization/en/quick_transfer.json @@ -121,7 +121,7 @@ "set_transfer_volume": "Set transfer volume", "source": "Source", "source_labware": "Source labware", - "source_labware_d2": "Source labware in D2", + "source_labware_c2": "Source labware in C2", "starting_well": "starting well", "storage_limit_reached": "Storage limit reached", "tip_drop_location": "Tip drop location", diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx index 8c41120d536..7bb9a73e105 100644 --- a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx @@ -13,6 +13,7 @@ interface NumericalKeyboardProps { isDecimal?: boolean hasHyphen?: boolean debug?: boolean + initialValue?: string } // the default keyboard layout intKeyboard that doesn't have decimal point and hyphen. @@ -22,6 +23,7 @@ export function NumericalKeyboard({ isDecimal = false, hasHyphen = false, debug = false, + initialValue = '', }: NumericalKeyboardProps): JSX.Element { const layoutName = `${isDecimal ? 'float' : 'int'}${ hasHyphen ? 'NegKeyboard' : 'Keyboard' @@ -35,6 +37,9 @@ export function NumericalKeyboard({ (keyboardRef.current = r)} theme={'hg-theme-default oddTheme1 numerical-keyboard'} + onInit={keyboard => { + keyboard.setInput(initialValue) + }} onChange={onChange} display={numericalCustom} useButtonTag={true} diff --git a/app/src/molecules/UploadInput/index.tsx b/app/src/molecules/UploadInput/index.tsx index 62a6cccfe0e..9802f6d28b5 100644 --- a/app/src/molecules/UploadInput/index.tsx +++ b/app/src/molecules/UploadInput/index.tsx @@ -29,8 +29,7 @@ const StyledLabel = styled.label` text-align: ${TYPOGRAPHY.textAlignCenter}; background-color: ${COLORS.white}; - &:hover, - &:focus-within { + &:hover { border: 2px dashed ${COLORS.blue50}; } ` diff --git a/app/src/organisms/CalibrationTaskList/index.tsx b/app/src/organisms/CalibrationTaskList/index.tsx index 77f0590c304..bcb95a9a12b 100644 --- a/app/src/organisms/CalibrationTaskList/index.tsx +++ b/app/src/organisms/CalibrationTaskList/index.tsx @@ -120,6 +120,7 @@ export function CalibrationTaskList({ width: 50rem; height: 47.5rem; `} + marginLeft="0" > {showCompletionScreen ? ( ) : ( - + { setCurrentPage(1) }} - width="51%" + width="50%" > {t('shared:change_protocol')} ) : ( - + { setCurrentPage(1) diff --git a/app/src/organisms/Devices/HistoricalProtocolRunOverflowMenu.tsx b/app/src/organisms/Devices/HistoricalProtocolRunOverflowMenu.tsx index 2e1a661ccb5..185172723ef 100644 --- a/app/src/organisms/Devices/HistoricalProtocolRunOverflowMenu.tsx +++ b/app/src/organisms/Devices/HistoricalProtocolRunOverflowMenu.tsx @@ -6,6 +6,7 @@ import { NavLink, useNavigate } from 'react-router-dom' import { ALIGN_CENTER, ALIGN_FLEX_END, + FLEX_MAX_CONTENT, Box, COLORS, DIRECTION_COLUMN, @@ -165,7 +166,6 @@ function MenuDropdown(props: MenuDropdownProps): JSX.Element { return ( diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx index 7a59b2a6cd1..2f324cbfb09 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx @@ -255,7 +255,7 @@ export function ProtocolRunHeader({ robotType, onSkipAndHome: () => { closeCurrentRun({ - onSuccess: () => { + onSettled: () => { if (isQuickTransfer) { deleteRun(runId) navigate(`/devices/${robotName}`) @@ -309,11 +309,11 @@ export function ProtocolRunHeader({ // TODO(jh, 08-15-24): The enteredER condition is a hack, because errorCommands are only returned when a run is current. // Ideally the run should not need to be current to view errorCommands. - // Close the run if no tips are attached after running tip check at least once. + // Close the run if no tips are attached after running tip check at least once. Post-run tip checks only occur on the Flex. // This marks the robot as "not busy" as soon as a run is cancelled if drop tip CTAs are unnecessary. - if (initialPipettesWithTipsCount === 0 && !enteredER) { + if ((initialPipettesWithTipsCount === 0 || !isFlex) && !enteredER) { closeCurrentRun({ - onSuccess: () => { + onSettled: () => { if (isQuickTransfer) { deleteRun(runId) navigate(`/devices/${robotName}`) @@ -568,7 +568,7 @@ export function ProtocolRunHeader({ void setTipStatusResolved(() => { toggleDTWiz() closeCurrentRun({ - onSuccess: () => { + onSettled: () => { if (isQuickTransfer) { deleteRun(runId) navigate(`/devices/${robotName}`) @@ -698,7 +698,6 @@ function ActionButton(props: ActionButtonProps): JSX.Element { reset, isPlayRunActionLoading, isPauseRunActionLoading, - isResetRunLoading, } = useRunControls(runId, (createRunResponse: Run): void => // redirect to new run after successful reset { @@ -707,7 +706,10 @@ function ActionButton(props: ActionButtonProps): JSX.Element { ) } ) - isResetRunLoadingRef.current = isResetRunLoading + const isResetRunLoading = isResetRunLoadingRef.current + if (runStatus === RUN_STATUS_IDLE || runStatus === RUN_STATUS_RUNNING) { + isResetRunLoadingRef.current = false + } const { missingModuleIds } = useUnmatchedModulesForProtocol(robotName, runId) const { complete: isCalibrationComplete } = useRunCalibrationStatus( robotName, @@ -790,8 +792,6 @@ function ActionButton(props: ActionButtonProps): JSX.Element { .some(module => module?.data != null && module.data.speedStatus !== 'idle') const isValidRunAgain = runStatus != null && RUN_AGAIN_STATUSES.includes(runStatus) - const validRunAgainButRequiresSetup = isValidRunAgain && !isSetupComplete - const runAgainWithSpinner = validRunAgainButRequiresSetup && isResetRunLoading let buttonText: string = '' let handleButtonClick = (): void => {} @@ -804,7 +804,7 @@ function ActionButton(props: ActionButtonProps): JSX.Element { !isValidRunAgain ) { disableReason = t('setup_incomplete') - } else if (isOtherRunCurrent) { + } else if (isOtherRunCurrent && !isResetRunLoading) { disableReason = t('shared:robot_is_busy') } else if (isRobotOnWrongVersionOfSoftware) { disableReason = t('shared:a_software_update_is_available') @@ -866,10 +866,11 @@ function ActionButton(props: ActionButtonProps): JSX.Element { }) } } - } else if (runStatus != null && RUN_AGAIN_STATUSES.includes(runStatus)) { - buttonIconName = runAgainWithSpinner ? 'ot-spinner' : 'play' + } else if (isValidRunAgain) { + buttonIconName = isResetRunLoading ? 'ot-spinner' : 'play' buttonText = t('run_again') handleButtonClick = () => { + isResetRunLoadingRef.current = true reset() trackEvent({ name: ANALYTICS_PROTOCOL_PROCEED_TO_RUN, @@ -889,7 +890,7 @@ function ActionButton(props: ActionButtonProps): JSX.Element { boxShadow="none" display={DISPLAY_FLEX} padding={`${SPACING.spacing12} ${SPACING.spacing16}`} - disabled={isRunControlButtonDisabled && !validRunAgainButRequiresSetup} + disabled={isRunControlButtonDisabled} onClick={handleButtonClick} id="ProtocolRunHeader_runControlButton" {...targetProps} @@ -902,7 +903,7 @@ function ActionButton(props: ActionButtonProps): JSX.Element { spin={ isProtocolAnalyzing || runStatus === RUN_STATUS_STOP_REQUESTED || - runAgainWithSpinner + isResetRunLoading } /> ) : null} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx index 51836841422..16e231b0dc8 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx @@ -108,23 +108,7 @@ export function ProtocolRunRuntimeParameters({ ) : null} {hasRunTimeParameters ? ( - - - - {t('values_are_view_only')} - - - {t('cancel_and_restart_to_edit')} - - - + ) : null} {!hasRunTimeParameters ? ( @@ -171,6 +155,31 @@ export function ProtocolRunRuntimeParameters({ ) } +interface RunTimeParametersBannerProps { + isRunTerminal: boolean +} + +function RunTimeParametersBanner({ + isRunTerminal, +}: RunTimeParametersBannerProps): JSX.Element { + const { t } = useTranslation('protocol_setup') + + return ( + + + + {isRunTerminal ? t('download_files') : t('values_are_view_only')} + + + {isRunTerminal + ? t('all_files_associated') + : t('cancel_and_restart_to_edit')} + + + + ) +} + interface StyledTableRowComponentProps { parameter: RunTimeParameter index: number diff --git a/app/src/organisms/Devices/ProtocolRun/SetupFlexPipetteCalibrationItem.tsx b/app/src/organisms/Devices/ProtocolRun/SetupFlexPipetteCalibrationItem.tsx index 49ed6243cf8..c7849e4f489 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupFlexPipetteCalibrationItem.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupFlexPipetteCalibrationItem.tsx @@ -16,6 +16,7 @@ import { import { useInstrumentsQuery } from '@opentrons/react-api-client' import { TertiaryButton } from '../../../atoms/buttons' import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { useStoredProtocolAnalysis } from '../hooks' import { PipetteWizardFlows } from '../../PipetteWizardFlows' import { FLOWS } from '../../PipetteWizardFlows/constants' import { SetupCalibrationItem } from './SetupCalibrationItem' @@ -40,11 +41,13 @@ export function SetupFlexPipetteCalibrationItem({ ) const { data: attachedInstruments } = useInstrumentsQuery() const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) - const loadPipetteCommand = mostRecentAnalysis?.commands.find( + const storedProtocolAnalysis = useStoredProtocolAnalysis(runId) + const completedAnalysis = mostRecentAnalysis ?? storedProtocolAnalysis + const loadPipetteCommand = completedAnalysis?.commands.find( (command): command is LoadPipetteRunTimeCommand => command.commandType === 'loadPipette' && command.params.mount === mount ) - const requestedPipette = mostRecentAnalysis?.pipettes?.find( + const requestedPipette = completedAnalysis?.pipettes?.find( pipette => pipette.id === loadPipetteCommand?.result?.pipetteId ) @@ -120,7 +123,7 @@ export function SetupFlexPipetteCalibrationItem({ ? NINETY_SIX_CHANNEL : SINGLE_MOUNT_PIPETTES } - pipetteInfo={mostRecentAnalysis?.pipettes} + pipetteInfo={completedAnalysis?.pipettes} onComplete={instrumentsRefetch} /> )} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx index c95adc49b36..d2c011ebd7a 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx @@ -88,6 +88,10 @@ export function SetupLabwareMap({ topLabwareDefinition != null && topLabwareId != null && hoverLabwareId === topLabwareId, + highlightShadowLabware: + topLabwareDefinition != null && + topLabwareId != null && + hoverLabwareId === topLabwareId, stacked: topLabwareDefinition != null && topLabwareId != null, moduleChildren: ( // open modal @@ -148,6 +152,7 @@ export function SetupLabwareMap({ topLabwareId, topLabwareDisplayName, highlight: isLabwareInStack && hoverLabwareId === topLabwareId, + highlightShadow: isLabwareInStack && hoverLabwareId === topLabwareId, labwareChildren: ( { }) it('should render title, and banner when RunTimeParameters are not empty and all values are default', () => { + when(useNotifyRunQuery) + .calledWith(RUN_ID) + .thenReturn({ + data: { + data: mockIdleUnstartedRun, + }, + } as any) render(props) screen.getByText('Parameters') screen.getByText('Default values') @@ -151,6 +161,13 @@ describe('ProtocolRunRuntimeParameters', () => { }, ], } as CompletedProtocolAnalysis) + when(useNotifyRunQuery) + .calledWith(RUN_ID) + .thenReturn({ + data: { + data: mockIdleUnstartedRun, + }, + } as any) render(props) screen.getByText('Parameters') screen.getByText('Custom values') @@ -160,6 +177,39 @@ describe('ProtocolRunRuntimeParameters', () => { screen.getByText('Value') }) + it('should render title, and banner when RunTimeParameters from view protocol run record overflow menu button', () => { + when(useNotifyRunQuery) + .calledWith(RUN_ID) + .thenReturn({ + data: { + data: { + ...mockSucceededRun, + runTimeParameters: mockRunTimeParameterData, + }, + }, + } as any) + vi.mocked(useMostRecentCompletedAnalysis).mockReturnValue({ + runTimeParameters: [ + ...mockRunTimeParameterData, + { + displayName: 'Dry Run', + variableName: 'DRYRUN', + description: 'Is this a dry or wet run? Wet is true, dry is false', + type: 'bool', + default: false, + value: true, + }, + ], + } as CompletedProtocolAnalysis) + + vi.mocked(useRunStatus).mockReturnValue('succeeded') + render(props) + screen.getByText('Download files') + screen.getByText( + 'All files associated with the protocol run are available on the robot detail screen.' + ) + }) + it('should render RunTimeParameters when RunTimeParameters are not empty', () => { render(props) screen.getByText('Dry Run') diff --git a/app/src/organisms/Devices/RecentProtocolRuns.tsx b/app/src/organisms/Devices/RecentProtocolRuns.tsx index 06815a7b064..dbb487df74f 100644 --- a/app/src/organisms/Devices/RecentProtocolRuns.tsx +++ b/app/src/organisms/Devices/RecentProtocolRuns.tsx @@ -120,14 +120,15 @@ export function RecentProtocolRuns({ const protocol = protocols?.data?.data.find( protocol => protocol.id === run.protocolId ) - + const isQuickTransfer = + protocol?.protocolKind === 'quick-transfer' const protocolName = protocol?.metadata.protocolName ?? protocol?.files[0].name ?? t('shared:loading') ?? '' - return ( + return !isQuickTransfer ? ( - ) + ) : null })} )} diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts index bee6eb0474c..ebe8a5f9bd9 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts @@ -174,6 +174,7 @@ interface UseTipSelectionUtilsResult { tipSelectorDef: LabwareDefinition2 selectTips: (tipGroup: WellGroup) => void deselectTips: (locations: string[]) => void + areTipsSelected: boolean } // TODO(jh, 06-18-24): Enforce failure/warning when accessing tipSelectionUtils @@ -215,11 +216,15 @@ function useTipSelectionUtils( [] ) + const areTipsSelected = + selectedLocs != null && Object.keys(selectedLocs).length > 0 + return { selectedTipLocations: selectedLocs, tipSelectorDef, selectTips, deselectTips, + areTipsSelected, } } diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx index d4012670c27..6bad4600c76 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx @@ -17,6 +17,7 @@ export function SelectTips(props: RecoveryContentProps): JSX.Element | null { routeUpdateActions, recoveryCommands, isOnDevice, + failedLabwareUtils, } = props const { ROBOT_PICKING_UP_TIPS } = RECOVERY_MAP const { pickUpTips } = recoveryCommands @@ -75,6 +76,7 @@ export function SelectTips(props: RecoveryContentProps): JSX.Element | null { + , getTopPortalEl() diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx index 15afe841639..bccc0567d8b 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx @@ -49,6 +49,10 @@ describe('SelectTips', () => { channels: 8, }, } as any, + failedLabwareUtils: { + selectedTipLocations: { A1: null }, + areTipsSelected: true, + } as any, } vi.mocked(TipSelectionModal).mockReturnValue( @@ -138,4 +142,22 @@ describe('SelectTips', () => { }) expect(tertiaryBtn[0]).toBeDisabled() }) + + it('disables the primary button if tips are not selected', () => { + props = { + ...props, + failedLabwareUtils: { + selectedTipLocations: null, + areTipsSelected: false, + } as any, + } + + render(props) + + const primaryBtn = screen.getAllByRole('button', { + name: 'Pick up tips', + }) + + expect(primaryBtn[0]).toBeDisabled() + }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TipSelectionModal.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TipSelectionModal.test.tsx index 608c870324c..78f9666b3e0 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TipSelectionModal.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TipSelectionModal.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { describe, it, vi, beforeEach } from 'vitest' +import { describe, it, vi, beforeEach, expect } from 'vitest' import { screen } from '@testing-library/react' import { mockRecoveryContentProps } from '../../__fixtures__' @@ -24,6 +24,10 @@ describe('TipSelectionModal', () => { ...mockRecoveryContentProps, allowTipSelection: true, toggleModal: vi.fn(), + failedLabwareUtils: { + selectedTipLocations: { A1: null }, + areTipsSelected: true, + } as any, } vi.mocked(TipSelection).mockReturnValue(
MOCK TIP SELECTION
) @@ -39,5 +43,17 @@ describe('TipSelectionModal', () => { render(props) screen.getByText('MOCK TIP SELECTION') + screen.getByLabelText('closeIcon') + }) + + it('prevents from users from exiting the modal if no well(s) are selected', () => { + props = { + ...props, + failedLabwareUtils: { areTipsSelected: false } as any, + } + + render(props) + + expect(screen.queryByLabelText('closeIcon')).not.toBeInTheDocument() }) }) diff --git a/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx b/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx index 4dc583d6c9d..0e48c4828cc 100644 --- a/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx @@ -237,9 +237,7 @@ describe('ModuleCard', () => { eatToast: mockEatToast, }) vi.mocked(getRequestById).mockReturnValue(null) - when(useCurrentRunStatus) - .calledWith(expect.any(Object)) - .thenReturn(RUN_STATUS_IDLE) + when(useCurrentRunStatus).calledWith().thenReturn(RUN_STATUS_IDLE) when(useIsFlex).calledWith(props.robotName).thenReturn(true) when(useIsEstopNotDisengaged).calledWith(props.robotName).thenReturn(false) }) @@ -311,9 +309,7 @@ describe('ModuleCard', () => { }) it('renders kebab icon and it is disabled when run is in progress', () => { - when(useCurrentRunStatus) - .calledWith(expect.any(Object)) - .thenReturn(RUN_STATUS_RUNNING) + when(useCurrentRunStatus).calledWith().thenReturn(RUN_STATUS_RUNNING) render({ ...props, module: mockMagneticModule, diff --git a/app/src/organisms/ModuleCard/index.tsx b/app/src/organisms/ModuleCard/index.tsx index 52b4b000374..e296328969f 100644 --- a/app/src/organisms/ModuleCard/index.tsx +++ b/app/src/organisms/ModuleCard/index.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import { Trans, useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' -import { useNavigate } from 'react-router-dom' import { ALIGN_START, @@ -126,14 +125,8 @@ export const ModuleCard = (props: ModuleCardProps): JSX.Element | null => { const [showCalModal, setShowCalModal] = React.useState(false) const [targetProps, tooltipProps] = useHoverTooltip() - const navigate = useNavigate() - const runStatus = useCurrentRunStatus({ - onSettled: data => { - if (data == null) { - navigate('/upload') - } - }, - }) + + const runStatus = useCurrentRunStatus() const isFlex = useIsFlex(robotName) const requireModuleCalibration = isFlex && diff --git a/app/src/organisms/Navigation/index.tsx b/app/src/organisms/Navigation/index.tsx index a9a55f53e63..e03a5f443d0 100644 --- a/app/src/organisms/Navigation/index.tsx +++ b/app/src/organisms/Navigation/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' -import { NavLink } from 'react-router-dom' +import { useLocation, NavLink } from 'react-router-dom' import styled from 'styled-components' import { @@ -39,15 +39,19 @@ const NAV_LINKS: Array = [ '/robot-settings', ] -// TODO(sb:7/10/24): update this wrapper to fade on both sides only when not scrolled completely to that side -// This will be accomplished in PLAT-399 const CarouselWrapper = styled.div` display: ${DISPLAY_FLEX}; flex-direction: ${DIRECTION_ROW}; align-items: ${ALIGN_FLEX_START}; - width: 42.25rem; + width: 56.75rem; overflow-x: ${OVERFLOW_SCROLL}; - -webkit-mask-image: linear-gradient(90deg, #000 90%, transparent); + -webkit-mask-image: linear-gradient( + to right, + transparent 0%, + black 0%, + black 96.5%, + transparent 100% + ); &::-webkit-scrollbar { display: none; } @@ -65,6 +69,7 @@ interface NavigationProps { export function Navigation(props: NavigationProps): JSX.Element { const { setNavMenuIsOpened, longPressModalIsOpened } = props const { t } = useTranslation('top_navigation') + const location = useLocation() const localRobot = useSelector(getLocalRobot) const [showNavMenu, setShowNavMenu] = React.useState(false) const robotName = localRobot?.name != null ? localRobot.name : 'no name' @@ -95,6 +100,15 @@ export function Navigation(props: NavigationProps): JSX.Element { if (scrollRef.current != null) { observer.observe(scrollRef.current) } + + const navBarScrollRef = React.useRef(null) + React.useEffect(() => { + navBarScrollRef?.current?.scrollIntoView({ + behavior: 'auto', + inline: 'center', + }) + }, []) + function getPathDisplayName(path: typeof NAV_LINKS[number]): string { switch (path) { case '/instruments': @@ -134,35 +148,40 @@ export function Navigation(props: NavigationProps): JSX.Element { aria-label="Navigation_container" > - - - {iconName != null ? ( - - ) : null} - - {NAV_LINKS.map(path => ( + + + {iconName != null ? ( + + ) : null} + {NAV_LINKS.map(path => ( + + + ))} diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx b/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx index e39e46ef62f..26cd6680653 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx +++ b/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx @@ -51,7 +51,8 @@ export function RecentRunProtocolCard({ const { data, isLoading } = useProtocolQuery(runData.protocolId ?? null) const protocolData = data?.data ?? null const isProtocolFetching = isLoading - return protocolData == null ? null : ( + return protocolData == null || + protocolData.protocolKind === 'quick-transfer' ? null : ( { - if (isQuickTransfer && !isActiveRun) { + onSettled: () => { + if (isQuickTransfer && protocolId != null) { deleteRun(runId) + navigate(`/quick-transfer/${protocolId}`) + } else if (isQuickTransfer) { + deleteRun(runId) + navigate('/quick-transfer') + } else if (protocolId != null) { + navigate(`/protocols/${protocolId}`) + } else { + navigate('/protocols') } }, }) @@ -87,17 +95,8 @@ export function ConfirmCancelRunModal({ React.useEffect(() => { if (runStatus === RUN_STATUS_STOPPED) { trackProtocolRunEvent({ name: ANALYTICS_PROTOCOL_RUN_ACTION.CANCEL }) - dismissCurrentRun(runId) if (!isActiveRun) { - if (isQuickTransfer && protocolId != null) { - navigate(`/quick-transfer/${protocolId}`) - } else if (isQuickTransfer) { - navigate('/quick-transfer') - } else if (protocolId != null) { - navigate(`/protocols/${protocolId}`) - } else { - navigate('/protocols') - } + dismissCurrentRun(runId) } } }, [runStatus]) diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/ConfirmCancelRunModal.test.tsx b/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/ConfirmCancelRunModal.test.tsx index 358436283aa..56651d3e44d 100644 --- a/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/ConfirmCancelRunModal.test.tsx +++ b/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/ConfirmCancelRunModal.test.tsx @@ -138,15 +138,7 @@ describe('ConfirmCancelRunModal', () => { expect(mockStopRun).toHaveBeenCalled() }) - it('when run is stopped, the run is dismissed and the modal closes', () => { - when(useRunStatus).calledWith(RUN_ID).thenReturn(RUN_STATUS_STOPPED) - render(props) - - expect(mockDismissCurrentRun).toHaveBeenCalled() - expect(mockTrackProtocolRunEvent).toHaveBeenCalled() - }) - - it('when run is stopped, the run is dismissed and the modal closes - in prepare to run', () => { + it('when run is stopped, the run is dismissed and the modal closes if the run is not yet active', () => { props = { ...props, isActiveRun: false, @@ -156,18 +148,13 @@ describe('ConfirmCancelRunModal', () => { expect(mockDismissCurrentRun).toHaveBeenCalled() expect(mockTrackProtocolRunEvent).toHaveBeenCalled() - expect(mockNavigate).toHaveBeenCalledWith('/protocols') }) - it('when quick transfer run is stopped, the run is dismissed and you return to quick transfer', () => { - props = { - ...props, - isActiveRun: false, - isQuickTransfer: true, - } + + it('when run is stopped, the run is not dismissed if the run is active', () => { when(useRunStatus).calledWith(RUN_ID).thenReturn(RUN_STATUS_STOPPED) render(props) - expect(mockDismissCurrentRun).toHaveBeenCalled() - expect(mockNavigate).toHaveBeenCalledWith('/quick-transfer') + expect(mockDismissCurrentRun).not.toHaveBeenCalled() + expect(mockTrackProtocolRunEvent).toHaveBeenCalled() }) }) diff --git a/app/src/organisms/ProtocolAnalysisFailure/index.tsx b/app/src/organisms/ProtocolAnalysisFailure/index.tsx index 378996edab3..72622a6746a 100644 --- a/app/src/organisms/ProtocolAnalysisFailure/index.tsx +++ b/app/src/organisms/ProtocolAnalysisFailure/index.tsx @@ -121,7 +121,7 @@ export function ProtocolAnalysisFailure( } const SCROLL_LONG = css` - overflow: scroll; + overflow: auto; width: inherit; max-height: 11.75rem; ` diff --git a/app/src/organisms/ProtocolDetails/index.tsx b/app/src/organisms/ProtocolDetails/index.tsx index d9579f68df6..c5d927ba73d 100644 --- a/app/src/organisms/ProtocolDetails/index.tsx +++ b/app/src/organisms/ProtocolDetails/index.tsx @@ -86,6 +86,12 @@ const GRID_STYLE = css` grid-template-columns: 26.6% 26.6% 26.6% 20.2%; ` +const TWO_COL_GRID_STYLE = css` + display: grid; + grid-gap: ${SPACING.spacing24}; + grid-template-columns: 22.5% 77.5%; +` + const ZOOM_ICON_STYLE = css` border-radius: ${BORDERS.borderRadius4}; &:hover { @@ -130,7 +136,9 @@ function MetadataDetails({ flexDirection={DIRECTION_COLUMN} data-testid="ProtocolDetails_description" > - {description} + + {description} + {filteredMetaData.map((item, index) => { return ( @@ -165,9 +173,11 @@ const ReadMoreContent = (props: ReadMoreContentProps): JSX.Element => { : metadata.description return ( - + {isReadMore ? ( - {description.slice(0, 160)} + + {description.slice(0, 160)} + ) : ( - + {analysisStatus === 'loading' diff --git a/app/src/organisms/ProtocolSetupLabware/LabwareMapView.tsx b/app/src/organisms/ProtocolSetupLabware/LabwareMapView.tsx index cdeabfa2a3e..39be3dc0f05 100644 --- a/app/src/organisms/ProtocolSetupLabware/LabwareMapView.tsx +++ b/app/src/organisms/ProtocolSetupLabware/LabwareMapView.tsx @@ -71,6 +71,8 @@ export function LabwareMapView(props: LabwareMapViewProps): JSX.Element { } : undefined, highlightLabware: true, + highlightShadowLabware: + topLabwareDefinition != null && topLabwareId != null, moduleChildren: null, stacked: topLabwareDefinition != null && topLabwareId != null, } @@ -99,6 +101,7 @@ export function LabwareMapView(props: LabwareMapViewProps): JSX.Element { }, labwareChildren: null, highlight: true, + highlightShadow: isLabwareInStack, stacked: isLabwareInStack, } } diff --git a/app/src/organisms/ProtocolSetupLabware/index.tsx b/app/src/organisms/ProtocolSetupLabware/index.tsx index 456358ddfb7..c745b3cf252 100644 --- a/app/src/organisms/ProtocolSetupLabware/index.tsx +++ b/app/src/organisms/ProtocolSetupLabware/index.tsx @@ -187,6 +187,7 @@ export function ProtocolSetupLabware({ setIsConfirmed(true) setSetupScreen('prepare to run') }} + buttonCategory="rounded" /> )} diff --git a/app/src/organisms/ProtocolSetupLiquids/index.tsx b/app/src/organisms/ProtocolSetupLiquids/index.tsx index 2f831950afe..5324ac91fa8 100644 --- a/app/src/organisms/ProtocolSetupLiquids/index.tsx +++ b/app/src/organisms/ProtocolSetupLiquids/index.tsx @@ -74,6 +74,7 @@ export function ProtocolSetupLiquids({ setIsConfirmed(true) setSetupScreen('prepare to run') }} + buttonCategory="rounded" /> )} diff --git a/app/src/organisms/ProtocolSetupOffsets/index.tsx b/app/src/organisms/ProtocolSetupOffsets/index.tsx index b0f8b6f78f6..b68fee294bc 100644 --- a/app/src/organisms/ProtocolSetupOffsets/index.tsx +++ b/app/src/organisms/ProtocolSetupOffsets/index.tsx @@ -1,10 +1,14 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { - Flex, + Chip, + DIRECTION_COLUMN, DIRECTION_ROW, + Flex, + InfoScreen, JUSTIFY_SPACE_BETWEEN, - Chip, + SPACING, + StyledText, } from '@opentrons/components' import type { LabwareOffset } from '@opentrons/api-client' @@ -92,19 +96,34 @@ export function ProtocolSetupOffsets({ /> ) : ( { setIsConfirmed(true) setSetupScreen('prepare to run') }} + buttonCategory="rounded" /> )} - + + {nonIdentityOffsets.length > 0 ? ( + <> + + {t('applied_labware_offset_data')} + + + + ) : ( + + )} + {enableAirGapDisplayItems.map(displayItem => ( - ))} @@ -207,6 +207,7 @@ export function AirGap(props: AirGapProps): JSX.Element { > { setVolume(Number(e)) }} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx index 9b58e356aae..10f43d68fab 100644 --- a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx @@ -6,7 +6,7 @@ import { SPACING, DIRECTION_COLUMN, POSITION_FIXED, - LargeButton, + RadioButton, COLORS, } from '@opentrons/components' import { @@ -102,14 +102,14 @@ export function BlowOut(props: BlowOutProps): JSX.Element { const enableBlowOutDisplayItems = [ { - value: true, + option: true, description: t('option_enabled'), onClick: () => { setisBlowOutEnabled(true) }, }, { - value: false, + option: false, description: t('option_disabled'), onClick: () => { setisBlowOutEnabled(false) @@ -175,15 +175,13 @@ export function BlowOut(props: BlowOutProps): JSX.Element { width="100%" > {enableBlowOutDisplayItems.map(displayItem => ( - { - setisBlowOutEnabled(displayItem.value) - }} - buttonText={displayItem.description} + isSelected={isBlowOutEnabled === displayItem.option} + onChange={displayItem.onClick} + buttonValue={displayItem.description} + buttonLabel={displayItem.description} + radioButtonType="large" /> ))} @@ -197,19 +195,17 @@ export function BlowOut(props: BlowOutProps): JSX.Element { width="100%" > {blowOutLocationItems.map(blowOutLocationItem => ( - { + isSelected={blowOutLocation === blowOutLocationItem.location} + onChange={() => { setBlowOutLocation( blowOutLocationItem.location as BlowOutLocation ) }} - buttonText={blowOutLocationItem.description} + buttonValue={blowOutLocationItem.description} + buttonLabel={blowOutLocationItem.description} + radioButtonType="large" /> ))}
diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx index 60f88eebfb1..c2a1046c807 100644 --- a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx @@ -8,7 +8,7 @@ import { DIRECTION_COLUMN, Flex, InputField, - LargeButton, + RadioButton, POSITION_FIXED, SPACING, } from '@opentrons/components' @@ -180,13 +180,13 @@ export function Delay(props: DelayProps): JSX.Element { width="100%" > {delayEnabledDisplayItems.map(displayItem => ( - ))}
@@ -264,6 +264,7 @@ export function Delay(props: DelayProps): JSX.Element { > { setPosition(Number(e)) }} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx index 766cc5faea1..de7af18592c 100644 --- a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx @@ -142,6 +142,7 @@ export function FlowRateEntry(props: FlowRateEntryProps): JSX.Element { > { setFlowRate(Number(e)) }} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Mix.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Mix.tsx index 75f3a14a6b3..93dd5f46cd4 100644 --- a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Mix.tsx +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Mix.tsx @@ -8,7 +8,7 @@ import { DIRECTION_COLUMN, Flex, InputField, - LargeButton, + RadioButton, POSITION_FIXED, SPACING, } from '@opentrons/components' @@ -160,13 +160,13 @@ export function Mix(props: MixProps): JSX.Element { width="100%" > {enableMixDisplayItems.map(displayItem => ( - ))}
@@ -204,6 +204,7 @@ export function Mix(props: MixProps): JSX.Element { > { setMixVolume(Number(e)) }} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx index ddea515f952..b9291f1d832 100644 --- a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx @@ -8,7 +8,7 @@ import { DIRECTION_COLUMN, Flex, InputField, - LargeButton, + RadioButton, POSITION_FIXED, SPACING, } from '@opentrons/components' @@ -156,15 +156,15 @@ export function PipettePath(props: PipettePathProps): JSX.Element { width="100%" > {allowedPipettePathOptions.map(option => ( - { + { setSelectedPath(option.pathOption) }} - buttonText={option.description} + buttonValue={option.description} + buttonLabel={option.description} + radioButtonType="large" /> ))}
@@ -202,6 +202,7 @@ export function PipettePath(props: PipettePathProps): JSX.Element { > { setDisposalVolume(Number(e)) }} @@ -218,15 +219,15 @@ export function PipettePath(props: PipettePathProps): JSX.Element { width="100%" > {blowOutLocationItems.map(option => ( - { + isSelected={blowOutLocation === option.location} + onChange={() => { setBlowOutLocation(option.location) }} - buttonText={option.description} + buttonValue={option.description} + buttonLabel={option.description} + radioButtonType="large" /> ))} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx index 18e7d100f37..6bd48f51cf3 100644 --- a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx @@ -133,6 +133,7 @@ export function TipPositionEntry(props: TipPositionEntryProps): JSX.Element { > { setTipPosition(Number(e)) }} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx index 45d48bde67c..9cf8949b897 100644 --- a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx @@ -8,7 +8,7 @@ import { DIRECTION_COLUMN, Flex, InputField, - LargeButton, + RadioButton, POSITION_FIXED, SPACING, } from '@opentrons/components' @@ -152,15 +152,13 @@ export function TouchTip(props: TouchTipProps): JSX.Element { width="100%" > {enableTouchTipDisplayItems.map(displayItem => ( - ))} @@ -198,6 +196,7 @@ export function TouchTip(props: TouchTipProps): JSX.Element { > { setPosition(Number(e)) }} diff --git a/app/src/organisms/QuickTransferFlow/SelectDestLabware.tsx b/app/src/organisms/QuickTransferFlow/SelectDestLabware.tsx index 1c3e656c6d4..591a6303346 100644 --- a/app/src/organisms/QuickTransferFlow/SelectDestLabware.tsx +++ b/app/src/organisms/QuickTransferFlow/SelectDestLabware.tsx @@ -117,8 +117,8 @@ export function SelectDestLabware( onChange={() => { setSelectedLabware('source') }} - buttonLabel={t('source_labware_d2')} - buttonValue="source-labware-d2" + buttonLabel={t('source_labware_c2')} + buttonValue="source-labware-c2" subButtonLabel={state.source.metadata.displayName} /> ) : null} diff --git a/app/src/organisms/QuickTransferFlow/SelectDestWells.tsx b/app/src/organisms/QuickTransferFlow/SelectDestWells.tsx index e526d28bcc2..63a70549121 100644 --- a/app/src/organisms/QuickTransferFlow/SelectDestWells.tsx +++ b/app/src/organisms/QuickTransferFlow/SelectDestWells.tsx @@ -107,9 +107,10 @@ export function SelectDestWells(props: SelectDestWellsProps): JSX.Element { const resetButtonProps: React.ComponentProps = { buttonType: 'tertiaryLowLight', buttonText: t('shared:reset'), - onClick: () => { + onClick: (e: React.MouseEvent) => { setIsNumberWellsSelectedError(false) setSelectedWells({}) + e.currentTarget.blur?.() }, } let labwareDefinition = diff --git a/app/src/organisms/QuickTransferFlow/SelectSourceWells.tsx b/app/src/organisms/QuickTransferFlow/SelectSourceWells.tsx index bd0aaceb65c..5f95482fe92 100644 --- a/app/src/organisms/QuickTransferFlow/SelectSourceWells.tsx +++ b/app/src/organisms/QuickTransferFlow/SelectSourceWells.tsx @@ -52,8 +52,9 @@ export function SelectSourceWells(props: SelectSourceWellsProps): JSX.Element { const resetButtonProps: React.ComponentProps = { buttonType: 'tertiaryLowLight', buttonText: t('shared:reset'), - onClick: () => { + onClick: (e: React.MouseEvent) => { setSelectedWells({}) + e.currentTarget.blur?.() }, } let displayLabwareDefinition = state.source diff --git a/app/src/organisms/QuickTransferFlow/__tests__/SelectDestLabware.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/SelectDestLabware.test.tsx index a2d2430c268..84e194834d0 100644 --- a/app/src/organisms/QuickTransferFlow/__tests__/SelectDestLabware.test.tsx +++ b/app/src/organisms/QuickTransferFlow/__tests__/SelectDestLabware.test.tsx @@ -96,7 +96,7 @@ describe('SelectDestLabware', () => { }, }) render(props) - screen.getByText('Source labware in D2') + screen.getByText('Source labware in C2') screen.getByText('source labware name') }) it('enables continue button if you select a labware', () => { @@ -109,7 +109,7 @@ describe('SelectDestLabware', () => { }) const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button') expect(continueBtn).toBeDisabled() - const sourceLabware = screen.getByText('Source labware in D2') + const sourceLabware = screen.getByText('Source labware in C2') fireEvent.click(sourceLabware) expect(continueBtn).toBeEnabled() }) diff --git a/app/src/organisms/QuickTransferFlow/utils/generateQuickTransferArgs.ts b/app/src/organisms/QuickTransferFlow/utils/generateQuickTransferArgs.ts index 4f571665b10..517e0aabb0d 100644 --- a/app/src/organisms/QuickTransferFlow/utils/generateQuickTransferArgs.ts +++ b/app/src/organisms/QuickTransferFlow/utils/generateQuickTransferArgs.ts @@ -299,18 +299,23 @@ export function generateQuickTransferArgs( const flowRatesForSupportedTip = quickTransferState.pipette.liquids.default.supportedTips[tipType] const pipetteEntity = Object.values(invariantContext.pipetteEntities)[0] - const labwareEntityValues = Object.values(invariantContext.labwareEntities) - const sourceLabwareEntity = labwareEntityValues.find( - entity => - entity.labwareDefURI === getLabwareDefURI(quickTransferState.source) + + const sourceLabwareId = Object.keys(robotState.labware).find( + labwareId => robotState.labware[labwareId].slot === 'C2' ) + const sourceLabwareEntity = + sourceLabwareId != null + ? invariantContext.labwareEntities[sourceLabwareId] + : undefined let destLabwareEntity = sourceLabwareEntity if (quickTransferState.destination !== 'source') { - destLabwareEntity = labwareEntityValues.find( - entity => - entity.labwareDefURI === - getLabwareDefURI(quickTransferState.destination as LabwareDefinition2) + const destinationLabwareId = Object.keys(robotState.labware).find( + labwareId => robotState.labware[labwareId].slot === 'D2' ) + destLabwareEntity = + destinationLabwareId != null + ? invariantContext.labwareEntities[destinationLabwareId] + : undefined } let nozzles = null diff --git a/app/src/organisms/RunPreview/index.tsx b/app/src/organisms/RunPreview/index.tsx index 0deff700e6e..a67719e48be 100644 --- a/app/src/organisms/RunPreview/index.tsx +++ b/app/src/organisms/RunPreview/index.tsx @@ -66,8 +66,6 @@ export const RunPreviewComponent = ( runId, { cursor: 0, pageLength: MAX_COMMANDS }, { - staleTime: Infinity, - cacheTime: Infinity, enabled: isRunTerminal, } ) diff --git a/app/src/pages/Devices/ProtocolRunDetails/index.tsx b/app/src/pages/Devices/ProtocolRunDetails/index.tsx index bcef73bcdca..f828617e35d 100644 --- a/app/src/pages/Devices/ProtocolRunDetails/index.tsx +++ b/app/src/pages/Devices/ProtocolRunDetails/index.tsx @@ -322,12 +322,13 @@ const SetupTab = (props: SetupTabProps): JSX.Element | null => { 'not_available_for_a_completed_run' )}` - // On the initial render only, navigate to "run preview" if the run has started. + // On the initial render or when a run first begins, navigate to "run preview" if the run has started. + // If "run again" is clicked, the user should NOT be directed back to the "setup" tab. React.useEffect(() => { - if (runHasStarted && protocolRunDetailsTab === 'setup') { + if (runHasStarted && protocolRunDetailsTab !== 'run-preview') { navigate(`/devices/${robotName}/protocol-runs/${runId}/run-preview`) } - }, []) + }, [runHasStarted]) return ( { + if (lastRun != null) { + lastRun = formatDistance(new Date(lastRun), new Date(), { + addSuffix: true, + }).replace('about ', '') + } + return lastRun === 'less than a minute ago' ? 'normal' : 'nowrap' + } + return ( - + {lastRun != null ? formatDistance(new Date(lastRun), new Date(), { addSuffix: true, diff --git a/app/src/pages/ProtocolDetails/index.tsx b/app/src/pages/ProtocolDetails/index.tsx index 73f9a5bd590..96389c322c2 100644 --- a/app/src/pages/ProtocolDetails/index.tsx +++ b/app/src/pages/ProtocolDetails/index.tsx @@ -521,7 +521,7 @@ export function ProtocolDetails(): JSX.Element | null { isScrolled={isScrolled} isProtocolFetching={isProtocolFetching} /> - + { @@ -403,7 +403,7 @@ export function QuickTransferDetails(): JSX.Element | null { isScrolled={isScrolled} isTransferFetching={isTransferFetching} /> - + 0 const disableErrorDetailsBtn = !( - hasCommandErrors || + (hasCommandErrors && !cancelledWithoutRecovery) || (runRecord?.data.errors != null && runRecord?.data.errors.length > 0) ) @@ -248,15 +250,10 @@ export function RunSummary(): JSX.Element { }, [isRunCurrent, enteredER]) const returnToQuickTransfer = (): void => { - if (!isRunCurrent) { + closeCurrentRunIfValid(() => { deleteRun(runId) navigate('/quick-transfer') - } else { - closeCurrentRunIfValid(() => { - deleteRun(runId) - navigate('/quick-transfer') - }) - } + }) } // TODO(jh, 05-30-24): EXEC-487. Refactor reset() so we can redirect to the setup page, showing the shimmer skeleton instead. diff --git a/app/src/redux/robot-api/http.ts b/app/src/redux/robot-api/http.ts index 7dd41b87da6..63f961acf85 100644 --- a/app/src/redux/robot-api/http.ts +++ b/app/src/redux/robot-api/http.ts @@ -5,6 +5,7 @@ import mapValues from 'lodash/mapValues' import toString from 'lodash/toString' import omitBy from 'lodash/omitBy' import inRange from 'lodash/inRange' +import type { AxiosError } from 'axios' import { OPENTRONS_USB } from '../../redux/discovery' import { appShellRequestor } from '../../redux/shell/remote' @@ -62,15 +63,27 @@ export function fetchRobotApi( url, data: options.body, }) + .then(response => ({ + isError: false as const, + response, + })) + .catch(err => ({ + isError: true as const, + ...(err as AxiosError), + })) ).pipe( - map(response => ({ + map(result => ({ host, path, method, - body: response?.data, - status: response?.status, + body: result?.response?.data, + // FIXME(sf) this doesn't seem right, but also the type interface isn't written to allow for request + // failures that don't come from valid connections + status: result?.response?.status ?? 444, // appShellRequestor eventually calls axios.request, which doesn't provide an ok boolean in the response - ok: inRange(response?.status, 200, 300), + ok: result.isError + ? false + : inRange(result?.response?.status, 200, 300), })) ) : from(fetch(url, options)).pipe( diff --git a/components/src/atoms/buttons/SecondaryButton.tsx b/components/src/atoms/buttons/SecondaryButton.tsx index 4746e1f6932..a7620495d5c 100644 --- a/components/src/atoms/buttons/SecondaryButton.tsx +++ b/components/src/atoms/buttons/SecondaryButton.tsx @@ -55,6 +55,7 @@ export const SecondaryButton = styled.button.withConfig({ box-shadow: none; border-color: ${COLORS.grey30}; color: ${COLORS.grey40}; + cursor: default; } ${styleProps} diff --git a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx index 5dc076b1781..9158cfe360b 100644 --- a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx +++ b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx @@ -57,6 +57,7 @@ export interface LabwareOnDeck { labwareChildren?: React.ReactNode onLabwareClick?: () => void highlight?: boolean + highlightShadow?: boolean stacked?: boolean } @@ -70,6 +71,7 @@ export interface ModuleOnDeck { moduleChildren?: React.ReactNode onLabwareClick?: () => void highlightLabware?: boolean + highlightShadowLabware?: boolean stacked?: boolean } interface BaseDeckProps { @@ -90,6 +92,8 @@ interface BaseDeckProps { svgProps?: React.ComponentProps } +const LABWARE_OFFSET_DISPLAY_THRESHOLD = 2 + export function BaseDeck(props: BaseDeckProps): JSX.Element { const { robotType, @@ -237,6 +241,7 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { )} <> + {/* render modules, nested labware, and overlays */} {modulesOnDeck.map( ({ moduleModel, @@ -247,7 +252,7 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { moduleChildren, onLabwareClick, highlightLabware, - stacked = false, + highlightShadowLabware, }) => { const slotPosition = getPositionFromSlotId( moduleLocation.slotName, @@ -275,14 +280,15 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { 'left' && moduleModel === HEATERSHAKER_MODULE_V1 } highlight={highlightLabware} + highlightShadow={highlightShadowLabware} /> ) : null} {moduleChildren} - {stacked ? : null} ) : null } )} + {/* render non-module labware and overlays */} {labwareOnDeck.map( ({ labwareLocation, @@ -292,7 +298,7 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { missingTips, onLabwareClick, highlight, - stacked = false, + highlightShadow, }) => { if ( labwareLocation === 'offDeck' || @@ -321,9 +327,75 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { wellFill={wellFill ?? undefined} missingTips={missingTips} highlight={highlight} + highlightShadow={highlightShadow} /> {labwareChildren} - {stacked ? : null} + + ) : null + } + )} + {/* render stacked badge on module labware */} + {modulesOnDeck.map( + ({ moduleModel, moduleLocation, stacked = false }) => { + const slotPosition = getPositionFromSlotId( + moduleLocation.slotName, + deckDef + ) + const moduleDef = getModuleDef2(moduleModel) + + const { + x: nestedLabwareOffsetX, + y: nestedLabwareOffsetY, + } = moduleDef.labwareOffset + + // labwareOffset values are more accurate than our SVG renderings, so ignore any deviations under a certain threshold + const clampedLabwareOffsetX = + Math.abs(nestedLabwareOffsetX) > LABWARE_OFFSET_DISPLAY_THRESHOLD + ? nestedLabwareOffsetX + : 0 + const clampedLabwareOffsetY = + Math.abs(nestedLabwareOffsetY) > LABWARE_OFFSET_DISPLAY_THRESHOLD + ? nestedLabwareOffsetY + : 0 + // transform to be applied to children which render within the labware interfacing surface of the module + const childrenTransform = `translate(${clampedLabwareOffsetX}, ${clampedLabwareOffsetY})` + + return slotPosition != null && stacked ? ( + + + + + + ) : null + } + )} + {/* render stacked badge on non-module labware */} + {labwareOnDeck.map( + ({ labwareLocation, definition, stacked = false }) => { + if ( + labwareLocation === 'offDeck' || + !('slotName' in labwareLocation) || + // for legacy protocols that list fixed trash as a labware, do not render + definition.parameters.loadName === + 'opentrons_1_trash_3200ml_fixed' + ) { + return null + } + + const slotPosition = getPositionFromSlotId( + labwareLocation.slotName, + deckDef + ) + + return slotPosition != null && stacked ? ( + + ) : null } diff --git a/components/src/hardware-sim/DeckConfigurator/index.tsx b/components/src/hardware-sim/DeckConfigurator/index.tsx index 9f1a9506a2f..25e2bd1c3a0 100644 --- a/components/src/hardware-sim/DeckConfigurator/index.tsx +++ b/components/src/hardware-sim/DeckConfigurator/index.tsx @@ -110,6 +110,10 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { const absorbanceReaderFixtures = deckConfig.filter( ({ cutoutFixtureId }) => cutoutFixtureId === ABSORBANCE_READER_V1_FIXTURE ) + const magneticBlockStagingAreaFixtures = deckConfig.filter( + ({ cutoutFixtureId }) => + cutoutFixtureId === STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE + ) return ( 0 || - wasteChuteStagingAreaFixtures.length > 0 + wasteChuteStagingAreaFixtures.length > 0 || + magneticBlockStagingAreaFixtures.length > 0 } /> {children} diff --git a/components/src/hardware-sim/Labware/LabwareAdapter/index.tsx b/components/src/hardware-sim/Labware/LabwareAdapter/index.tsx index fc05d8b5621..476fff397f6 100644 --- a/components/src/hardware-sim/Labware/LabwareAdapter/index.tsx +++ b/components/src/hardware-sim/Labware/LabwareAdapter/index.tsx @@ -25,12 +25,18 @@ export interface LabwareAdapterProps { labwareLoadName: LabwareAdapterLoadName definition?: LabwareDefinition2 highlight?: boolean + highlightShadow?: boolean } export const LabwareAdapter = ( props: LabwareAdapterProps ): JSX.Element | null => { - const { labwareLoadName, definition, highlight = false } = props + const { + labwareLoadName, + definition, + highlight = false, + highlightShadow, + } = props const highlightOutline = highlight && definition != null ? ( ) : null + const highlightShadowOutline = + highlight && definition != null ? ( + + ) : null const SVGElement = LABWARE_ADAPTER_LOADNAME_PATHS[labwareLoadName] return ( + {/** + * render an initial shadow outline first in the DOM so that the SVG highlight shadow + * does not layer over the inside of the SVG labware adapter + */} + {highlightShadowOutline} {highlightOutline} diff --git a/components/src/hardware-sim/Labware/LabwareRender.tsx b/components/src/hardware-sim/Labware/LabwareRender.tsx index 9137a2d2f15..f9b87336f7a 100644 --- a/components/src/hardware-sim/Labware/LabwareRender.tsx +++ b/components/src/hardware-sim/Labware/LabwareRender.tsx @@ -51,6 +51,8 @@ export interface LabwareRenderProps { labwareStroke?: CSSProperties['stroke'] /** adds thicker blue border with blur to labware */ highlight?: boolean + /** adds a drop shadow to the highlight border */ + highlightShadow?: boolean /** Optional callback, called with WellMouseEvent args onMouseEnter */ onMouseEnterWell?: (e: WellMouseEvent) => unknown /** Optional callback, called with WellMouseEvent args onMouseLeave */ @@ -90,6 +92,7 @@ export const LabwareRender = (props: LabwareRenderProps): JSX.Element => { labwareLoadName={labwareLoadName as LabwareAdapterLoadName} definition={definition} highlight={props.highlight} + highlightShadow={props.highlightShadow} /> @@ -107,6 +110,7 @@ export const LabwareRender = (props: LabwareRenderProps): JSX.Element => { onMouseLeaveWell={props.onMouseLeaveWell} onLabwareClick={props.onLabwareClick} highlight={props.highlight} + highlightShadow={props.highlightShadow} /> {props.wellStroke != null ? ( - + {/* * + * TODO(bh, 2024-08-23): layer drop shadow filters to mimic CSS box shadow - may need to evaluate performance + * https://stackoverflow.com/questions/22486039/css3-filter-drop-shadow-spread-property-alternatives + * */} + + + - {/* TODO(bh, 2024-07-22): adjust gaussian blur for stacks */} ) : ( diff --git a/components/src/hardware-sim/Labware/labwareInternals/StaticLabware.tsx b/components/src/hardware-sim/Labware/labwareInternals/StaticLabware.tsx index 4094ea1e038..b3441d0254a 100644 --- a/components/src/hardware-sim/Labware/labwareInternals/StaticLabware.tsx +++ b/components/src/hardware-sim/Labware/labwareInternals/StaticLabware.tsx @@ -17,6 +17,8 @@ export interface StaticLabwareProps { definition: LabwareDefinition2 /** Add thicker blurred blue border to labware, defaults to false */ highlight?: boolean + /** adds a drop shadow to the highlight border */ + highlightShadow?: boolean /** Optional callback to be executed when entire labware element is clicked */ onLabwareClick?: () => void /** Optional callback to be executed when mouse enters a well element */ @@ -55,6 +57,7 @@ export function StaticLabwareComponent(props: StaticLabwareProps): JSX.Element { const { definition, highlight, + highlightShadow, onLabwareClick, onMouseEnterWell, onMouseLeaveWell, @@ -69,6 +72,7 @@ export function StaticLabwareComponent(props: StaticLabwareProps): JSX.Element { diff --git a/components/src/hardware-sim/ProtocolDeck/index.tsx b/components/src/hardware-sim/ProtocolDeck/index.tsx index 366e7f51cf5..fb172dea93d 100644 --- a/components/src/hardware-sim/ProtocolDeck/index.tsx +++ b/components/src/hardware-sim/ProtocolDeck/index.tsx @@ -92,6 +92,10 @@ export function ProtocolDeck(props: ProtocolDeckProps): JSX.Element | null { ) : null, highlightLabware: handleLabwareClick != null, + highlightShadowLabware: + handleLabwareClick != null && + topLabwareDefinition != null && + topLabwareId != null, onLabwareClick: handleLabwareClick != null && topLabwareDefinition != null && @@ -140,6 +144,7 @@ export function ProtocolDeck(props: ProtocolDeckProps): JSX.Element | null { ) : null, highlight: handleLabwareClick != null, + highlightShadow: handleLabwareClick != null && isLabwareInStack, onLabwareClick: handleLabwareClick != null ? () => { diff --git a/components/src/molecules/ParametersTable/InfoScreen.tsx b/components/src/molecules/ParametersTable/InfoScreen.tsx index 9508c250e4b..55ba604e257 100644 --- a/components/src/molecules/ParametersTable/InfoScreen.tsx +++ b/components/src/molecules/ParametersTable/InfoScreen.tsx @@ -1,11 +1,12 @@ import * as React from 'react' import { BORDERS, COLORS } from '../../helix-design-system' -import { SPACING, TYPOGRAPHY } from '../../ui-style-constants/index' -import { LegacyStyledText } from '../../atoms/StyledText' +import { RESPONSIVENESS, SPACING } from '../../ui-style-constants/index' +import { StyledText } from '../../atoms/StyledText' import { Icon } from '../../icons' import { Flex } from '../../primitives' -import { ALIGN_CENTER, DIRECTION_COLUMN } from '../../styles' +import { ALIGN_CENTER, DIRECTION_COLUMN, JUSTIFY_CENTER } from '../../styles' +import { css } from 'styled-components' interface InfoScreenProps { contentType: @@ -15,15 +16,14 @@ interface InfoScreenProps { | 'labware' | 'noFiles' | 'noLabwareOffsetData' + | 'noLabwareOffsetDataYet' t?: any backgroundColor?: string + height?: string } -export function InfoScreen({ - contentType, - t, - backgroundColor, -}: InfoScreenProps): JSX.Element { +export function InfoScreen(props: InfoScreenProps): JSX.Element { + const { contentType, t, backgroundColor } = props let bodyText: string = '' switch (contentType) { case 'parameters': @@ -54,20 +54,42 @@ export function InfoScreen({ ? t('no_offsets_available') : 'No Labware Offset data available' break + case 'noLabwareOffsetDataYet': + bodyText = + t != null ? t('no_labware_offset_data') : 'No labware offset data yet' + break default: bodyText = contentType } return ( svg { + height: 1.25rem; + width: 1.25rem; + } + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + height: 27.25rem; + padding: 0; + grid-gap: ${SPACING.spacing32}; + background-color: ${backgroundColor ?? COLORS.grey35}; + border-radius: ${BORDERS.borderRadius12}; + > svg { + height: 3rem; + width: 3rem; + } + } + `} > - + {bodyText} - + ) } diff --git a/robot-server/robot_server/service/legacy/routers/networking.py b/robot-server/robot_server/service/legacy/routers/networking.py index 17f4a3364cc..b47cf283ddf 100644 --- a/robot-server/robot_server/service/legacy/routers/networking.py +++ b/robot-server/robot_server/service/legacy/routers/networking.py @@ -1,10 +1,10 @@ import logging import os import subprocess +from typing import Annotated, Optional from starlette import status from starlette.responses import JSONResponse -from typing import Annotated, Optional from fastapi import APIRouter, HTTPException, File, Path, UploadFile, Query from opentrons_shared_data.errors import ErrorCodes @@ -45,8 +45,20 @@ async def get_networking_status() -> NetworkingStatus: try: connectivity = await nmcli.is_connected() - # TODO(mc, 2020-09-17): interfaces should be typed - interfaces = {i.value: await nmcli.iface_info(i) for i in nmcli.NETWORK_IFACES} + + async def _permissive_get_iface( + i: nmcli.NETWORK_IFACES, + ) -> dict[str, dict[str, str | None]]: + try: + return {i.value: await nmcli.iface_info(i)} + except ValueError: + log.warning(f"Could not get state of iface {i.value}") + return {} + + interfaces: dict[str, dict[str, str | None]] = {} + for interface in nmcli.NETWORK_IFACES: + this_iface = await _permissive_get_iface(interface) + interfaces.update(this_iface) log.debug(f"Connectivity: {connectivity}") log.debug(f"Interfaces: {interfaces}") return NetworkingStatus( diff --git a/robot-server/robot_server/service/notifications/publisher_notifier.py b/robot-server/robot_server/service/notifications/publisher_notifier.py index f96ae9c3f96..89a53e27b64 100644 --- a/robot-server/robot_server/service/notifications/publisher_notifier.py +++ b/robot-server/robot_server/service/notifications/publisher_notifier.py @@ -1,5 +1,6 @@ """Provides an interface for alerting notification publishers to events and related lifecycle utilities.""" import asyncio +from logging import getLogger from fastapi import Depends from typing import Annotated, Optional, Callable, List, Awaitable, Union @@ -11,6 +12,8 @@ from opentrons.util.change_notifier import ChangeNotifier, ChangeNotifier_ts +LOG = getLogger(__name__) + class PublisherNotifier: """An interface that invokes notification callbacks whenever a generic notify event occurs.""" @@ -28,10 +31,9 @@ def register_publish_callbacks( def _initialize(self) -> None: """Initializes an instance of PublisherNotifier. This method should only be called once.""" - # fixme(mm, 2024-08-20): This task currently leaks; this class needs a close() - # method or something. This gets easier when app_setup.py switches to using a - # context manager for ASGI app setup and teardown. - self._notifier = asyncio.create_task(self._wait_for_event()) + self._notifier = asyncio.create_task( + self._wait_for_event(), name="Run publisher notifier" + ) def _notify_publishers(self) -> None: """A generic notifier, alerting all `waiters` of a change.""" @@ -39,10 +41,18 @@ def _notify_publishers(self) -> None: async def _wait_for_event(self) -> None: """Indefinitely wait for an event to occur, then invoke each callback.""" - while True: - await self._change_notifier.wait() - for callback in self._callbacks: - await callback() + try: + while True: + await self._change_notifier.wait() + for callback in self._callbacks: + try: + await callback() + except BaseException: + LOG.exception( + f'PublisherNotifier: exception in callback {getattr(callback, "__name__", "")}' + ) + except BaseException: + LOG.exception("PublisherNotifer notify task failed") _pe_publisher_notifier_accessor: AppStateAccessor[PublisherNotifier] = AppStateAccessor[ diff --git a/robot-server/tests/runs/test_run_controller.py b/robot-server/tests/runs/test_run_controller.py index 9f19e54d0cc..cab805a5ba4 100644 --- a/robot-server/tests/runs/test_run_controller.py +++ b/robot-server/tests/runs/test_run_controller.py @@ -215,6 +215,12 @@ async def test_create_play_action_to_start( times=1, ) + # Verify maintenance run publication after background task execution + decoy.verify( + mock_maintenance_runs_publisher.publish_current_maintenance_run(), + times=1, + ) + def test_create_pause_action( decoy: Decoy, diff --git a/robot-server/tests/runs/test_run_data_manager.py b/robot-server/tests/runs/test_run_data_manager.py index 1d67ff295fb..f97bcd359cf 100644 --- a/robot-server/tests/runs/test_run_data_manager.py +++ b/robot-server/tests/runs/test_run_data_manager.py @@ -668,6 +668,10 @@ async def test_update_current( mock_runs_publisher.publish_runs_advise_refetch(run_id), times=1, ) + decoy.verify( + mock_runs_publisher.publish_runs_advise_refetch(run_id), + times=1, + ) assert result == Run( current=False, id=run_resource.run_id, diff --git a/robot-server/tests/service/legacy/routers/test_networking.py b/robot-server/tests/service/legacy/routers/test_networking.py index a6185c66d7e..22ea2359a92 100755 --- a/robot-server/tests/service/legacy/routers/test_networking.py +++ b/robot-server/tests/service/legacy/routers/test_networking.py @@ -53,6 +53,35 @@ async def mock_is_connected(): assert resp.status_code == 500 +def test_networking_status_tolerates_bad_iface(api_client, monkeypatch): + connection_status = { + "eth0": { + "ipAddress": "169.254.229.173/16", + "macAddress": "B8:27:EB:39:C0:9A", + "gatewayAddress": None, + "state": "connecting (configuring)", + "type": "ethernet", + } + } + + async def mock_is_connected(): + return "full" + + async def mock_get_connection_status(iface): + if iface == nmcli.NETWORK_IFACES.WIFI: + raise ValueError("Oh no!") + else: + return connection_status["eth0"] + + monkeypatch.setattr(nmcli, "is_connected", mock_is_connected) + monkeypatch.setattr(nmcli, "iface_info", mock_get_connection_status) + expected = {"status": "full", "interfaces": connection_status} + resp = api_client.get("/networking/status") + body_json = resp.json() + assert resp.status_code == 200 + assert body_json == expected + + def test_wifi_list(api_client, monkeypatch): expected_res = [ { diff --git a/shared-data/python/opentrons_shared_data/robot/types.py b/shared-data/python/opentrons_shared_data/robot/types.py index e478957bc29..81d27fd34d7 100644 --- a/shared-data/python/opentrons_shared_data/robot/types.py +++ b/shared-data/python/opentrons_shared_data/robot/types.py @@ -37,6 +37,15 @@ class mountOffset(TypedDict): gripper: NotRequired[List[float]] +class paddingOffset(TypedDict): + """The padding offsets for a given robot type based off how far the pipettes can travel beyond the deck extents.""" + + rear: float + front: float + leftSide: float + rightSide: float + + class RobotDefinition(TypedDict): """A python version of the robot definition type.""" @@ -44,4 +53,5 @@ class RobotDefinition(TypedDict): robotType: RobotType models: List[str] extents: List[float] + paddingOffsets: paddingOffset mountOffsets: mountOffset diff --git a/shared-data/robot/definitions/1/ot2.json b/shared-data/robot/definitions/1/ot2.json index 50c6eb4256a..c1199f86045 100644 --- a/shared-data/robot/definitions/1/ot2.json +++ b/shared-data/robot/definitions/1/ot2.json @@ -3,6 +3,12 @@ "robotType": "OT-2 Standard", "models": ["OT-2 Standard", "OT-2 Refresh"], "extents": [446.75, 347.5, 0.0], + "paddingOffsets": { + "rear": -35.91, + "front": 31.89, + "leftSide": 0, + "rightSide": 0 + }, "mountOffsets": { "left": [-34.0, 0.0, 0.0], "right": [0.0, 0.0, 0.0] diff --git a/shared-data/robot/definitions/1/ot3.json b/shared-data/robot/definitions/1/ot3.json index eb3a943d886..05b0db6928c 100644 --- a/shared-data/robot/definitions/1/ot3.json +++ b/shared-data/robot/definitions/1/ot3.json @@ -3,6 +3,12 @@ "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32 + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], diff --git a/shared-data/robot/schemas/1.json b/shared-data/robot/schemas/1.json index 44e25e6caf5..f0c50eb0ca5 100644 --- a/shared-data/robot/schemas/1.json +++ b/shared-data/robot/schemas/1.json @@ -37,6 +37,29 @@ "description": "The maximum addressable coordinates of the deck without instruments.", "$ref": "#/definitions/xyzArray" }, + "paddingOffsets": { + "description": "The distance from a given edge of a deck extent by which the maximum amount of travel is limited.", + "type": "object", + "required": ["rear", "front", "leftSide", "rightSide"], + "properties": { + "rear": { + "description": "The padding distance from the rear edge of the deck extents which the front nozzles of a pipette must not exceed.", + "type": "number" + }, + "front": { + "description": "The padding distance from the front edge of the deck extents which the rear nozzles of a pipette must not exceed.", + "type": "number" + }, + "leftSide": { + "description": "The padding distance from the left edge of the deck extents which the right-most nozzles of a pipette must not exceed.", + "type": "number" + }, + "rightSide": { + "description": "The padding distance from the right edge of the deck extents which the left-most nozzles of a pipette must not exceed.", + "type": "number" + } + } + }, "mountOffsets": { "description": "The physical mount offsets from the center of the instrument carriage.", "type": "object",